neon 3.0.0 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/README.md +70 -5
  2. package/dist/_chunks/auth_selection-DGgq6ifc.js +83 -0
  3. package/dist/_chunks/cmd_pipeline-CUbBO9U_.js +2818 -0
  4. package/dist/_chunks/credentials-MYdHdKah.js +188 -0
  5. package/dist/_chunks/env-NbA61JR3.js +585 -0
  6. package/dist/_chunks/env_services-Tz9G4JeT.js +531 -0
  7. package/dist/_chunks/paths-DMq0Lt7a.js +151 -0
  8. package/dist/_chunks/profiles-Ir29rqns.js +217 -0
  9. package/dist/_chunks/psql-DWH-kc69.js +2169 -0
  10. package/dist/_chunks/rolldown-runtime-D7D4PA-g.js +13 -0
  11. package/dist/_chunks/secure_file-BucZj4yQ.js +39 -0
  12. package/dist/analytics.js +163 -207
  13. package/dist/api.js +815 -758
  14. package/dist/auth.js +121 -141
  15. package/dist/auth_context.js +39 -53
  16. package/dist/cli.js +4 -7
  17. package/dist/commands/api.js +220 -250
  18. package/dist/commands/api_keys.js +251 -314
  19. package/dist/commands/auth.js +283 -328
  20. package/dist/commands/bootstrap.js +372 -437
  21. package/dist/commands/branches.js +304 -455
  22. package/dist/commands/bucket.js +374 -514
  23. package/dist/commands/checkout.js +213 -298
  24. package/dist/commands/config.js +573 -690
  25. package/dist/commands/connection_string.js +137 -165
  26. package/dist/commands/data_api.js +238 -260
  27. package/dist/commands/databases.js +67 -76
  28. package/dist/commands/deploy.js +31 -25
  29. package/dist/commands/dev.js +639 -719
  30. package/dist/commands/diff.js +156 -200
  31. package/dist/commands/env.js +255 -305
  32. package/dist/commands/functions.js +275 -355
  33. package/dist/commands/index.js +70 -65
  34. package/dist/commands/init.js +84 -119
  35. package/dist/commands/inspect.js +55 -55
  36. package/dist/commands/ip_allow.js +88 -120
  37. package/dist/commands/link.js +874 -1019
  38. package/dist/commands/logs.js +291 -0
  39. package/dist/commands/neon_auth.js +725 -933
  40. package/dist/commands/operations.js +34 -25
  41. package/dist/commands/orgs.js +28 -18
  42. package/dist/commands/profile.js +615 -846
  43. package/dist/commands/projects.js +313 -373
  44. package/dist/commands/psql.js +60 -58
  45. package/dist/commands/roles.js +55 -58
  46. package/dist/commands/schema_diff.js +87 -131
  47. package/dist/commands/set_context.js +34 -26
  48. package/dist/commands/snapshots.js +288 -413
  49. package/dist/commands/status.js +41 -37
  50. package/dist/commands/user.js +21 -10
  51. package/dist/commands/vpc_endpoints.js +85 -113
  52. package/dist/config.js +7 -6
  53. package/dist/config_format.js +50 -66
  54. package/dist/config_template.js +128 -157
  55. package/dist/context.js +183 -235
  56. package/dist/current_branch_fast_path.js +40 -49
  57. package/dist/dev/env.js +2 -446
  58. package/dist/dev/functions.js +54 -68
  59. package/dist/dev/inputs.js +46 -58
  60. package/dist/dev/runtime.js +135 -164
  61. package/dist/dev/websocket.js +766 -959
  62. package/dist/env.js +27 -33
  63. package/dist/env_file.js +118 -132
  64. package/dist/env_services.js +2 -51
  65. package/dist/errors.js +57 -68
  66. package/dist/functions_api.js +45 -43
  67. package/dist/help.js +189 -140
  68. package/dist/index.js +182 -257
  69. package/dist/init/agents.js +137 -118
  70. package/dist/init/auth.js +58 -68
  71. package/dist/init/bootstrap.js +325 -396
  72. package/dist/init/build_config.js +4 -2
  73. package/dist/init/detect_agent.js +56 -101
  74. package/dist/init/editors.js +35 -52
  75. package/dist/init/enrich_output.js +51 -66
  76. package/dist/init/extension.js +134 -171
  77. package/dist/init/inspect.js +179 -266
  78. package/dist/init/interactive.js +510 -622
  79. package/dist/init/neonctl.js +117 -168
  80. package/dist/init/orchestrate.js +157 -173
  81. package/dist/init/phases/auth.js +188 -202
  82. package/dist/init/phases/cleanup.js +23 -23
  83. package/dist/init/phases/db.js +251 -277
  84. package/dist/init/phases/getting_started.js +213 -223
  85. package/dist/init/phases/mcp.js +174 -224
  86. package/dist/init/phases/migrations.js +247 -248
  87. package/dist/init/phases/neon_auth.js +114 -133
  88. package/dist/init/phases/setup.js +546 -703
  89. package/dist/init/phases/skills.js +75 -86
  90. package/dist/init/phases/status.js +72 -67
  91. package/dist/init/resolve_context.js +102 -99
  92. package/dist/init/route_command.js +91 -98
  93. package/dist/init/skills.js +174 -218
  94. package/dist/init/vsix.js +77 -99
  95. package/dist/log.js +17 -16
  96. package/dist/neon_services.js +104 -129
  97. package/dist/parameters.gen.js +481 -471
  98. package/dist/pkg.js +17 -19
  99. package/dist/profile_keys.js +44 -47
  100. package/dist/psql/cli.js +44 -47
  101. package/dist/psql/command/cmd_cond.js +231 -406
  102. package/dist/psql/command/cmd_connect.js +557 -764
  103. package/dist/psql/command/cmd_copy.js +728 -984
  104. package/dist/psql/command/cmd_describe.js +1499 -1688
  105. package/dist/psql/command/cmd_format.js +733 -905
  106. package/dist/psql/command/cmd_io.js +2 -2193
  107. package/dist/psql/command/cmd_lo.js +297 -359
  108. package/dist/psql/command/cmd_meta.js +727 -878
  109. package/dist/psql/command/cmd_misc.js +138 -172
  110. package/dist/psql/command/cmd_pipeline.js +2 -1148
  111. package/dist/psql/command/cmd_restrict.js +119 -155
  112. package/dist/psql/command/cmd_show.js +529 -688
  113. package/dist/psql/command/dispatch.js +260 -325
  114. package/dist/psql/command/inputQueue.js +35 -33
  115. package/dist/psql/command/shared.js +49 -63
  116. package/dist/psql/complete/filenames.js +90 -133
  117. package/dist/psql/complete/index.js +59 -97
  118. package/dist/psql/complete/matcher.js +236 -300
  119. package/dist/psql/complete/psqlVars.js +218 -223
  120. package/dist/psql/complete/queries.js +159 -177
  121. package/dist/psql/complete/rules.js +1493 -2299
  122. package/dist/psql/core/common.js +2 -1253
  123. package/dist/psql/core/help.js +456 -546
  124. package/dist/psql/core/mainloop.js +692 -1303
  125. package/dist/psql/core/prompt.js +391 -408
  126. package/dist/psql/core/settings.js +429 -644
  127. package/dist/psql/core/sqlHelp.js +480 -554
  128. package/dist/psql/core/startup.js +2 -846
  129. package/dist/psql/core/syncVars.js +67 -110
  130. package/dist/psql/core/variables.js +156 -278
  131. package/dist/psql/describe/formatters.js +884 -1285
  132. package/dist/psql/describe/processNamePattern.js +173 -260
  133. package/dist/psql/describe/queries.js +1368 -2403
  134. package/dist/psql/describe/versionGate.js +32 -41
  135. package/dist/psql/index.js +2 -2030
  136. package/dist/psql/io/history.js +232 -271
  137. package/dist/psql/io/input.js +103 -108
  138. package/dist/psql/io/lineEditor/buffer.js +238 -319
  139. package/dist/psql/io/lineEditor/complete.js +135 -213
  140. package/dist/psql/io/lineEditor/filename.js +139 -148
  141. package/dist/psql/io/lineEditor/index.js +653 -870
  142. package/dist/psql/io/lineEditor/keymap.js +544 -702
  143. package/dist/psql/io/lineEditor/vt100.js +294 -341
  144. package/dist/psql/io/pgpass.js +158 -187
  145. package/dist/psql/io/pgservice.js +146 -183
  146. package/dist/psql/io/psqlrc.js +328 -403
  147. package/dist/psql/print/aligned.js +1020 -1683
  148. package/dist/psql/print/asciidoc.js +180 -214
  149. package/dist/psql/print/crosstab.js +281 -442
  150. package/dist/psql/print/csv.js +48 -70
  151. package/dist/psql/print/html.js +195 -226
  152. package/dist/psql/print/json.js +75 -88
  153. package/dist/psql/print/latex.js +291 -364
  154. package/dist/psql/print/pager.js +171 -242
  155. package/dist/psql/print/troff.js +194 -226
  156. package/dist/psql/print/unaligned.js +69 -95
  157. package/dist/psql/print/units.js +167 -169
  158. package/dist/psql/scanner/slash.js +428 -483
  159. package/dist/psql/scanner/sql.js +445 -889
  160. package/dist/psql/scanner/stringutils.js +309 -379
  161. package/dist/psql/types/index.js +8 -7
  162. package/dist/psql/types/scanner.js +25 -22
  163. package/dist/psql/wire/connection.js +2042 -2803
  164. package/dist/psql/wire/copy.js +84 -100
  165. package/dist/psql/wire/notify.js +39 -59
  166. package/dist/psql/wire/pipeline.js +305 -518
  167. package/dist/psql/wire/protocol.js +349 -417
  168. package/dist/psql/wire/sasl.js +180 -265
  169. package/dist/psql/wire/tls.js +400 -561
  170. package/dist/storage_api.js +115 -129
  171. package/dist/test_utils/fixtures.js +94 -113
  172. package/dist/test_utils/oauth_server.js +10 -7
  173. package/dist/test_utils/project_dir.js +33 -0
  174. package/dist/utils/ai_gateway_notice.js +131 -162
  175. package/dist/utils/api_enums.js +21 -28
  176. package/dist/utils/auth.js +10 -4
  177. package/dist/utils/branch_notice.js +20 -19
  178. package/dist/utils/branch_picker.js +83 -89
  179. package/dist/utils/cli_name.js +15 -12
  180. package/dist/utils/compute_units.js +20 -27
  181. package/dist/utils/config_diff.js +127 -158
  182. package/dist/utils/enrichers.js +95 -148
  183. package/dist/utils/esbuild.js +130 -189
  184. package/dist/utils/flags.js +35 -47
  185. package/dist/utils/formats.js +8 -15
  186. package/dist/utils/git_diff.js +69 -80
  187. package/dist/utils/inspect_db.js +101 -143
  188. package/dist/utils/inspect_queries.js +179 -142
  189. package/dist/utils/middlewares.js +39 -45
  190. package/dist/utils/openapi.js +87 -99
  191. package/dist/utils/package_manager.js +312 -110
  192. package/dist/utils/point_in_time.js +49 -53
  193. package/dist/utils/psql.js +89 -106
  194. package/dist/utils/service_picker.js +55 -58
  195. package/dist/utils/string.js +5 -5
  196. package/dist/utils/ui.js +38 -55
  197. package/dist/utils/write_sync.js +26 -35
  198. package/dist/utils/zip.js +4 -3
  199. package/dist/writer.js +67 -87
  200. package/package.json +11 -6
  201. package/dist/_shared/auth_selection.js +0 -86
  202. package/dist/_shared/credentials.js +0 -209
  203. package/dist/_shared/env-core/env.js +0 -558
  204. package/dist/_shared/env-core/reuse-secrets.js +0 -223
  205. package/dist/_shared/paths.js +0 -148
  206. package/dist/_shared/profiles.js +0 -276
  207. package/dist/_shared/secure_file.js +0 -43
@@ -1,2857 +1,2096 @@
1
- /**
2
- * PgConnection — the wire-layer Connection implementation (WP-02 + WP-16).
3
- *
4
- * Implements `Connection` (frozen WP-00 interface in
5
- * `src/psql/types/connection.ts`) on top of a single TCP / TLS socket. The
6
- * class is essentially a state machine:
7
- *
8
- * auth → drive Authentication* messages
9
- * await-ready → collect ParameterStatus + BackendKeyData
10
- * idle → ready for a Query / extended-protocol cycle
11
- * in-query → simple Query in flight, accumulating ResultSets
12
- * in-copy-in → COPY FROM STDIN data transfer in progress (WP-16)
13
- * in-copy-out → COPY TO STDOUT data transfer in progress (WP-16)
14
- * closed → socket gone
15
- *
16
- * What this module owns:
17
- * - Socket lifecycle (open, optional TLS, Terminate, close).
18
- * - Auth: Cleartext, MD5, SASL/SCRAM (delegating to ./sasl.ts).
19
- * - Tracking server `serverVersion` from ParameterStatus.
20
- * - Simple-query path (`execSimple`) — collect RowDescription/DataRow/
21
- * CommandComplete tuples into ResultSet[].
22
- * - Async messages (Notice, Notification) routed through ./notify.ts.
23
- * - ErrorResponse → rejected promise mapped to ConnectError shape.
24
- * - Async cancel via a side connection (CancelRequest).
25
- * - COPY streaming (WP-16): `startCopyIn` returns a `CopyInStream` that
26
- * wires CopyData / CopyDone / CopyFail; `startCopyOut` returns an
27
- * `AsyncIterable<Buffer>` that drains CopyData messages from the wire.
28
- * The state machine threads through `in-copy-in` / `in-copy-out`.
29
- *
30
- * What is stubbed / deferred:
31
- * - Extended-query protocol (Parse/Bind/Execute/Sync). The framing is in
32
- * protocol.ts but the high-level path (parameterised `query()`, prepared
33
- * statements, pipeline) is WP-21. We throw clearly when called.
34
- */
35
- import { Buffer } from "node:buffer";
36
- import { createHash } from "node:crypto";
37
- import * as dns from "node:dns/promises";
38
- import * as net from "node:net";
39
- import * as tls from "node:tls";
40
1
  import { NoticeMultiplexer } from "./notify.js";
2
+ import { Bind, CancelRequest, Close, CopyData, CopyDone, CopyFail, Describe, Execute, MessageParser, Parse, PasswordMessage, Query, SASLInitialResponse, SASLResponse, StartupMessage, Sync, Terminate, fieldsToNotice } from "./protocol.js";
41
3
  import { PipelineSession } from "./pipeline.js";
42
- import { Bind, CancelRequest, Close, CopyData, CopyDone, CopyFail, Describe, Execute, fieldsToNotice, MessageParser, Parse, PasswordMessage, Query, SASLInitialResponse, SASLResponse, StartupMessage, Sync, Terminate, } from "./protocol.js";
43
4
  import { createScramClient } from "./sasl.js";
44
5
  import { negotiateTls } from "./tls.js";
6
+ import { createHash } from "node:crypto";
7
+ import { Buffer } from "node:buffer";
8
+ import * as dns from "node:dns/promises";
9
+ import * as net from "node:net";
10
+ import * as tls from "node:tls";
11
+ //#region src/psql/wire/connection.ts
12
+ /**
13
+ * PgConnection — the wire-layer Connection implementation (WP-02 + WP-16).
14
+ *
15
+ * Implements `Connection` (frozen WP-00 interface in
16
+ * `src/psql/types/connection.ts`) on top of a single TCP / TLS socket. The
17
+ * class is essentially a state machine:
18
+ *
19
+ * auth → drive Authentication* messages
20
+ * await-ready → collect ParameterStatus + BackendKeyData
21
+ * idle → ready for a Query / extended-protocol cycle
22
+ * in-query → simple Query in flight, accumulating ResultSets
23
+ * in-copy-in → COPY FROM STDIN data transfer in progress (WP-16)
24
+ * in-copy-out → COPY TO STDOUT data transfer in progress (WP-16)
25
+ * closed → socket gone
26
+ *
27
+ * What this module owns:
28
+ * - Socket lifecycle (open, optional TLS, Terminate, close).
29
+ * - Auth: Cleartext, MD5, SASL/SCRAM (delegating to ./sasl.ts).
30
+ * - Tracking server `serverVersion` from ParameterStatus.
31
+ * - Simple-query path (`execSimple`) — collect RowDescription/DataRow/
32
+ * CommandComplete tuples into ResultSet[].
33
+ * - Async messages (Notice, Notification) routed through ./notify.ts.
34
+ * - ErrorResponse → rejected promise mapped to ConnectError shape.
35
+ * - Async cancel via a side connection (CancelRequest).
36
+ * - COPY streaming (WP-16): `startCopyIn` returns a `CopyInStream` that
37
+ * wires CopyData / CopyDone / CopyFail; `startCopyOut` returns an
38
+ * `AsyncIterable<Buffer>` that drains CopyData messages from the wire.
39
+ * The state machine threads through `in-copy-in` / `in-copy-out`.
40
+ *
41
+ * What is stubbed / deferred:
42
+ * - Extended-query protocol (Parse/Bind/Execute/Sync). The framing is in
43
+ * protocol.ts but the high-level path (parameterised `query()`, prepared
44
+ * statements, pipeline) is WP-21. We throw clearly when called.
45
+ */
45
46
  /**
46
- * Map a Notice-flavoured fields map into a `ConnectError` that includes a
47
- * recognizable `message` plus a `cause` slot for the raw record.
48
- */
47
+ * Map a Notice-flavoured fields map into a `ConnectError` that includes a
48
+ * recognizable `message` plus a `cause` slot for the raw record.
49
+ */
49
50
  function fieldsToConnectError(fields) {
50
- return { ...fieldsToNotice(fields), cause: fields };
51
+ return {
52
+ ...fieldsToNotice(fields),
53
+ cause: fields
54
+ };
51
55
  }
52
56
  /**
53
- * Synthetic ConnectError used as the rejection reason for queued
54
- * pipeline ops that the server skipped after a preceding ErrorResponse.
55
- * Mirrors libpq's `PGRES_PIPELINE_ABORTED` result — the message text
56
- * matches the string libpq stamps onto skipped ops so the cmd layer
57
- * can render it byte-identically with vanilla psql's `\getresults` /
58
- * `\endpipeline` output.
59
- */
57
+ * Synthetic ConnectError used as the rejection reason for queued
58
+ * pipeline ops that the server skipped after a preceding ErrorResponse.
59
+ * Mirrors libpq's `PGRES_PIPELINE_ABORTED` result — the message text
60
+ * matches the string libpq stamps onto skipped ops so the cmd layer
61
+ * can render it byte-identically with vanilla psql's `\getresults` /
62
+ * `\endpipeline` output.
63
+ */
60
64
  function pipelineAbortedError() {
61
- return {
62
- severity: "ERROR",
63
- code: "",
64
- message: "Pipeline aborted, command did not run",
65
- pipelineAborted: true,
66
- };
65
+ return {
66
+ severity: "ERROR",
67
+ code: "",
68
+ message: "Pipeline aborted, command did not run",
69
+ pipelineAborted: true
70
+ };
67
71
  }
68
72
  /** Parse "PostgreSQL 16.2 …" into a numeric `major * 10000 + minor * 100`. */
69
73
  function parseServerVersion(value) {
70
- // libpq's PQserverVersion returns NNNNNN (e.g. 160002 for 16.2). For
71
- // pre-10 versions the layout was NNMMSS (e.g. 90608 for 9.6.8). Our
72
- // consumers only need monotonic comparability and a major-version
73
- // accessor; the libpq formula is the simplest match.
74
- const m = /^([0-9]+)(?:\.([0-9]+))?(?:\.([0-9]+))?/.exec(value);
75
- if (!m)
76
- return 0;
77
- const major = parseInt(m[1], 10);
78
- const minor = m[2] !== undefined ? parseInt(m[2], 10) : 0;
79
- if (major >= 10) {
80
- return major * 10000 + minor;
81
- }
82
- const patch = m[3] !== undefined ? parseInt(m[3], 10) : 0;
83
- return major * 10000 + minor * 100 + patch;
74
+ const m = /^([0-9]+)(?:\.([0-9]+))?(?:\.([0-9]+))?/.exec(value);
75
+ if (!m) return 0;
76
+ const major = parseInt(m[1], 10);
77
+ const minor = m[2] !== void 0 ? parseInt(m[2], 10) : 0;
78
+ if (major >= 10) return major * 1e4 + minor;
79
+ const patch = m[3] !== void 0 ? parseInt(m[3], 10) : 0;
80
+ return major * 1e4 + minor * 100 + patch;
84
81
  }
85
82
  /**
86
- * MD5 auth: `'md5' + md5( md5(password + user) || salt )`. Inner hash uses
87
- * the username; outer is salted. PG uses lowercase hex everywhere.
88
- */
83
+ * MD5 auth: `'md5' + md5( md5(password + user) || salt )`. Inner hash uses
84
+ * the username; outer is salted. PG uses lowercase hex everywhere.
85
+ */
89
86
  function md5AuthPayload(user, password, salt) {
90
- const inner = createHash("md5")
91
- .update(password + user, "utf8")
92
- .digest("hex");
93
- const outer = createHash("md5")
94
- .update(inner, "utf8")
95
- .update(salt)
96
- .digest("hex");
97
- return "md5" + outer;
87
+ const inner = createHash("md5").update(password + user, "utf8").digest("hex");
88
+ return "md5" + createHash("md5").update(inner, "utf8").update(salt).digest("hex");
98
89
  }
99
90
  /**
100
- * Fisher-Yates in-place shuffle of the candidate hosts list. Used by
101
- * `load_balance_hosts=random`. Hook the random source so tests can inject a
102
- * deterministic permutation.
103
- */
91
+ * Fisher-Yates in-place shuffle of the candidate hosts list. Used by
92
+ * `load_balance_hosts=random`. Hook the random source so tests can inject a
93
+ * deterministic permutation.
94
+ */
104
95
  function shuffleInPlace(arr, rng = Math.random) {
105
- for (let i = arr.length - 1; i > 0; i -= 1) {
106
- const j = Math.floor(rng() * (i + 1));
107
- const tmp = arr[i];
108
- arr[i] = arr[j];
109
- arr[j] = tmp;
110
- }
96
+ for (let i = arr.length - 1; i > 0; i -= 1) {
97
+ const j = Math.floor(rng() * (i + 1));
98
+ const tmp = arr[i];
99
+ arr[i] = arr[j];
100
+ arr[j] = tmp;
101
+ }
111
102
  }
112
103
  /**
113
- * After a successful handshake, decide whether this connection matches the
114
- * caller's `target_session_attrs` constraint by running
115
- * `SELECT pg_is_in_recovery()`. Returns `true` if the connection is
116
- * acceptable, `false` if it should be torn down and the next host tried.
117
- *
118
- * - 'any' (or undefined) — always accepts.
119
- * - 'read-write' / 'primary' — accepts when NOT in recovery.
120
- * - 'read-only' / 'standby' — accepts when IN recovery.
121
- *
122
- * If the probe query itself fails we treat the connection as unacceptable
123
- * (returns false) so the orchestrator falls through to the next candidate
124
- * rather than handing the caller a half-broken connection.
125
- */
104
+ * After a successful handshake, decide whether this connection matches the
105
+ * caller's `target_session_attrs` constraint by running
106
+ * `SELECT pg_is_in_recovery()`. Returns `true` if the connection is
107
+ * acceptable, `false` if it should be torn down and the next host tried.
108
+ *
109
+ * - 'any' (or undefined) — always accepts.
110
+ * - 'read-write' / 'primary' — accepts when NOT in recovery.
111
+ * - 'read-only' / 'standby' — accepts when IN recovery.
112
+ *
113
+ * If the probe query itself fails we treat the connection as unacceptable
114
+ * (returns false) so the orchestrator falls through to the next candidate
115
+ * rather than handing the caller a half-broken connection.
116
+ */
126
117
  async function checkSessionAttrs(conn, tsa) {
127
- if (tsa === undefined || tsa === "any")
128
- return true;
129
- let inRecovery;
130
- try {
131
- const sets = await conn.execSimple("SELECT pg_is_in_recovery()");
132
- if (sets.length === 0 || sets[0].rows.length === 0) {
133
- return false;
134
- }
135
- const raw = sets[0].rows[0][0];
136
- // pg_is_in_recovery returns boolean; text-format rows surface as
137
- // 't' / 'f'. Be liberal: also handle 'true' / 'false'.
138
- if (raw === true) {
139
- inRecovery = true;
140
- }
141
- else if (raw === false) {
142
- inRecovery = false;
143
- }
144
- else if (typeof raw === "string") {
145
- inRecovery = raw === "t" || raw.toLowerCase() === "true";
146
- }
147
- else {
148
- return false;
149
- }
150
- }
151
- catch {
152
- return false;
153
- }
154
- switch (tsa) {
155
- case "read-write":
156
- case "primary":
157
- return !inRecovery;
158
- case "read-only":
159
- case "standby":
160
- return inRecovery;
161
- // 'prefer-standby' is unwrapped by the orchestrator into two passes
162
- // ('standby' then 'any'), so we never see it here. Fall through to
163
- // accept-any for safety.
164
- default:
165
- return true;
166
- }
118
+ if (tsa === void 0 || tsa === "any") return true;
119
+ let inRecovery;
120
+ try {
121
+ const sets = await conn.execSimple("SELECT pg_is_in_recovery()");
122
+ if (sets.length === 0 || sets[0].rows.length === 0) return false;
123
+ const raw = sets[0].rows[0][0];
124
+ if (raw === true) inRecovery = true;
125
+ else if (raw === false) inRecovery = false;
126
+ else if (typeof raw === "string") inRecovery = raw === "t" || raw.toLowerCase() === "true";
127
+ else return false;
128
+ } catch {
129
+ return false;
130
+ }
131
+ switch (tsa) {
132
+ case "read-write":
133
+ case "primary": return !inRecovery;
134
+ case "read-only":
135
+ case "standby": return inRecovery;
136
+ default: return true;
137
+ }
167
138
  }
168
139
  /**
169
- * COPY-OUT backpressure thresholds (bytes). When buffered data exceeds the
170
- * high-water mark we pause the socket; the consumer's `next()` resumes it once
171
- * it drains below the low-water mark. Bounds RSS for a server that produces
172
- * rows faster than the sink consumes them (review item #11).
173
- */
174
- const COPY_OUT_HWM = 8 * 1024 * 1024;
175
- const COPY_OUT_LWM = 1 * 1024 * 1024;
176
- export class PgConnection {
177
- /**
178
- * Queue one COPY-FROM-STDIN data block. Call once per expected
179
- * CopyInResponse, in the order they will arrive during the upcoming
180
- * `execSimple`. The mainloop owns the parsing of `\.`-terminated stdin
181
- * blocks; we just buffer the raw bytes and ship them when the server is
182
- * ready.
183
- */
184
- queueCopyInData(data) {
185
- this.copyInMidBatchQueue.push(data);
186
- }
187
- /** Drop any queued COPY-FROM-STDIN data blocks. */
188
- clearCopyInDataQueue() {
189
- this.copyInMidBatchQueue.length = 0;
190
- }
191
- constructor(socket, opts, channelBindingData) {
192
- this.parser = new MessageParser();
193
- // -- Backend state
194
- this.serverVersion = 0;
195
- this.params = new Map();
196
- this.processId = 0;
197
- this.secretKey = 0;
198
- this.txStatus = "I";
199
- // -- Connection state machine
200
- this.state = "auth";
201
- this.pendingQuery = null;
202
- this.extDriver = null;
203
- /** True when `pipeline()` has handed out a PipelineSession (WP-21). */
204
- this._extPipelineActive = false;
205
- this.copyIn = null;
206
- this.copyOut = null;
207
- /**
208
- * Resolver for `startCopyIn` / `startCopyOut`. Captured when the caller has
209
- * already wired the `Query(COPY …)` and is now waiting for the server's
210
- * CopyInResponse / CopyOutResponse to confirm that the protocol switched.
211
- */
212
- this.copyStartResolve = null;
213
- this.copyStartReject = null;
214
- /**
215
- * Last-COPY command tag (e.g. `"COPY 17"`), or `null` if no COPY has run
216
- * since connection startup. Used by the `\copy` command runner to print the
217
- * upstream-style "COPY N" footer.
218
- */
219
- this.lastCopyTag = null;
220
- /**
221
- * Pre-buffered CopyData payloads keyed to the order of CopyInResponse
222
- * messages we expect to see during the next `execSimple`. Used to drive
223
- * `COPY ... FROM STDIN` segments that appear as part of a `\;`-chained
224
- * simple-query batch — the mainloop pre-scans its input for `\.`-terminated
225
- * COPY data blocks, hands the bytes in here, and the in-query dispatcher
226
- * pops the head buffer when a CopyInResponse arrives. Each buffer becomes
227
- * one CopyData frame followed by CopyDone (the upstream wire shape).
228
- *
229
- * If the queue is empty when CopyInResponse arrives, the wire layer falls
230
- * back to CopyFail so the connection doesn't deadlock.
231
- */
232
- this.copyInMidBatchQueue = [];
233
- /**
234
- * Sink for COPY-TO-STDOUT mid-batch data. When `COPY ... TO STDOUT` is one
235
- * segment of a `\;`-chained simple-query batch, the server pushes CopyData
236
- * messages to us mid-`execSimple`. With no `startCopyOut` driver active,
237
- * upstream `handleCopyOut` would write the bytes verbatim to the caller's
238
- * output stream. We expose a settable sink so the mainloop can wire stdout
239
- * (or any other WritableStream); if unset, the bytes are dropped (matching
240
- * libpq's behaviour when `PQexec` lacks a copy handler — the data still
241
- * gets consumed off the wire so the protocol stays in sync, but is
242
- * silently discarded).
243
- */
244
- this.copyOutMidBatchSink = null;
245
- /**
246
- * Mid-batch COPY-OUT state. While `true`, CopyData/CopyDone messages
247
- * arriving in `handleQueryMessage` are routed to the sink rather than
248
- * triggering the "unexpected message" diagnostic. Flipped on by
249
- * CopyOutResponse and off again when CopyDone arrives.
250
- */
251
- this.copyOutMidBatchActive = false;
252
- /**
253
- * Set to `true` once we actually respond to a server password challenge
254
- * (cleartext, MD5, or the SASL/SCRAM exchange). Mirrors libpq's
255
- * `conn->password_needed`, which `\conninfo` reports as "Password Used".
256
- * A trust/cert/peer login leaves this `false` — the password may have been
257
- * supplied but was never sent.
258
- */
259
- this.passwordUsed = false;
260
- // -- Async messages
261
- this.notify = new NoticeMultiplexer();
262
- // -- Auth state (only used during state=auth)
263
- this.scram = null;
264
- /**
265
- * True once the server has sent ANY authentication challenge (Cleartext,
266
- * MD5, SASL, ...). Used by the AuthenticationOk handler to distinguish
267
- * "server picked trust/cert auth" (no challenge, method=`none`) from
268
- * "server just completed a SASL exchange" (challenge seen, method
269
- * already validated when the challenge arrived).
270
- */
271
- this.authChallengeSeen = false;
272
- /**
273
- * True once `AuthenticationSASLFinal` arrived AND `scram.finish()` verified
274
- * the server signature (RFC 5802 §3 mutual auth). Used to reject an
275
- * `AuthenticationOk` that skips SASLFinal — a rogue/MITM server that doesn't
276
- * know the password could otherwise send SASLContinue then jump straight to
277
- * AuthenticationOk and never have its signature checked (review item #8).
278
- */
279
- this.saslFinalSeen = false;
280
- // -- Error-after-close guard
281
- this.socketError = null;
282
- this.startupResolve = null;
283
- this.startupReject = null;
284
- this.socket = socket;
285
- this.opts = opts;
286
- this.channelBindingData = channelBindingData;
287
- this._password = opts.password ?? null;
288
- socket.on("data", (chunk) => {
289
- this.onData(chunk);
290
- });
291
- socket.on("error", (err) => {
292
- this.socketError = err;
293
- this.failPending(err);
294
- });
295
- socket.on("close", () => {
296
- if (this.state !== "closed") {
297
- this.state = "closed";
298
- this.failPending(this.socketError ?? new Error("Socket closed"));
299
- }
300
- });
301
- }
302
- /**
303
- * Open a Postgres connection. Supports multi-host (`opts.hosts`) with
304
- * sequential or random iteration, a `target_session_attrs` filter, and the
305
- * libpq-style `prefer-standby` two-pass fallback.
306
- *
307
- * Iteration semantics:
308
- * 1. Build the candidate list from `opts.hosts` (preferred) or
309
- * `[{host: opts.host, port: opts.port}]`.
310
- * 2. If `loadBalanceHosts === 'random'`, Fisher-Yates shuffle in place.
311
- * 3. For each candidate (in order), attempt: openSocket → TLS → auth →
312
- * startup. On failure, record the error and try the next.
313
- * 4. On successful handshake, if `target_session_attrs` is restrictive,
314
- * run `SELECT pg_is_in_recovery()`. If the role doesn't match, close
315
- * this connection and try the next.
316
- * 5. `prefer-standby` runs TWO passes: first accepting only standbys,
317
- * second falling back to any host.
318
- * 6. If no candidate succeeds, throw the LAST encountered error
319
- * (preserves the most-recent failure mode for diagnostics).
320
- */
321
- static async connect(opts) {
322
- const seed = opts.hosts !== undefined && opts.hosts.length > 0
323
- ? [...opts.hosts]
324
- : [{ host: opts.host, port: opts.port }];
325
- // DNS fan-out: a single hostname can resolve to multiple A/AAAA records,
326
- // and libpq treats each resulting IP as its own candidate so the
327
- // iteration walks the FLAT (ip, port) list, not the (hostname, port)
328
- // list. Each candidate carries BOTH the original hostname (for TLS
329
- // SNI / SAN verification + `conn.host` reporting) AND the resolved
330
- // address (used only by `net.connect`). Unix-domain socket paths
331
- // and IP literals bypass the lookup — they become `{host, port}`
332
- // with no address override.
333
- //
334
- // Active for every mode, not just `load_balance_hosts=random`: even
335
- // `disable` benefits from the fall-through behaviour when the first
336
- // A record is dead. Mirrors upstream `004_load_balance_dns.pl`.
337
- //
338
- // `hostaddr` short-circuits the lookup entirely: libpq connects to the
339
- // fixed IP without consulting DNS while still using `host` for TLS SNI /
340
- // certificate hostname verification. We map this onto the same
341
- // `addressOverride` seam the DNS fan-out uses — the literal IP becomes
342
- // the candidate's `address`, and `host` (the user-typed name) is
343
- // preserved for SNI / `conn.host`. Only the single-host form carries a
344
- // `hostaddr`; libpq does not support a hostaddr-per-host list here.
345
- const candidates = opts.hostaddr !== undefined && opts.hostaddr !== ""
346
- ? seed.map((c) => ({
347
- host: c.host,
348
- address: opts.hostaddr,
349
- port: c.port,
350
- }))
351
- : await expandHostsViaDns(seed);
352
- if (opts.loadBalanceHosts === "random") {
353
- shuffleInPlace(candidates, PgConnection._loadBalanceRng ?? Math.random);
354
- }
355
- const tsa = opts.targetSessionAttrs ?? "any";
356
- // `prefer-standby` runs two passes: first 'standby', then 'any'. Every
357
- // other mode runs a single pass with the literal target.
358
- const passes = tsa === "prefer-standby" ? ["standby", "any"] : [tsa];
359
- let lastErr = null;
360
- for (const passTsa of passes) {
361
- for (const candidate of candidates) {
362
- const candidateOpts = {
363
- ...opts,
364
- host: candidate.host,
365
- port: candidate.port,
366
- };
367
- let conn;
368
- try {
369
- conn = await PgConnection.connectSingle(candidateOpts, candidate.address);
370
- }
371
- catch (err) {
372
- lastErr = err;
373
- continue;
374
- }
375
- // Apply target_session_attrs filter via pg_is_in_recovery().
376
- const accepted = await checkSessionAttrs(conn, passTsa);
377
- if (accepted) {
378
- return conn;
379
- }
380
- // Mismatch: close this connection and move on.
381
- try {
382
- await conn.close();
383
- }
384
- catch {
385
- // ignore
386
- }
387
- lastErr = new Error(`target_session_attrs=${String(passTsa)} did not match host ${candidate.host}:${String(candidate.port)}`);
388
- }
389
- }
390
- if (lastErr !== null) {
391
- // `throw lastErr` would trip the `only-throw-error` lint rule because
392
- // `lastErr` is typed `unknown`. Normalise to an Error for the throw
393
- // while preserving the original via `cause` so callers can introspect.
394
- if (lastErr instanceof Error)
395
- throw lastErr;
396
- let message;
397
- if (typeof lastErr === "object" &&
398
- lastErr !== null &&
399
- "message" in lastErr &&
400
- typeof lastErr.message === "string") {
401
- message = lastErr.message;
402
- }
403
- else if (typeof lastErr === "string" ||
404
- typeof lastErr === "number" ||
405
- typeof lastErr === "boolean") {
406
- message = String(lastErr);
407
- }
408
- else {
409
- message = "PgConnection.connect: unknown error";
410
- }
411
- const wrapped = new Error(message);
412
- wrapped.cause = lastErr;
413
- throw wrapped;
414
- }
415
- throw new Error("PgConnection.connect: no candidate hosts configured");
416
- }
417
- /**
418
- * Per-host connect attempt: open the socket, negotiate TLS, run the auth
419
- * dance, complete startup. Same shape as the pre-multihost `connect()`;
420
- * the multi-host orchestrator above wraps this for each candidate.
421
- */
422
- static async connectSingle(opts,
423
- /**
424
- * Optional resolved IP address. When set, `openSocket` uses it for the
425
- * actual `net.connect({host: address})`. `opts.host` remains the
426
- * user-typed hostname so TLS SNI / SAN verification and `conn.host`
427
- * report the original identity. Set by the DNS fan-out in `connect()`.
428
- */
429
- addressOverride) {
430
- // TLS over Unix-domain sockets is meaningless (the kernel guarantees
431
- // the channel) and libpq refuses `sslmode=require|verify-*` for socket
432
- // connections. We mirror the early rejection so a misconfigured caller
433
- // gets a clear diagnostic instead of a confused TLS handshake.
434
- if (isUnixSocketHost(opts.host) &&
435
- (opts.ssl === "require" ||
436
- opts.ssl === "verify-ca" ||
437
- opts.ssl === "verify-full")) {
438
- throw new Error(`sslmode=${opts.ssl} is not supported over Unix-domain sockets (host=${opts.host})`);
439
- }
440
- // libpq `requirepeer`: for Unix-domain sockets, libpq verifies the server
441
- // process runs as the named OS user via peer credentials (getpeereid /
442
- // SO_PEERCRED). Node exposes NO portable peer-credential API for Unix
443
- // sockets, so we CANNOT enforce this. We honestly do NOT verify it — to
444
- // avoid pretending a security check passed, we reject the connection with
445
- // a clear diagnostic when `requirepeer` is set on a socket connection,
446
- // rather than silently connecting as if the check had succeeded. (TCP
447
- // connections ignore `requirepeer`, matching libpq.)
448
- if (isUnixSocketHost(opts.host) &&
449
- opts.requirepeer !== undefined &&
450
- opts.requirepeer !== "") {
451
- throw new Error(`requirepeer="${opts.requirepeer}" cannot be enforced: Node provides no ` +
452
- `Unix-domain socket peer-credential API; refusing to connect rather ` +
453
- `than skip the check`);
454
- }
455
- const rawSocket = await openSocket(opts, addressOverride);
456
- let socket = rawSocket;
457
- let channelBindingData = null;
458
- try {
459
- // verify-ca skips hostname check; verify-full = default Node behavior.
460
- // require/prefer/allow accept any cert chain (libpq default).
461
- // NOTE: do NOT set `checkServerIdentity: undefined` — newer Node
462
- // versions reject that with "must be of type function". Omit the
463
- // property when verify-full so the default validator runs.
464
- // libpq `sslsni=0` suppresses the TLS SNI extension: omit `servername`
465
- // entirely so no hostname is sent in the ClientHello. Default (unset /
466
- // true) sends SNI = the connection host, as libpq does.
467
- const servername = tlsServername(opts);
468
- const tlsConnectionOptions = {
469
- ...(servername !== undefined ? { servername } : {}),
470
- rejectUnauthorized: opts.ssl === "verify-ca" || opts.ssl === "verify-full",
471
- // PG 17+ advertises ALPN for the 'postgresql' protocol; libpq sets
472
- // this so a future-proof TLS proxy can route on ALPN instead of
473
- // probing the wire. Always offer it — older servers ignore.
474
- ALPNProtocols: ["postgresql"],
475
- // Cipher preference is left to the runtime's TLS library. Our
476
- // ClientHello offers the byte-identical TLS-1.3 ciphersuite list and
477
- // order as libpq (AES-256-GCM, ChaCha20, AES-128-GCM), so the suite
478
- // is the server's choice from an identical offer. Under Node
479
- // (OpenSSL) that lands on AES-256-GCM, matching vanilla psql; under
480
- // Bun (BoringSSL) it lands on AES-128-GCM. Both are TLS-1.3 AEAD
481
- // suites with no practical security difference, and neither runtime
482
- // exposes a client-side knob to steer TLS-1.3 selection (`ciphers`
483
- // is TLS-1.2-only; `ciphersuites`/secureContext are ignored for the
484
- // client offer), so this is left as-is.
485
- };
486
- if (opts.ssl !== "verify-full") {
487
- tlsConnectionOptions.checkServerIdentity = () => undefined;
488
- }
489
- else if (opts.sslsni === false) {
490
- // verify-full still verifies the peer name against `host`, but with
491
- // SNI suppressed Node has no `servername` to drive its default
492
- // identity check — so verify explicitly against the connection host.
493
- // (libpq decouples SNI (sslsni) from peer-name verification (sslmode);
494
- // we mirror that here.)
495
- tlsConnectionOptions.checkServerIdentity = (_host, cert) => tls.checkServerIdentity(opts.host, cert);
496
- }
497
- applyTlsProtocolVersionRange(tlsConnectionOptions, opts);
498
- const tlsResult = await negotiateTls(rawSocket,
499
- // libpq refuses TLS on a socket connection even for sslmode=allow /
500
- // prefer — instead of negotiating it just stays plain. We short-
501
- // circuit by passing 'disable' to negotiateTls; the caller's
502
- // requested sslmode is preserved on opts for error reporting.
503
- isUnixSocketHost(opts.host) ? "disable" : opts.ssl, tlsConnectionOptions, {
504
- sslcert: opts.sslcert,
505
- sslkey: opts.sslkey,
506
- sslcertmode: opts.sslcertmode,
507
- sslpassword: opts.sslpassword,
508
- sslrootcert: opts.sslrootcert,
509
- sslcrl: opts.sslcrl,
510
- sslcrldir: opts.sslcrldir,
511
- sslkeylogfile: opts.sslkeylogfile,
512
- },
513
- // libpq `sslnegotiation=direct` (PG 17+): start TLS without the
514
- // SSLRequest probe. Never reached for Unix-domain sockets (forced to
515
- // sslmode 'disable' above) — direct SSL is a TCP-only concept. The
516
- // ALPN protocol is already on `tlsConnectionOptions` for both paths.
517
- isUnixSocketHost(opts.host)
518
- ? "postgres"
519
- : (opts.sslnegotiation ?? "postgres"));
520
- if (tlsResult.kind === "tls") {
521
- socket = tlsResult.socket;
522
- channelBindingData = tlsResult.channelBindingData;
523
- }
524
- else {
525
- socket = tlsResult.socket;
526
- }
527
- }
528
- catch (err) {
529
- try {
530
- rawSocket.destroy();
531
- }
532
- catch {
533
- // ignore
534
- }
535
- throw err;
536
- }
537
- const conn = new PgConnection(socket, opts, channelBindingData);
538
- await conn.startup();
539
- return conn;
540
- }
541
- // -------------------------------------------------------------------------
542
- // Connection interface — public methods
543
- // -------------------------------------------------------------------------
544
- parameterStatus(name) {
545
- return this.params.get(name);
546
- }
547
- /**
548
- * Expose the connection target as `meta.database` / `meta.user` / `meta.host`
549
- * / `meta.port` / `meta.pid` so the prompt renderer (which duck-types these
550
- * via `MaybeWithMeta`) can render `%/`, `%n`, `%m`, `%>`, `%p` without
551
- * additional plumbing. Postgres doesn't emit a `database` ParameterStatus,
552
- * so these come from the connect opts / BackendKeyData.
553
- */
554
- get database() {
555
- return this.opts.database;
556
- }
557
- get user() {
558
- return this.opts.user;
559
- }
560
- get host() {
561
- return this.opts.host;
562
- }
563
- get port() {
564
- return this.opts.port;
565
- }
566
- get pid() {
567
- return this.processId;
568
- }
569
- /**
570
- * The password supplied at connect time (or `null`). Mirrors libpq's
571
- * retention of the password on the live `PGconn` so `\c <newdb>` can
572
- * reconnect transparently. Read-only by design — the field is set once in
573
- * the constructor and never mutated.
574
- */
575
- get password() {
576
- return this._password;
577
- }
578
- /**
579
- * If the connection was upgraded to TLS during negotiation, return the
580
- * cipher info for the active session. Returns `null` for plain-text
581
- * connections. Used by the startup banner to render an `SSL connection
582
- * (protocol: …, cipher: …)` line that mirrors upstream psql, and by
583
- * `\conninfo` (PG18 connection-information table) to fill the SSL rows.
584
- */
585
- getTlsInfo() {
586
- const s = this.socket;
587
- if (typeof s.getCipher !== "function")
588
- return null;
589
- try {
590
- const cipher = s.getCipher();
591
- const protocol = s.getProtocol?.() ?? cipher.version ?? "unknown";
592
- if (!cipher.name)
593
- return null;
594
- // TLS compression has been disabled by every modern stack since CRIME
595
- // (2012); Node's TLS doesn't expose a compression accessor, so we
596
- // always report "off". libpq does the same.
597
- const compression = "off";
598
- // Node exposes the negotiated ALPN protocol on TLSSocket.alpnProtocol
599
- // (string when negotiated, false when not). Postgres 17+ uses
600
- // 'postgresql' here.
601
- const alpnRaw = s
602
- .alpnProtocol;
603
- const alpn = typeof alpnRaw === "string" && alpnRaw.length > 0
604
- ? alpnRaw
605
- : null;
606
- return {
607
- protocol: String(protocol),
608
- cipher: cipher.standardName ?? cipher.name,
609
- standardName: cipher.standardName,
610
- compression,
611
- alpn,
612
- library: "OpenSSL",
613
- keyBits: sslKeyBitsFromCipher(cipher.standardName ?? cipher.name),
614
- };
615
- }
616
- catch {
617
- return null;
618
- }
619
- }
620
- /**
621
- * Static connection facts for the PG18 `\conninfo` table, sourced from the
622
- * connect opts and the live socket (no SQL is issued):
623
- *
624
- * - `host` / `port` / `options`: from the connect opts.
625
- * - `hostaddr`: the resolved peer IP — `opts.hostaddr` if the caller
626
- * fixed one, else the TCP socket's `remoteAddress` (undefined for a
627
- * Unix-domain socket → `null`).
628
- * - `backendPid`: the backend process id from BackendKeyData.
629
- * - `passwordUsed`: whether a password was actually sent during auth.
630
- * - `gssapiUsed`: always `false` — we have no GSSAPI support.
631
- */
632
- getConnectionInfo() {
633
- const remote = this.opts.hostaddr !== undefined && this.opts.hostaddr !== ""
634
- ? this.opts.hostaddr
635
- : (this.socket.remoteAddress ?? null);
636
- return {
637
- host: this.opts.host,
638
- hostaddr: remote,
639
- port: this.opts.port,
640
- options: this.opts.options ?? null,
641
- backendPid: this.processId,
642
- passwordUsed: this.passwordUsed,
643
- gssapiUsed: false,
644
- };
645
- }
646
- async query(sql, params) {
647
- // `params === undefined` → caller has no intent to use extended protocol
648
- // (e.g. a buffered SQL run without `\bind`). Use the simple-query path so
649
- // chained `\;`-separated statements still work via `PQexec`-shaped
650
- // semantics.
651
- //
652
- // `params` defined (even as `[]`) → caller staged a `\bind` (or a
653
- // describe-formatter explicitly asking for the extended path). The
654
- // extended protocol sends a single Parse message verbatim; the server
655
- // rejects multi-statement SQL with SQLSTATE 42601 / "cannot insert
656
- // multiple commands into a prepared statement", which is the upstream
657
- // contract for `\bind`/`\parse`. Switching on length === 0 would silently
658
- // fall back to simple-query and mask that diagnostic.
659
- if (params === undefined) {
660
- const sets = await this.execSimple(sql);
661
- if (sets.length === 0) {
662
- throw new Error("PgConnection.query: server returned no result sets");
663
- }
664
- return sets[sets.length - 1];
665
- }
666
- // Extended-protocol single-shot: Parse('', sql, []) → Bind('', '', [],
667
- // text-encoded params, [text]) → Describe('P', '') → Execute('', 0) →
668
- // Sync. Every param goes out in text format and the server coerces.
669
- this.ensureIdle();
670
- const encoded = encodeParams(params);
671
- this.startExtendedBatch();
672
- const parseP = this.enqueueParse();
673
- const bindP = this.enqueueBind();
674
- const descP = this.enqueueDescribePortalIntoNextExecute();
675
- const execP = this.enqueueExecute();
676
- const syncP = this.enqueueSync();
677
- this.socket.write(Parse("", sql, []));
678
- this.socket.write(Bind("", "", [], encoded, [0]));
679
- this.socket.write(Describe("P", ""));
680
- this.socket.write(Execute("", 0));
681
- this.socket.write(Sync());
682
- let firstErr = null;
683
- const cap = (e) => {
684
- if (firstErr === null)
685
- firstErr = e;
686
- };
687
- parseP.catch(cap);
688
- bindP.catch(cap);
689
- descP.catch(cap);
690
- let result = null;
691
- execP.then((rs) => {
692
- result = rs;
693
- }, cap);
694
- await syncP.catch(cap);
695
- if (firstErr !== null)
696
- throw asThrowable(firstErr);
697
- if (result === null) {
698
- throw new Error("PgConnection.query: server returned no result");
699
- }
700
- return result;
701
- }
702
- async execSimple(sql) {
703
- this.ensureIdle();
704
- return new Promise((resolve, reject) => {
705
- this.pendingQuery = {
706
- resolve,
707
- reject,
708
- current: null,
709
- finished: [],
710
- notices: [],
711
- error: null,
712
- };
713
- this.state = "in-query";
714
- this.socket.write(Query(sql));
715
- });
716
- }
717
- async prepare(name, sql, paramTypes) {
718
- this.ensureIdle();
719
- const oids = paramTypes ?? [];
720
- this.startExtendedBatch();
721
- const parseP = this.enqueueParse();
722
- const descP = this.enqueueDescribeStatement();
723
- const syncP = this.enqueueSync();
724
- this.socket.write(Parse(name, sql, oids));
725
- this.socket.write(Describe("S", name));
726
- this.socket.write(Sync());
727
- let firstErr = null;
728
- const cap = (e) => {
729
- if (firstErr === null)
730
- firstErr = e;
731
- };
732
- parseP.catch(cap);
733
- let descResult = null;
734
- descP.then((r) => {
735
- descResult = r;
736
- }, cap);
737
- await syncP.catch(cap);
738
- if (firstErr !== null)
739
- throw asThrowable(firstErr);
740
- if (descResult === null) {
741
- throw new Error("PgConnection.prepare: server returned no parameter description");
742
- }
743
- const { paramOids, fields } = descResult;
744
- const conn = this;
745
- return {
746
- name,
747
- paramTypes: paramOids,
748
- async bind(values, paramFormats) {
749
- conn.ensureIdle();
750
- const encoded = encodeParams(values);
751
- conn.startExtendedBatch();
752
- const bP = conn.enqueueBind();
753
- const sP = conn.enqueueSync();
754
- conn.socket.write(Bind("", name, paramFormats ?? [], encoded, [0]));
755
- conn.socket.write(Sync());
756
- let err = null;
757
- bP.catch((e) => {
758
- if (err === null)
759
- err = e;
760
- });
761
- await sP.catch((e) => {
762
- if (err === null)
763
- err = e;
764
- });
765
- if (err !== null)
766
- throw asThrowable(err);
767
- },
768
- describe() {
769
- return Promise.resolve(fields);
770
- },
771
- async execute(maxRows) {
772
- conn.ensureIdle();
773
- conn.startExtendedBatch();
774
- const eP = conn.enqueueExecuteWithFields(fields);
775
- const sP = conn.enqueueSync();
776
- conn.socket.write(Execute("", maxRows ?? 0));
777
- conn.socket.write(Sync());
778
- let err = null;
779
- let rs = null;
780
- eP.then((r) => {
781
- rs = r;
782
- }, (e) => {
783
- if (err === null)
784
- err = e;
785
- });
786
- await sP.catch((e) => {
787
- if (err === null)
788
- err = e;
789
- });
790
- if (err !== null)
791
- throw asThrowable(err);
792
- if (rs === null) {
793
- throw new Error("PgConnection.prepare.execute: server returned no result");
794
- }
795
- return rs;
796
- },
797
- async bindAndExecute(values, maxRows, paramFormats) {
798
- conn.ensureIdle();
799
- const encoded = encodeParams(values);
800
- conn.startExtendedBatch();
801
- const bP = conn.enqueueBind();
802
- const eP = conn.enqueueExecuteWithFields(fields);
803
- const sP = conn.enqueueSync();
804
- conn.socket.write(Bind("", name, paramFormats ?? [], encoded, [0]));
805
- conn.socket.write(Execute("", maxRows ?? 0));
806
- conn.socket.write(Sync());
807
- let err = null;
808
- let rs = null;
809
- bP.catch((e) => {
810
- if (err === null)
811
- err = e;
812
- });
813
- eP.then((r) => {
814
- rs = r;
815
- }, (e) => {
816
- if (err === null)
817
- err = e;
818
- });
819
- await sP.catch((e) => {
820
- if (err === null)
821
- err = e;
822
- });
823
- if (err !== null)
824
- throw asThrowable(err);
825
- if (rs === null) {
826
- throw new Error("PgConnection.prepare.bindAndExecute: server returned no result");
827
- }
828
- return rs;
829
- },
830
- async close() {
831
- conn.ensureIdle();
832
- conn.startExtendedBatch();
833
- const cP = conn.enqueueClose();
834
- const sP = conn.enqueueSync();
835
- conn.socket.write(Close("S", name));
836
- conn.socket.write(Sync());
837
- let err = null;
838
- cP.catch((e) => {
839
- if (err === null)
840
- err = e;
841
- });
842
- await sP.catch((e) => {
843
- if (err === null)
844
- err = e;
845
- });
846
- if (err !== null)
847
- throw asThrowable(err);
848
- },
849
- };
850
- }
851
- /**
852
- * Issue `Close('S', name) + Sync` directly, without a preceding Parse.
853
- * The server responds with CloseComplete + ReadyForQuery (even when
854
- * the named statement doesn't exist — PG treats unknown-name Close as
855
- * a no-op). Used by `\close_prepared NAME` so we don't have to fake a
856
- * Parse just to reach the Close step.
857
- */
858
- async closePreparedStatement(name) {
859
- this.ensureIdle();
860
- this.startExtendedBatch();
861
- const cP = this.enqueueClose();
862
- const sP = this.enqueueSync();
863
- this.socket.write(Close("S", name));
864
- this.socket.write(Sync());
865
- let err = null;
866
- cP.catch((e) => {
867
- if (err === null)
868
- err = e;
869
- });
870
- await sP.catch((e) => {
871
- if (err === null)
872
- err = e;
873
- });
874
- if (err !== null)
875
- throw asThrowable(err);
876
- }
877
- startCopyIn(sql) {
878
- this.ensureIdle();
879
- // COPY mid-pipeline is rejected by libpq with a fixed diagnostic; we
880
- // mirror that synchronously so callers don't need a round-trip to learn
881
- // their command is invalid. The wire-level dispatch also guards this
882
- // (see handleCopyStartMessage) for any path that bypasses this check.
883
- if (this._extPipelineActive) {
884
- this.abortForCopyInPipeline();
885
- return Promise.reject(Object.assign(new Error("COPY in a pipeline is not supported, aborting connection"), { severity: "FATAL" }));
886
- }
887
- // The driver waits in `in-query` state until CopyInResponse arrives — at
888
- // which point the protocol switches and we move to `in-copy-in`. The
889
- // server can also reply with an ErrorResponse (e.g. "no such table"),
890
- // which we surface as a rejected promise.
891
- return new Promise((resolve, reject) => {
892
- this.copyIn = {
893
- resolveDone: null,
894
- rejectDone: null,
895
- error: null,
896
- commandTag: null,
897
- closed: false,
898
- };
899
- this.copyStartResolve = () => {
900
- // The protocol-switch landed; hand the caller a usable stream.
901
- resolve(this.makeCopyInStream());
902
- };
903
- this.copyStartReject = reject;
904
- this.state = "in-query";
905
- this.socket.write(Query(sql));
906
- });
907
- }
908
- startCopyOut(sql) {
909
- this.ensureIdle();
910
- if (this._extPipelineActive) {
911
- this.abortForCopyInPipeline();
912
- return Promise.reject(Object.assign(new Error("COPY in a pipeline is not supported, aborting connection"), { severity: "FATAL" }));
913
- }
914
- return new Promise((resolve, reject) => {
915
- this.copyOut = {
916
- queue: [],
917
- queuedBytes: 0,
918
- waker: null,
919
- done: false,
920
- abandoned: false,
921
- error: null,
922
- commandTag: null,
923
- };
924
- this.copyStartResolve = () => {
925
- resolve(this.makeCopyOutStream());
926
- };
927
- this.copyStartReject = reject;
928
- this.state = "in-query";
929
- this.socket.write(Query(sql));
930
- });
931
- }
932
- pipeline() {
933
- this.ensureIdle();
934
- return new PipelineSession(this);
935
- }
936
- /**
937
- * Cancel whatever the connection is currently doing.
938
- *
939
- * The routing is state-aware so the mainloop SIGINT handler can call this
940
- * blindly without knowing the protocol phase:
941
- *
942
- * - `in-copy-in`: we hold the writing end of the data stream, so the
943
- * correct action is a client-initiated CopyFail on the *same* socket.
944
- * Sending a side CancelRequest would race with our own pending writes;
945
- * CopyFail is the spec-blessed abort path. The server replies with
946
- * ErrorResponse + ReadyForQuery and we transition back to idle.
947
- * - `in-copy-out`: the server is pushing data at us. CopyFail is not a
948
- * valid client message here, so we fall back to the side CancelRequest
949
- * path that normal queries use. PG will surface an ErrorResponse and
950
- * tear the COPY down.
951
- * - everything else: side CancelRequest, the historical behaviour.
952
- *
953
- * Best-effort. We don't reject if BackendKeyData hasn't arrived yet
954
- * during the auth dance — there's nothing to cancel; we just return.
955
- */
956
- async cancel() {
957
- // In-copy-in: send CopyFail on the live socket so the server returns
958
- // to ReadyForQuery cleanly. This is the same abort path the upstream
959
- // SIGINT handler in `copy.c::handleCopyIn` triggers via longjmp.
960
- if (this.state === "in-copy-in" && this.copyIn && !this.copyIn.closed) {
961
- this.copyIn.closed = true;
962
- try {
963
- this.socket.write(CopyFail("canceled by user"));
964
- }
965
- catch {
966
- // Socket may have died — failPending() will surface that.
967
- }
968
- return;
969
- }
970
- if (this.processId === 0) {
971
- // Nothing to cancel — startup hasn't reached BackendKeyData. Be
972
- // forgiving: the mainloop SIGINT handler shouldn't crash on cancel
973
- // during a half-open connection.
974
- return;
975
- }
976
- // Per the PG protocol, CancelRequest is sent on a *fresh* connection,
977
- // not the one running the query. We TLS-negotiate against the same
978
- // sslmode but we don't auth — we just write the request and close.
979
- // Honour `hostaddr` on the cancel connection too: dial the fixed IP while
980
- // keeping the user-typed host for SNI / cert verification, mirroring the
981
- // primary connect path's `addressOverride`.
982
- const cancelSocket = await openSocket(this.opts, this.opts.hostaddr !== undefined && this.opts.hostaddr !== ""
983
- ? this.opts.hostaddr
984
- : undefined);
985
- let writeSocket = cancelSocket;
986
- try {
987
- // sslsni=0 suppresses SNI on the cancel connection too (mirrors the
988
- // primary connect path).
989
- const cancelServername = tlsServername(this.opts);
990
- const cancelTlsOpts = {
991
- ...(cancelServername !== undefined
992
- ? { servername: cancelServername }
993
- : {}),
994
- rejectUnauthorized: this.opts.ssl === "verify-ca" ||
995
- this.opts.ssl === "verify-full",
996
- ALPNProtocols: ["postgresql"],
997
- };
998
- if (this.opts.ssl !== "verify-full") {
999
- cancelTlsOpts.checkServerIdentity = () => undefined;
1000
- }
1001
- else if (this.opts.sslsni === false) {
1002
- const host = this.opts.host;
1003
- cancelTlsOpts.checkServerIdentity = (_h, cert) => tls.checkServerIdentity(host, cert);
1004
- }
1005
- applyTlsProtocolVersionRange(cancelTlsOpts, this.opts);
1006
- const t = await negotiateTls(cancelSocket,
1007
- // Unix-domain socket: no TLS, regardless of caller's sslmode.
1008
- isUnixSocketHost(this.opts.host) ? "disable" : this.opts.ssl, cancelTlsOpts, {
1009
- sslcert: this.opts.sslcert,
1010
- sslkey: this.opts.sslkey,
1011
- sslcertmode: this.opts.sslcertmode,
1012
- sslpassword: this.opts.sslpassword,
1013
- sslrootcert: this.opts.sslrootcert,
1014
- sslcrl: this.opts.sslcrl,
1015
- sslcrldir: this.opts.sslcrldir,
1016
- sslkeylogfile: this.opts.sslkeylogfile,
1017
- },
1018
- // Mirror the primary connect path's negotiation mode so a server
1019
- // configured for direct SSL also accepts the cancel connection.
1020
- isUnixSocketHost(this.opts.host)
1021
- ? "postgres"
1022
- : (this.opts.sslnegotiation ?? "postgres"));
1023
- writeSocket = t.kind === "tls" ? t.socket : t.socket;
1024
- await new Promise((resolve, reject) => {
1025
- writeSocket.write(CancelRequest(this.processId, this.secretKey), (err) => {
1026
- if (err)
1027
- reject(err);
1028
- else
1029
- resolve();
1030
- });
1031
- });
1032
- }
1033
- finally {
1034
- try {
1035
- writeSocket.end();
1036
- }
1037
- catch {
1038
- // ignore
1039
- }
1040
- try {
1041
- cancelSocket.destroy();
1042
- }
1043
- catch {
1044
- // ignore
1045
- }
1046
- }
1047
- }
1048
- escapeIdentifier(value) {
1049
- return '"' + value.replace(/"/g, '""') + '"';
1050
- }
1051
- escapeLiteral(value) {
1052
- // Per PG docs: doubled single-quotes always; if the string contains a
1053
- // backslash, use the E'...' escape-string syntax so backslashes don't
1054
- // depend on `standard_conforming_strings`.
1055
- const doubled = value.replace(/'/g, "''");
1056
- if (value.includes("\\")) {
1057
- return "E'" + doubled.replace(/\\/g, "\\\\") + "'";
1058
- }
1059
- return "'" + doubled + "'";
1060
- }
1061
- async setClientEncoding(name) {
1062
- // libpq's PQsetClientEncoding sends `SET client_encoding TO '<value>'`
1063
- // down the wire (see fe-connect.c). We do the same via the simple-query
1064
- // path, quoting the value as a string literal so encoding names are
1065
- // never mistaken for SQL tokens. On success the server emits a
1066
- // `client_encoding` ParameterStatus which the message loop folds into
1067
- // `this.params` (see the ParameterStatus cases), so
1068
- // `parameterStatus('client_encoding')` is up to date afterwards.
1069
- //
1070
- // We keep no separate client-side decoder state: text-format values are
1071
- // always decoded as UTF-8 in `decodeDataRow`, matching how the rest of
1072
- // this client treats backend text. Tracking only the ParameterStatus is
1073
- // therefore sufficient. A non-zero `SET` failure (e.g. a name the server
1074
- // rejects) surfaces as a thrown ConnectError from `execSimple`.
1075
- await this.execSimple(`SET client_encoding TO ${this.escapeLiteral(name)}`);
1076
- }
1077
- onNotice(handler) {
1078
- return this.notify.onNotice(handler);
1079
- }
1080
- onNotification(handler) {
1081
- return this.notify.onNotification(handler);
1082
- }
1083
- async close() {
1084
- if (this.state === "closed")
1085
- return;
1086
- try {
1087
- this.socket.write(Terminate());
1088
- }
1089
- catch {
1090
- // socket may already be dead; we still want to mark closed
1091
- }
1092
- this.state = "closed";
1093
- this.notify.clear();
1094
- await new Promise((resolve) => {
1095
- this.socket.once("close", () => {
1096
- resolve();
1097
- });
1098
- try {
1099
- this.socket.end();
1100
- }
1101
- catch {
1102
- resolve();
1103
- }
1104
- });
1105
- }
1106
- isClosed() {
1107
- return this.state === "closed";
1108
- }
1109
- // -------------------------------------------------------------------------
1110
- // Startup / auth state machine
1111
- // -------------------------------------------------------------------------
1112
- startup() {
1113
- return new Promise((resolve, reject) => {
1114
- const params = {
1115
- user: this.opts.user,
1116
- database: this.opts.database,
1117
- // psql sends client_encoding=UTF8 by default; we follow.
1118
- client_encoding: this.opts.clientEncoding ?? "UTF8",
1119
- };
1120
- if (this.opts.applicationName !== undefined) {
1121
- params.application_name = this.opts.applicationName;
1122
- }
1123
- if (this.opts.options !== undefined) {
1124
- params.options = this.opts.options;
1125
- }
1126
- // Walsender (replication) mode: the server enters a restricted
1127
- // command set (IDENTIFY_SYSTEM, START_REPLICATION, etc.) keyed off
1128
- // this startup parameter. Values mirror libpq's normalisation:
1129
- // 'true' for physical, 'database' for logical. We do not stream the
1130
- // CopyBoth phase — the Query path still surfaces ErrorResponse and
1131
- // any pre-streaming ResultSet, which is enough for the negative
1132
- // conformance test (`psql -c 'START_REPLICATION 0/1'` must exit
1133
- // non-zero with a syntax error from the server).
1134
- if (this.opts.replication !== undefined) {
1135
- params.replication = this.opts.replication;
1136
- }
1137
- this.startupResolve = resolve;
1138
- this.startupReject = reject;
1139
- this.socket.write(StartupMessage(params));
1140
- });
1141
- }
1142
- /**
1143
- * Enforce `require_auth` against an observed server-requested method.
1144
- * Returns true when the connection should proceed; false (with
1145
- * {@link failStartup} already called) when the policy was violated.
1146
- */
1147
- checkRequireAuth(observed) {
1148
- const policy = this.opts.requireAuth;
1149
- if (policy === undefined)
1150
- return true;
1151
- const hit = policy.methods.has(observed);
1152
- const allowed = policy.negated ? !hit : hit;
1153
- if (!allowed) {
1154
- this.failStartup(new Error(`auth method "${observed}" requirement failed`));
1155
- return false;
1156
- }
1157
- return true;
1158
- }
1159
- handleAuthMessage(msg) {
1160
- switch (msg.type) {
1161
- case "AuthenticationOk": {
1162
- // libpq parity: channel_binding=require demands that some prior
1163
- // auth step actually negotiated channel binding. A bare
1164
- // AuthenticationOk after no challenge ("trust") or after a cert
1165
- // exchange ("cert" HBA, clientcert=verify-full) means no SCRAM
1166
- // happened and we must refuse.
1167
- if (this.opts.channelBinding === "require" &&
1168
- (this.scram === null ||
1169
- this.scram.mechanism !== "SCRAM-SHA-256-PLUS")) {
1170
- this.failStartup(new Error("channel binding required, but server authenticated client without channel binding"));
1171
- return;
1172
- }
1173
- // Mutual-auth integrity: if a SCRAM exchange was started it MUST have
1174
- // completed via AuthenticationSASLFinal (where the server signature is
1175
- // verified). A server that sends SASLContinue then jumps to
1176
- // AuthenticationOk never proves it knows the password (review #8).
1177
- if (this.scram !== null && !this.saslFinalSeen) {
1178
- this.failStartup(new Error("server sent AuthenticationOk without completing SCRAM " +
1179
- "authentication (server signature not verified)"));
1180
- return;
1181
- }
1182
- // require_auth=none allows trust auth; anything else is rejected
1183
- // here. If a prior challenge was sent and validated, skip — that
1184
- // method was already accepted by the check at its own branch.
1185
- if (!this.authChallengeSeen && !this.checkRequireAuth("none")) {
1186
- return;
1187
- }
1188
- this.state = "await-ready";
1189
- return;
1190
- }
1191
- case "AuthenticationCleartextPassword": {
1192
- this.authChallengeSeen = true;
1193
- if (this.opts.channelBinding === "require") {
1194
- this.failStartup(new Error("channel binding required but not supported by server's authentication request"));
1195
- return;
1196
- }
1197
- if (!this.checkRequireAuth("password"))
1198
- return;
1199
- if (this.opts.password === undefined) {
1200
- this.failStartup(new Error("Server requested cleartext password but no password was provided"));
1201
- return;
1202
- }
1203
- this.passwordUsed = true;
1204
- this.socket.write(PasswordMessage(this.opts.password));
1205
- return;
1206
- }
1207
- case "AuthenticationMD5Password": {
1208
- this.authChallengeSeen = true;
1209
- if (this.opts.channelBinding === "require") {
1210
- this.failStartup(new Error("channel binding required but not supported by server's authentication request"));
1211
- return;
1212
- }
1213
- if (!this.checkRequireAuth("md5"))
1214
- return;
1215
- if (this.opts.password === undefined) {
1216
- this.failStartup(new Error("Server requested MD5 password but no password was provided"));
1217
- return;
1218
- }
1219
- const payload = md5AuthPayload(this.opts.user, this.opts.password, msg.salt);
1220
- this.passwordUsed = true;
1221
- this.socket.write(PasswordMessage(payload));
1222
- return;
1223
- }
1224
- case "AuthenticationSASL": {
1225
- this.authChallengeSeen = true;
1226
- if (this.opts.password === undefined) {
1227
- this.failStartup(new Error("Server requested SASL auth but no password was provided"));
1228
- return;
1229
- }
1230
- if (!this.checkRequireAuth("scram-sha-256"))
1231
- return;
1232
- // channel_binding=require AND server didn't offer the PLUS
1233
- // variant — refuse before the SASL handshake starts. The check
1234
- // is split between here (no PLUS in the mechanism list) and
1235
- // chooseMechanism's fallback (PLUS present but no binding data).
1236
- if (this.opts.channelBinding === "require" &&
1237
- !msg.mechanisms.includes("SCRAM-SHA-256-PLUS")) {
1238
- this.failStartup(new Error("channel binding required but not supported by server's authentication request"));
1239
- return;
1240
- }
1241
- if (this.opts.channelBinding === "require" &&
1242
- this.channelBindingData === null) {
1243
- this.failStartup(new Error("channel binding required but not supported by server's authentication request"));
1244
- return;
1245
- }
1246
- try {
1247
- this.scram = createScramClient({
1248
- user: this.opts.user,
1249
- password: this.opts.password,
1250
- mechanisms: msg.mechanisms,
1251
- channelBinding: this.channelBindingData !== null &&
1252
- this.opts.channelBinding !== "disable"
1253
- ? {
1254
- type: "tls-server-end-point",
1255
- data: this.channelBindingData,
1256
- }
1257
- : undefined,
1258
- });
1259
- const { mechanism, clientFirstMessage } = this.scram.start();
1260
- this.passwordUsed = true;
1261
- this.socket.write(SASLInitialResponse(mechanism, clientFirstMessage));
1262
- }
1263
- catch (err) {
1264
- this.failStartup(err);
1265
- }
1266
- return;
1267
- }
1268
- case "AuthenticationSASLContinue": {
1269
- if (!this.scram) {
1270
- this.failStartup(new Error("Received AuthenticationSASLContinue without an active SCRAM client"));
1271
- return;
1272
- }
1273
- try {
1274
- const reply = this.scram.continue(msg.data);
1275
- this.socket.write(SASLResponse(reply));
1276
- }
1277
- catch (err) {
1278
- this.failStartup(err);
1279
- }
1280
- return;
1281
- }
1282
- case "AuthenticationSASLFinal": {
1283
- if (!this.scram) {
1284
- this.failStartup(new Error("Received AuthenticationSASLFinal without an active SCRAM client"));
1285
- return;
1286
- }
1287
- try {
1288
- this.scram.finish(msg.data);
1289
- this.saslFinalSeen = true;
1290
- }
1291
- catch (err) {
1292
- this.failStartup(err);
1293
- }
1294
- return;
1295
- }
1296
- case "ErrorResponse":
1297
- this.failStartup(fieldsToConnectError(msg.fields));
1298
- return;
1299
- case "NoticeResponse":
1300
- this.notify.emit(fieldsToNotice(msg.fields));
1301
- return;
1302
- default:
1303
- // ParameterStatus / BackendKeyData / ReadyForQuery may arrive in
1304
- // `await-ready`; auth state shouldn't see them, but we tolerate by
1305
- // forwarding to the post-auth handler if so.
1306
- this.handleAwaitReady(msg);
1307
- return;
1308
- }
1309
- }
1310
- handleAwaitReady(msg) {
1311
- switch (msg.type) {
1312
- case "ParameterStatus":
1313
- this.params.set(msg.name, msg.value);
1314
- if (msg.name === "server_version") {
1315
- this.serverVersion = parseServerVersion(msg.value);
1316
- }
1317
- return;
1318
- case "BackendKeyData":
1319
- this.processId = msg.processId;
1320
- this.secretKey = msg.secretKey;
1321
- return;
1322
- case "ReadyForQuery":
1323
- this.txStatus = msg.status;
1324
- this.state = "idle";
1325
- if (this.startupResolve) {
1326
- const r = this.startupResolve;
1327
- this.startupResolve = null;
1328
- this.startupReject = null;
1329
- r();
1330
- }
1331
- return;
1332
- case "ErrorResponse":
1333
- this.failStartup(fieldsToConnectError(msg.fields));
1334
- return;
1335
- case "NoticeResponse":
1336
- this.notify.emit(fieldsToNotice(msg.fields));
1337
- return;
1338
- default:
1339
- this.failStartup(new Error(`Unexpected message ${msg.type} during connection startup`));
1340
- return;
1341
- }
1342
- }
1343
- failStartup(err) {
1344
- if (this.startupReject) {
1345
- const r = this.startupReject;
1346
- this.startupResolve = null;
1347
- this.startupReject = null;
1348
- r(err);
1349
- }
1350
- try {
1351
- this.socket.destroy();
1352
- }
1353
- catch {
1354
- // ignore
1355
- }
1356
- this.state = "closed";
1357
- }
1358
- // -------------------------------------------------------------------------
1359
- // COPY state machine (WP-16).
1360
- //
1361
- // The frontend transitions through:
1362
- // idle → in-query (after writing Query("COPY …"))
1363
- // in-query → in-copy-in on CopyInResponse
1364
- // in-query → in-copy-out on CopyOutResponse
1365
- // in-copy-in → idle on ReadyForQuery (after our CopyDone/CopyFail +
1366
- // server CommandComplete)
1367
- // in-copy-out → idle on ReadyForQuery (after server CopyDone +
1368
- // CommandComplete)
1369
- //
1370
- // ErrorResponse may arrive at any point; we drain until ReadyForQuery and
1371
- // then surface as a rejected promise.
1372
- // -------------------------------------------------------------------------
1373
- makeCopyInStream() {
1374
- const driver = this.copyIn;
1375
- if (!driver) {
1376
- throw new Error("PgConnection: makeCopyInStream called without driver");
1377
- }
1378
- return {
1379
- write: (chunk) => {
1380
- if (this.state === "closed") {
1381
- return Promise.reject(new Error("Connection closed"));
1382
- }
1383
- if (driver.closed) {
1384
- return Promise.reject(new Error("CopyInStream already closed"));
1385
- }
1386
- const data = typeof chunk === "string"
1387
- ? Buffer.from(chunk, "utf8")
1388
- : chunk;
1389
- return new Promise((resolve, reject) => {
1390
- this.socket.write(CopyData(data), (err) => {
1391
- if (err)
1392
- reject(err);
1393
- else
1394
- resolve();
1395
- });
1396
- });
1397
- },
1398
- end: () => {
1399
- if (driver.closed) {
1400
- return Promise.reject(new Error("CopyInStream already closed"));
1401
- }
1402
- driver.closed = true;
1403
- return new Promise((resolve, reject) => {
1404
- driver.resolveDone = resolve;
1405
- driver.rejectDone = reject;
1406
- this.socket.write(CopyDone());
1407
- });
1408
- },
1409
- fail: (reason) => {
1410
- if (driver.closed) {
1411
- return Promise.reject(new Error("CopyInStream already closed"));
1412
- }
1413
- driver.closed = true;
1414
- return new Promise((resolve, reject) => {
1415
- // The server is expected to reject with an ErrorResponse echoing
1416
- // our reason; we still resolve so callers can move on. We wire the
1417
- // resolver after the socket flush so a fast-close error surfaces.
1418
- driver.resolveDone = () => {
1419
- resolve();
1420
- };
1421
- driver.rejectDone = reject;
1422
- this.socket.write(CopyFail(reason));
1423
- });
1424
- },
1425
- };
1426
- }
1427
- makeCopyOutStream() {
1428
- const driver = this.copyOut;
1429
- if (!driver) {
1430
- throw new Error("PgConnection: makeCopyOutStream called without driver");
1431
- }
1432
- // Capture the state-getter as a closure so the iterator can observe
1433
- // connection close without holding a `this` alias (no-this-alias rule).
1434
- const isClosed = () => this.state === "closed";
1435
- // Resume reading once the buffered data drains — paired with the
1436
- // pause() the CopyData handler applies at the high-water mark (#11).
1437
- const resumeSocket = () => {
1438
- this.socket.resume?.();
1439
- };
1440
- return {
1441
- [Symbol.asyncIterator]() {
1442
- return {
1443
- async next() {
1444
- for (;;) {
1445
- if (driver.queue.length > 0) {
1446
- const next = driver.queue.shift();
1447
- if (next === undefined)
1448
- continue;
1449
- driver.queuedBytes -= next.length;
1450
- if (driver.queuedBytes <= COPY_OUT_LWM)
1451
- resumeSocket();
1452
- return { value: next, done: false };
1453
- }
1454
- if (driver.error) {
1455
- // ConnectError isn't strictly an Error instance; the rule
1456
- // wants a real Error. Wrap once before throwing.
1457
- const ce = driver.error;
1458
- const wrapped = new Error(ce.message);
1459
- wrapped.cause =
1460
- ce;
1461
- throw wrapped;
1462
- }
1463
- if (driver.done) {
1464
- return { value: undefined, done: true };
1465
- }
1466
- if (isClosed()) {
1467
- throw new Error("Connection closed mid-COPY-OUT");
1468
- }
1469
- await new Promise((resolve) => {
1470
- driver.waker = resolve;
1471
- });
1472
- }
1473
- },
1474
- return() {
1475
- // Consumer broke early. We must keep reading the wire until
1476
- // ReadyForQuery to clear the protocol state, but we mark the
1477
- // stream abandoned (so the handler DROPS further CopyData rather
1478
- // than buffering it) and free what's queued — otherwise RSS grows
1479
- // to the full result size (review item #11). Resume in case the
1480
- // socket was paused at the high-water mark.
1481
- driver.abandoned = true;
1482
- driver.queue.length = 0;
1483
- driver.queuedBytes = 0;
1484
- resumeSocket();
1485
- return Promise.resolve({
1486
- value: undefined,
1487
- done: true,
1488
- });
1489
- },
1490
- };
1491
- },
1492
- };
1493
- }
1494
- /**
1495
- * Handle messages arriving in `in-query` state when there is no
1496
- * `pendingQuery` — i.e. the caller invoked `startCopyIn` / `startCopyOut`
1497
- * and is waiting for the server to switch into copy mode.
1498
- */
1499
- handleCopyStartMessage(msg) {
1500
- switch (msg.type) {
1501
- case "CopyInResponse":
1502
- // COPY-in-pipeline: libpq aborts the connection with this exact
1503
- // diagnostic (matching upstream psql's behaviour). The `\copy`
1504
- // command layer detects pipeline-active and fails fast before
1505
- // reaching the wire, but if anything else slips through we abort
1506
- // here as a defence-in-depth.
1507
- if (this._extPipelineActive) {
1508
- this.abortForCopyInPipeline();
1509
- return;
1510
- }
1511
- if (this.copyIn) {
1512
- this.state = "in-copy-in";
1513
- const r = this.copyStartResolve;
1514
- this.copyStartResolve = null;
1515
- this.copyStartReject = null;
1516
- if (r)
1517
- r();
1518
- }
1519
- return;
1520
- case "CopyOutResponse":
1521
- if (this._extPipelineActive) {
1522
- this.abortForCopyInPipeline();
1523
- return;
1524
- }
1525
- if (this.copyOut) {
1526
- this.state = "in-copy-out";
1527
- const r = this.copyStartResolve;
1528
- this.copyStartResolve = null;
1529
- this.copyStartReject = null;
1530
- if (r)
1531
- r();
1532
- }
1533
- return;
1534
- case "ErrorResponse": {
1535
- const err = fieldsToConnectError(msg.fields);
1536
- if (this.copyIn) {
1537
- if (this.copyStartReject)
1538
- this.copyStartReject(err);
1539
- this.copyStartResolve = null;
1540
- this.copyStartReject = null;
1541
- this.copyIn = null;
1542
- }
1543
- if (this.copyOut) {
1544
- if (this.copyStartReject)
1545
- this.copyStartReject(err);
1546
- this.copyStartResolve = null;
1547
- this.copyStartReject = null;
1548
- this.copyOut = null;
1549
- }
1550
- // Stay in `in-query` until ReadyForQuery, then return to idle below.
1551
- return;
1552
- }
1553
- case "ReadyForQuery":
1554
- // ReadyForQuery without a prior CopyXxxResponse means the server
1555
- // immediately rejected the COPY (we surfaced the ErrorResponse just
1556
- // above) — return to idle so the next command can fire.
1557
- this.txStatus = msg.status;
1558
- this.state = "idle";
1559
- return;
1560
- case "NoticeResponse":
1561
- this.notify.emit(fieldsToNotice(msg.fields));
1562
- return;
1563
- case "ParameterStatus":
1564
- this.params.set(msg.name, msg.value);
1565
- return;
1566
- default:
1567
- if (this.copyStartReject) {
1568
- this.copyStartReject(new Error(`Unexpected backend message before COPY response: ${msg.type}`));
1569
- }
1570
- this.copyStartResolve = null;
1571
- this.copyStartReject = null;
1572
- return;
1573
- }
1574
- }
1575
- handleCopyInMessage(msg) {
1576
- const driver = this.copyIn;
1577
- if (!driver)
1578
- return;
1579
- switch (msg.type) {
1580
- case "CommandComplete":
1581
- driver.commandTag = msg.tag;
1582
- this.lastCopyTag = msg.tag;
1583
- return;
1584
- case "ErrorResponse":
1585
- driver.error = fieldsToConnectError(msg.fields);
1586
- return;
1587
- case "NoticeResponse":
1588
- this.notify.emit(fieldsToNotice(msg.fields));
1589
- return;
1590
- case "ParameterStatus":
1591
- this.params.set(msg.name, msg.value);
1592
- return;
1593
- case "ReadyForQuery":
1594
- this.txStatus = msg.status;
1595
- this.state = "idle";
1596
- this.copyIn = null;
1597
- if (driver.error) {
1598
- if (driver.rejectDone)
1599
- driver.rejectDone(driver.error);
1600
- }
1601
- else if (driver.resolveDone) {
1602
- driver.resolveDone();
1603
- }
1604
- return;
1605
- default:
1606
- // Unknown messages mid-COPY-IN are protocol errors; record and let
1607
- // the trailing ReadyForQuery flush the state.
1608
- driver.error = {
1609
- severity: "ERROR",
1610
- message: `Unexpected backend message during COPY IN: ${msg.type}`,
1611
- };
1612
- return;
1613
- }
1614
- }
1615
- handleCopyOutMessage(msg) {
1616
- const driver = this.copyOut;
1617
- if (!driver)
1618
- return;
1619
- switch (msg.type) {
1620
- case "CopyData": {
1621
- // Consumer broke early: drop instead of buffering (review item #11).
1622
- if (driver.abandoned)
1623
- return;
1624
- driver.queue.push(msg.data);
1625
- driver.queuedBytes += msg.data.length;
1626
- // Apply backpressure when the sink falls behind: pause the socket so
1627
- // the server stops flooding us. `next()` resumes at the low-water
1628
- // mark. (No-op if the socket doesn't expose pause(), e.g. a mock.)
1629
- if (driver.queuedBytes >= COPY_OUT_HWM) {
1630
- this.socket.pause?.();
1631
- }
1632
- if (driver.waker) {
1633
- const w = driver.waker;
1634
- driver.waker = null;
1635
- w();
1636
- }
1637
- return;
1638
- }
1639
- case "CopyDone":
1640
- // Server signals it's done sending — we now expect CommandComplete +
1641
- // ReadyForQuery. Stay in in-copy-out until ReadyForQuery; the queue
1642
- // may still drain via the consumer.
1643
- return;
1644
- case "CommandComplete":
1645
- driver.commandTag = msg.tag;
1646
- this.lastCopyTag = msg.tag;
1647
- return;
1648
- case "ErrorResponse":
1649
- driver.error = fieldsToConnectError(msg.fields);
1650
- return;
1651
- case "NoticeResponse":
1652
- this.notify.emit(fieldsToNotice(msg.fields));
1653
- return;
1654
- case "ParameterStatus":
1655
- this.params.set(msg.name, msg.value);
1656
- return;
1657
- case "ReadyForQuery":
1658
- this.txStatus = msg.status;
1659
- this.state = "idle";
1660
- driver.done = true;
1661
- this.copyOut = null;
1662
- if (driver.waker) {
1663
- const w = driver.waker;
1664
- driver.waker = null;
1665
- w();
1666
- }
1667
- return;
1668
- default:
1669
- driver.error = {
1670
- severity: "ERROR",
1671
- message: `Unexpected backend message during COPY OUT: ${msg.type}`,
1672
- };
1673
- if (driver.waker) {
1674
- const w = driver.waker;
1675
- driver.waker = null;
1676
- w();
1677
- }
1678
- return;
1679
- }
1680
- }
1681
- // -------------------------------------------------------------------------
1682
- // Query state machine
1683
- // -------------------------------------------------------------------------
1684
- handleQueryMessage(msg) {
1685
- const q = this.pendingQuery;
1686
- if (!q) {
1687
- // No active execSimple — we must be in the "starting a COPY" phase
1688
- // where the caller wrote Query("COPY …") via startCopyIn/startCopyOut.
1689
- this.handleCopyStartMessage(msg);
1690
- return;
1691
- }
1692
- switch (msg.type) {
1693
- case "RowDescription":
1694
- q.current = {
1695
- command: "",
1696
- rowCount: null,
1697
- oid: null,
1698
- fields: msg.fields,
1699
- rows: [],
1700
- notices: [],
1701
- };
1702
- return;
1703
- case "DataRow": {
1704
- if (!q.current) {
1705
- // Server sent rows without a prior description — extremely rare
1706
- // (only for some legacy COPY error paths). Treat as empty desc.
1707
- q.current = {
1708
- command: "",
1709
- rowCount: null,
1710
- oid: null,
1711
- fields: [],
1712
- rows: [],
1713
- notices: [],
1714
- };
1715
- }
1716
- q.current.rows.push(decodeDataRow(msg.values, q.current.fields));
1717
- return;
1718
- }
1719
- case "CommandComplete": {
1720
- const { command, rowCount, oid } = parseCommandTag(msg.tag);
1721
- const set = q.current ?? {
1722
- command,
1723
- rowCount: rowCount,
1724
- oid,
1725
- fields: [],
1726
- rows: [],
1727
- notices: [],
1728
- };
1729
- set.command = command;
1730
- set.rowCount = rowCount;
1731
- set.oid = oid;
1732
- set.notices = q.notices.splice(0);
1733
- q.finished.push(set);
1734
- q.current = null;
1735
- return;
1736
- }
1737
- case "EmptyQueryResponse": {
1738
- const set = {
1739
- command: "",
1740
- rowCount: null,
1741
- oid: null,
1742
- fields: [],
1743
- rows: [],
1744
- notices: q.notices.splice(0),
1745
- };
1746
- q.finished.push(set);
1747
- q.current = null;
1748
- return;
1749
- }
1750
- case "ParameterStatus":
1751
- this.params.set(msg.name, msg.value);
1752
- if (msg.name === "server_version") {
1753
- this.serverVersion = parseServerVersion(msg.value);
1754
- }
1755
- return;
1756
- case "NoticeResponse": {
1757
- const notice = fieldsToNotice(msg.fields);
1758
- q.notices.push(notice);
1759
- this.notify.emit(notice);
1760
- return;
1761
- }
1762
- case "NotificationResponse":
1763
- this.notify.emitNotification(msg.channel, msg.payload, msg.processId);
1764
- return;
1765
- case "ErrorResponse": {
1766
- q.error = fieldsToConnectError(msg.fields);
1767
- // Don't reject yet — ReadyForQuery will arrive shortly and we want
1768
- // to drain queued NoticeResponse messages first.
1769
- return;
1770
- }
1771
- case "ReadyForQuery": {
1772
- this.txStatus = msg.status;
1773
- this.state = "idle";
1774
- this.pendingQuery = null;
1775
- if (q.error) {
1776
- // Mirror libpq's behaviour: the result list contains every
1777
- // PGresult the server produced before the ErrorResponse — for
1778
- // a `\;`-chained simple-query batch, that's all the statements
1779
- // before the failing one. We surface them by attaching the
1780
- // accumulated `finished[]` to the thrown Error so callers
1781
- // (`executeAndPrint`) can render the pre-error rows in order
1782
- // before printing the error itself.
1783
- const err = asThrowable(q.error);
1784
- err.partialResults = q.finished;
1785
- q.reject(err);
1786
- }
1787
- else {
1788
- q.resolve(q.finished);
1789
- }
1790
- return;
1791
- }
1792
- case "CopyInResponse": {
1793
- // PG 17 added pipeline + COPY support but libpq still rejects the
1794
- // combination ("COPY in a pipeline is not supported, aborting
1795
- // connection"). Upstream psql surfaces that diagnostic and tears down
1796
- // the connection. We mirror the behaviour: if the user fires a COPY
1797
- // statement via execSimple while a pipeline is active, abort.
1798
- if (this._extPipelineActive) {
1799
- this.abortForCopyInPipeline();
1800
- return;
1801
- }
1802
- // CopyInResponse during execSimple (no active CopyIn driver) — the
1803
- // common path is `COPY ... FROM STDIN` as one segment of a `\;`-chained
1804
- // simple-query batch. Upstream psql pumps stdin lines until `\.`; the
1805
- // mainloop pre-scans its input and buffers the bytes into
1806
- // `copyInMidBatchQueue` before calling execSimple. We pop the head
1807
- // buffer and ship it as CopyData + CopyDone. If no buffer is queued
1808
- // (caller forgot to seed, or scan was inaccurate), CopyFail so the
1809
- // server returns to ReadyForQuery rather than blocking.
1810
- const data = this.copyInMidBatchQueue.shift();
1811
- if (data !== undefined) {
1812
- try {
1813
- // Empty payload still needs a CopyDone — the server transitions
1814
- // back to CopyIn-done state on CopyDone regardless of byte
1815
- // count. Wrapping a zero-length CopyData is harmless.
1816
- if (data.length > 0) {
1817
- this.socket.write(CopyData(data));
1818
- }
1819
- this.socket.write(CopyDone());
1820
- }
1821
- catch {
1822
- // Write failures are surfaced via socket 'error' / 'close'
1823
- // handlers which will fail the pending query.
1824
- }
1825
- return;
1826
- }
1827
- q.error = {
1828
- severity: "ERROR",
1829
- message: "COPY FROM STDIN not supported via execSimple — use \\copy or startCopyIn",
1830
- };
1831
- try {
1832
- this.socket.write(CopyFail("COPY FROM STDIN not driven by client"));
1833
- }
1834
- catch {
1835
- // Write failures are surfaced via socket 'error' / 'close' handlers
1836
- // which will fail the pending query — nothing to do here.
1837
- }
1838
- return;
1839
- }
1840
- case "CopyOutResponse": {
1841
- if (this._extPipelineActive) {
1842
- this.abortForCopyInPipeline();
1843
- return;
1844
- }
1845
- // CopyOutResponse during execSimple (no active CopyOut driver) — the
1846
- // common path is `COPY ... TO STDOUT` as one segment of a `\;`-chained
1847
- // simple-query batch. We accumulate the CopyData payloads onto the
1848
- // current ResultSet's `copyOutBytes` so the renderer emits them at
1849
- // the result's position in the chain — instead of streaming them
1850
- // straight to a sink at receive time, which would hoist the COPY
1851
- // bytes above any tuples-producing results that haven't been
1852
- // rendered yet (see hunk 5722-5730 in regress/psql).
1853
- this.copyOutMidBatchActive = true;
1854
- q.current = q.current ?? {
1855
- command: "",
1856
- rowCount: null,
1857
- oid: null,
1858
- fields: [],
1859
- rows: [],
1860
- notices: [],
1861
- };
1862
- q.current.copyOutBytes = q.current.copyOutBytes ?? [];
1863
- return;
1864
- }
1865
- case "CopyData": {
1866
- // CopyData arrives during execSimple only when we're in the mid-batch
1867
- // COPY-OUT phase (CopyOutResponse flipped the flag above). Stash the
1868
- // payload on the current result's `copyOutBytes` so the caller can
1869
- // render in order. Anything else is a protocol error.
1870
- if (this.copyOutMidBatchActive) {
1871
- const cur = q.current;
1872
- if (cur) {
1873
- cur.copyOutBytes = cur.copyOutBytes ?? [];
1874
- cur.copyOutBytes.push(msg.data);
1875
- }
1876
- return;
1877
- }
1878
- q.error = {
1879
- severity: "ERROR",
1880
- message: "Unexpected backend message during query: CopyData",
1881
- };
1882
- return;
1883
- }
1884
- case "CopyDone": {
1885
- // Server signals end of COPY-OUT data — next message will be
1886
- // CommandComplete for the COPY statement, then the batch resumes.
1887
- if (this.copyOutMidBatchActive) {
1888
- this.copyOutMidBatchActive = false;
1889
- return;
1890
- }
1891
- q.error = {
1892
- severity: "ERROR",
1893
- message: "Unexpected backend message during query: CopyDone",
1894
- };
1895
- return;
1896
- }
1897
- case "CopyBothResponse": {
1898
- // Walsender (`replication=database` / `replication=true`) commands
1899
- // such as `START_REPLICATION` transition the connection into a
1900
- // CopyBoth streaming phase (WAL records flowing from server +
1901
- // keepalive replies flowing from client). This client does not
1902
- // implement WAL streaming — upstream libpq's `PQexec` similarly
1903
- // refuses to handle PGRES_COPY_BOTH and surfaces a diagnostic. We
1904
- // mirror that: reject the pending query with a "syntax error" style
1905
- // message (matching the conformance assertion) and tear the socket
1906
- // down so the next query / process exit is clean.
1907
- const cbErr = {
1908
- severity: "ERROR",
1909
- code: "0A000",
1910
- message: "syntax error: unexpected CopyBothResponse from server (replication streaming is not supported by this client)",
1911
- };
1912
- q.error = cbErr;
1913
- q.reject(asThrowable(cbErr));
1914
- this.pendingQuery = null;
1915
- this.socketError = new Error(cbErr.message);
1916
- try {
1917
- this.socket.destroy();
1918
- }
1919
- catch {
1920
- // ignore
1921
- }
1922
- this.state = "closed";
1923
- return;
1924
- }
1925
- default:
1926
- // Unknown messages during a query are protocol errors but not fatal
1927
- // for the connection — record them.
1928
- q.error = {
1929
- severity: "ERROR",
1930
- message: `Unexpected backend message during query: ${msg.type}`,
1931
- };
1932
- return;
1933
- }
1934
- }
1935
- // -------------------------------------------------------------------------
1936
- // Socket → parser → state dispatch
1937
- // -------------------------------------------------------------------------
1938
- onData(chunk) {
1939
- let messages;
1940
- try {
1941
- messages = this.parser.feed(chunk);
1942
- }
1943
- catch (err) {
1944
- this.socketError =
1945
- err instanceof Error ? err : new Error(String(err));
1946
- this.failPending(this.socketError);
1947
- try {
1948
- this.socket.destroy();
1949
- }
1950
- catch {
1951
- // ignore
1952
- }
1953
- this.state = "closed";
1954
- return;
1955
- }
1956
- for (const msg of messages) {
1957
- this.dispatch(msg);
1958
- if (this.state === "closed")
1959
- break;
1960
- }
1961
- }
1962
- dispatch(msg) {
1963
- // Async backend messages always allowed. NotificationResponse can arrive
1964
- // in *any* state since LISTEN payloads come in whenever a NOTIFY fires.
1965
- if (msg.type === "NotificationResponse") {
1966
- this.notify.emitNotification(msg.channel, msg.payload, msg.processId);
1967
- return;
1968
- }
1969
- switch (this.state) {
1970
- case "auth":
1971
- this.handleAuthMessage(msg);
1972
- return;
1973
- case "await-ready":
1974
- this.handleAwaitReady(msg);
1975
- return;
1976
- case "idle":
1977
- // ParameterStatus changes can arrive asynchronously (SET).
1978
- if (msg.type === "ParameterStatus") {
1979
- this.params.set(msg.name, msg.value);
1980
- if (msg.name === "server_version") {
1981
- this.serverVersion = parseServerVersion(msg.value);
1982
- }
1983
- return;
1984
- }
1985
- if (msg.type === "NoticeResponse") {
1986
- this.notify.emit(fieldsToNotice(msg.fields));
1987
- return;
1988
- }
1989
- // (NotificationResponse is handled by the early-out above.)
1990
- // Anything else in idle is unexpected.
1991
- this.socketError = new Error(`Unexpected ${msg.type} in idle state`);
1992
- try {
1993
- this.socket.destroy();
1994
- }
1995
- catch {
1996
- // ignore
1997
- }
1998
- this.state = "closed";
1999
- return;
2000
- case "in-query":
2001
- this.handleQueryMessage(msg);
2002
- return;
2003
- case "in-extended":
2004
- this.handleExtendedMessage(msg);
2005
- return;
2006
- case "in-copy-in":
2007
- this.handleCopyInMessage(msg);
2008
- return;
2009
- case "in-copy-out":
2010
- this.handleCopyOutMessage(msg);
2011
- return;
2012
- case "closed":
2013
- return;
2014
- }
2015
- }
2016
- failPending(err) {
2017
- if (this.pendingQuery) {
2018
- const q = this.pendingQuery;
2019
- this.pendingQuery = null;
2020
- // If the server delivered an ErrorResponse just before the socket
2021
- // closed (e.g. a FATAL "terminating connection due to administrator
2022
- // command" when the backend is killed mid-query), prefer that
2023
- // structured error over the generic "Socket closed" fallback so the
2024
- // diagnostic carries the server's wording. Mirrors libpq's behaviour
2025
- // where `PQexec` surfaces the FATAL message and `PQerrorMessage`
2026
- // returns the server-supplied text.
2027
- //
2028
- // `q.error` is initialised to `null` by `execSimple`; only a non-null
2029
- // value indicates a server-side ErrorResponse was actually captured.
2030
- q.reject(q.error != null ? asThrowable(q.error) : err);
2031
- }
2032
- if (this.extDriver) {
2033
- const d = this.extDriver;
2034
- this.extDriver = null;
2035
- for (const op of d.queue)
2036
- op.reject(err);
2037
- }
2038
- if (this.startupReject) {
2039
- const r = this.startupReject;
2040
- this.startupResolve = null;
2041
- this.startupReject = null;
2042
- r(err);
2043
- }
2044
- if (this.copyStartReject) {
2045
- const r = this.copyStartReject;
2046
- this.copyStartResolve = null;
2047
- this.copyStartReject = null;
2048
- r(err);
2049
- }
2050
- if (this.copyIn) {
2051
- const d = this.copyIn;
2052
- this.copyIn = null;
2053
- if (d.rejectDone)
2054
- d.rejectDone(err);
2055
- }
2056
- if (this.copyOut) {
2057
- const d = this.copyOut;
2058
- this.copyOut = null;
2059
- d.error =
2060
- err instanceof Error
2061
- ? { severity: "ERROR", message: err.message }
2062
- : { severity: "ERROR", message: String(err) };
2063
- if (d.waker) {
2064
- const w = d.waker;
2065
- d.waker = null;
2066
- w();
2067
- }
2068
- }
2069
- }
2070
- ensureIdle() {
2071
- if (this.state === "closed") {
2072
- throw new Error("PgConnection: connection is closed");
2073
- }
2074
- if (this.state !== "idle" && this.state !== "in-extended") {
2075
- throw new Error(`PgConnection: cannot start query in state ${this.state}`);
2076
- }
2077
- }
2078
- // -------------------------------------------------------------------------
2079
- // Extended-protocol driver (WP-21).
2080
- //
2081
- // Each public enqueueX method appends one op to `extDriver.queue` and
2082
- // returns a Promise that resolves when the op's terminator backend message
2083
- // arrives. The caller is responsible for writing the matching wire frame
2084
- // (Parse/Bind/Describe/Execute/Close/Sync) to the socket.
2085
- // -------------------------------------------------------------------------
2086
- startExtendedBatch() {
2087
- if (this.state === "idle") {
2088
- this.state = "in-extended";
2089
- this.extDriver = { queue: [], error: null };
2090
- }
2091
- else if (this.state !== "in-extended") {
2092
- throw new Error(`PgConnection: cannot start extended batch in state ${this.state}`);
2093
- }
2094
- else if (!this.extDriver) {
2095
- this.extDriver = { queue: [], error: null };
2096
- }
2097
- }
2098
- writeRaw(buf) {
2099
- this.socket.write(buf);
2100
- }
2101
- enqueueParse() {
2102
- return this.enqueueOp({
2103
- kind: "parse",
2104
- resolve: () => undefined,
2105
- reject: () => undefined,
2106
- });
2107
- }
2108
- enqueueBind() {
2109
- return this.enqueueOp({
2110
- kind: "bind",
2111
- resolve: () => undefined,
2112
- reject: () => undefined,
2113
- });
2114
- }
2115
- enqueueDescribeStatement() {
2116
- return this.enqueueOp({
2117
- kind: "describeS",
2118
- resolve: () => undefined,
2119
- reject: () => undefined,
2120
- paramOids: null,
2121
- });
2122
- }
2123
- enqueueDescribePortal() {
2124
- return this.enqueueOp({
2125
- kind: "describeP",
2126
- resolve: () => undefined,
2127
- reject: () => undefined,
2128
- });
2129
- }
2130
- /**
2131
- * Variant of {@link enqueueDescribePortal} that pipes the resolved fields
2132
- * onto the very next `execute` op already (or yet to be) on the queue.
2133
- */
2134
- enqueueDescribePortalIntoNextExecute() {
2135
- const driver = this.extDriver;
2136
- if (!driver) {
2137
- return Promise.reject(new Error("enqueueDescribePortalIntoNextExecute: not in extended state"));
2138
- }
2139
- return new Promise((resolve, reject) => {
2140
- driver.queue.push({
2141
- kind: "describeP",
2142
- resolve: (v) => {
2143
- const fields = v;
2144
- for (const op of driver.queue) {
2145
- if (op.kind === "execute" && op.fields === null) {
2146
- op.fields = fields;
2147
- break;
2148
- }
2149
- }
2150
- resolve();
2151
- },
2152
- reject,
2153
- });
2154
- });
2155
- }
2156
- enqueueExecute() {
2157
- return this.enqueueOp({
2158
- kind: "execute",
2159
- resolve: () => undefined,
2160
- reject: () => undefined,
2161
- current: null,
2162
- notices: [],
2163
- fields: null,
2164
- });
2165
- }
2166
- enqueueExecuteWithFields(fields) {
2167
- return this.enqueueOp({
2168
- kind: "execute",
2169
- resolve: () => undefined,
2170
- reject: () => undefined,
2171
- current: null,
2172
- notices: [],
2173
- fields,
2174
- });
2175
- }
2176
- enqueueClose() {
2177
- return this.enqueueOp({
2178
- kind: "close",
2179
- resolve: () => undefined,
2180
- reject: () => undefined,
2181
- });
2182
- }
2183
- enqueueSync() {
2184
- return this.enqueueOp({
2185
- kind: "sync",
2186
- resolve: () => undefined,
2187
- reject: () => undefined,
2188
- });
2189
- }
2190
- enqueueOp(opSkeleton) {
2191
- if (!this.extDriver) {
2192
- return Promise.reject(new Error("enqueueOp: not in extended state"));
2193
- }
2194
- const driver = this.extDriver;
2195
- return new Promise((resolve, reject) => {
2196
- const op = opSkeleton;
2197
- op.resolve = resolve;
2198
- op.reject = reject;
2199
- driver.queue.push(op);
2200
- });
2201
- }
2202
- handleExtendedMessage(msg) {
2203
- const driver = this.extDriver;
2204
- if (!driver)
2205
- return;
2206
- if (msg.type === "ParameterStatus") {
2207
- this.params.set(msg.name, msg.value);
2208
- if (msg.name === "server_version") {
2209
- this.serverVersion = parseServerVersion(msg.value);
2210
- }
2211
- return;
2212
- }
2213
- if (msg.type === "NoticeResponse") {
2214
- const notice = fieldsToNotice(msg.fields);
2215
- this.notify.emit(notice);
2216
- const head = driver.queue[0];
2217
- if (head && head.kind === "execute")
2218
- head.notices.push(notice);
2219
- return;
2220
- }
2221
- if (msg.type === "NotificationResponse") {
2222
- this.notify.emitNotification(msg.channel, msg.payload, msg.processId);
2223
- return;
2224
- }
2225
- if (msg.type === "ErrorResponse") {
2226
- driver.error = fieldsToConnectError(msg.fields);
2227
- // Reject ALL queued non-sync ops eagerly. Upstream server semantics:
2228
- // once a P/B/D/E op errors, the server skips every subsequent message
2229
- // until the next Sync. If the client (e.g. `\flushrequest` + `\getresults`
2230
- // after an aborted bind) doesn't issue Sync next, no further wire
2231
- // messages will arrive — so we must cascade-reject the rest of the
2232
- // queue NOW or those promises hang forever.
2233
- //
2234
- // Mirror libpq's `PGRES_PIPELINE_ABORTED` marker for follow-on ops:
2235
- // the FIRST failing op carries the real `ErrorResponse` payload,
2236
- // every subsequent op gets a synthetic "Pipeline aborted, command
2237
- // did not run" error so `\getresults` / `\endpipeline` can
2238
- // distinguish the originating ERROR from the cascaded skips. See
2239
- // upstream `pqPipelineProcessQueue` in `fe-exec.c`.
2240
- let first = true;
2241
- while (driver.queue.length > 0) {
2242
- const head = driver.queue[0];
2243
- if (head.kind === "sync")
2244
- break;
2245
- driver.queue.shift();
2246
- if (first) {
2247
- head.reject(driver.error);
2248
- first = false;
2249
- }
2250
- else {
2251
- head.reject(pipelineAbortedError());
2252
- }
2253
- }
2254
- return;
2255
- }
2256
- // COPY-in-pipeline: when an `Execute` in pipeline mode hits a
2257
- // `COPY ... FROM STDIN` / `COPY ... TO STDOUT`, the server replies
2258
- // with `CopyInResponse` / `CopyOutResponse` instead of the usual
2259
- // result-stream messages. Upstream libpq refuses the combination
2260
- // with "COPY in a pipeline is not supported, aborting connection"
2261
- // and tears the connection down. Mirror that so `\startpipeline +
2262
- // COPY ...` surfaces the expected fatal error rather than hanging
2263
- // on a response the extended driver doesn't know how to consume.
2264
- if (msg.type === "CopyInResponse" || msg.type === "CopyOutResponse") {
2265
- this.abortForCopyInPipeline();
2266
- return;
2267
- }
2268
- // Drain any ops added to the queue AFTER the initial cascade-reject
2269
- // (e.g. a `\sendpipeline` issued by the user once `\getresults`
2270
- // returned the first ErrorResponse) — those ops were never visible
2271
- // to the original cascade loop, but the server skipped them too
2272
- // because it stays in PIPELINE_ABORTED until the next Sync. Mark
2273
- // them as cascaded (`pipelineAborted`) rather than the real error:
2274
- // libpq surfaces the real error only on the OP that actually
2275
- // failed, and stamps every subsequent skipped op with the
2276
- // PGRES_PIPELINE_ABORTED marker. The cmd layer's `\getresults` /
2277
- // `\endpipeline` paths render the marker as
2278
- // `Pipeline aborted, command did not run` (no `ERROR:` prefix).
2279
- while (driver.error !== null) {
2280
- const head = driver.queue[0];
2281
- if (!head || head.kind === "sync")
2282
- break;
2283
- driver.queue.shift();
2284
- head.reject(pipelineAbortedError());
2285
- }
2286
- const head = driver.queue[0];
2287
- if (!head) {
2288
- this.protocolFail(new Error(`Unexpected backend message ${msg.type} in in-extended`));
2289
- return;
2290
- }
2291
- switch (msg.type) {
2292
- case "ParseComplete":
2293
- if (head.kind !== "parse") {
2294
- this.protocolFail(new Error("ParseComplete arrived but head op is " + head.kind));
2295
- return;
2296
- }
2297
- driver.queue.shift();
2298
- head.resolve(undefined);
2299
- return;
2300
- case "BindComplete":
2301
- if (head.kind !== "bind") {
2302
- this.protocolFail(new Error("BindComplete arrived but head op is " + head.kind));
2303
- return;
2304
- }
2305
- driver.queue.shift();
2306
- head.resolve(undefined);
2307
- return;
2308
- case "CloseComplete":
2309
- if (head.kind !== "close") {
2310
- this.protocolFail(new Error("CloseComplete arrived but head op is " + head.kind));
2311
- return;
2312
- }
2313
- driver.queue.shift();
2314
- head.resolve(undefined);
2315
- return;
2316
- case "ParameterDescription":
2317
- if (head.kind !== "describeS") {
2318
- this.protocolFail(new Error("ParameterDescription arrived but head op is " +
2319
- head.kind));
2320
- return;
2321
- }
2322
- head.paramOids = msg.oids;
2323
- return;
2324
- case "RowDescription":
2325
- if (head.kind === "describeS") {
2326
- driver.queue.shift();
2327
- head.resolve({
2328
- paramOids: head.paramOids ?? [],
2329
- fields: msg.fields,
2330
- });
2331
- return;
2332
- }
2333
- if (head.kind === "describeP") {
2334
- driver.queue.shift();
2335
- head.resolve(msg.fields);
2336
- return;
2337
- }
2338
- if (head.kind === "execute") {
2339
- head.current = {
2340
- command: "",
2341
- rowCount: null,
2342
- oid: null,
2343
- fields: msg.fields,
2344
- rows: [],
2345
- notices: [],
2346
- };
2347
- head.fields = msg.fields;
2348
- return;
2349
- }
2350
- this.protocolFail(new Error("Unexpected RowDescription at head op " + head.kind));
2351
- return;
2352
- case "NoData":
2353
- if (head.kind === "describeS") {
2354
- driver.queue.shift();
2355
- head.resolve({
2356
- paramOids: head.paramOids ?? [],
2357
- fields: [],
2358
- });
2359
- return;
2360
- }
2361
- if (head.kind === "describeP") {
2362
- driver.queue.shift();
2363
- head.resolve([]);
2364
- return;
2365
- }
2366
- this.protocolFail(new Error("Unexpected NoData at head op " + head.kind));
2367
- return;
2368
- case "DataRow": {
2369
- if (head.kind !== "execute") {
2370
- this.protocolFail(new Error("DataRow at head op " + head.kind));
2371
- return;
2372
- }
2373
- const fields = head.fields ?? head.current?.fields ?? [];
2374
- if (!head.current) {
2375
- head.current = {
2376
- command: "",
2377
- rowCount: null,
2378
- oid: null,
2379
- fields,
2380
- rows: [],
2381
- notices: [],
2382
- };
2383
- }
2384
- head.current.rows.push(decodeDataRow(msg.values, fields));
2385
- return;
2386
- }
2387
- case "CommandComplete": {
2388
- if (head.kind !== "execute") {
2389
- this.protocolFail(new Error("CommandComplete at head op " + head.kind));
2390
- return;
2391
- }
2392
- const { command, rowCount, oid } = parseCommandTag(msg.tag);
2393
- const set = head.current ?? {
2394
- command,
2395
- rowCount,
2396
- oid,
2397
- fields: head.fields ?? [],
2398
- rows: [],
2399
- notices: [],
2400
- };
2401
- set.command = command;
2402
- set.rowCount = rowCount;
2403
- set.oid = oid;
2404
- set.notices = head.notices.splice(0);
2405
- driver.queue.shift();
2406
- head.resolve(set);
2407
- return;
2408
- }
2409
- case "EmptyQueryResponse": {
2410
- if (head.kind !== "execute") {
2411
- this.protocolFail(new Error("EmptyQueryResponse at head op " + head.kind));
2412
- return;
2413
- }
2414
- const set = {
2415
- command: "",
2416
- rowCount: null,
2417
- oid: null,
2418
- fields: [],
2419
- rows: [],
2420
- notices: head.notices.splice(0),
2421
- };
2422
- driver.queue.shift();
2423
- head.resolve(set);
2424
- return;
2425
- }
2426
- case "PortalSuspended": {
2427
- if (head.kind !== "execute") {
2428
- this.protocolFail(new Error("PortalSuspended at head op " + head.kind));
2429
- return;
2430
- }
2431
- const set = head.current ?? {
2432
- command: "",
2433
- rowCount: null,
2434
- oid: null,
2435
- fields: head.fields ?? [],
2436
- rows: [],
2437
- notices: head.notices.splice(0),
2438
- };
2439
- set.notices = head.notices.splice(0);
2440
- driver.queue.shift();
2441
- head.resolve(set);
2442
- return;
2443
- }
2444
- case "ReadyForQuery": {
2445
- this.txStatus = msg.status;
2446
- if (head.kind !== "sync") {
2447
- this.protocolFail(new Error("ReadyForQuery but head op is " + head.kind));
2448
- return;
2449
- }
2450
- driver.queue.shift();
2451
- const stickyErr = driver.error;
2452
- driver.error = null;
2453
- if (stickyErr) {
2454
- head.reject(stickyErr);
2455
- }
2456
- else {
2457
- head.resolve(undefined);
2458
- }
2459
- if (driver.queue.length === 0 && !this._extPipelineActive) {
2460
- this.state = "idle";
2461
- this.extDriver = null;
2462
- }
2463
- return;
2464
- }
2465
- default:
2466
- this.protocolFail(new Error(`Unexpected ${msg.type} in in-extended state`));
2467
- return;
2468
- }
2469
- }
2470
- protocolFail(err) {
2471
- this.socketError = err;
2472
- this.failPending(err);
2473
- try {
2474
- this.socket.destroy();
2475
- }
2476
- catch {
2477
- // ignore
2478
- }
2479
- this.state = "closed";
2480
- }
2481
- /**
2482
- * Abort the connection because the server replied with CopyInResponse /
2483
- * CopyOutResponse while a pipeline (`_extPipelineActive`) was active.
2484
- * Upstream libpq emits the exact diagnostic
2485
- * `"COPY in a pipeline is not supported, aborting connection"` and tears
2486
- * the socket down — we mirror that. Pending operations are rejected; the
2487
- * connection is left in `closed` so subsequent commands fail cleanly
2488
- * (matching the "aborting connection" promise).
2489
- */
2490
- abortForCopyInPipeline() {
2491
- const err = {
2492
- severity: "FATAL",
2493
- message: "COPY in a pipeline is not supported, aborting connection",
2494
- };
2495
- this.socketError = new Error(err.message);
2496
- this.failPending(err);
2497
- try {
2498
- this.socket.destroy();
2499
- }
2500
- catch {
2501
- // ignore
2502
- }
2503
- this.state = "closed";
2504
- }
2505
- }
2506
- // -------------------------------------------------------------------------
2507
- // Public factory
2508
- // -------------------------------------------------------------------------
2509
- /**
2510
- * Pluggable random source for `load_balance_hosts=random`. Public so tests
2511
- * can inject a deterministic permutation; production code leaves it `null`
2512
- * and falls back to `Math.random`. NOT part of the connection's external
2513
- * contract — internal-only escape hatch.
2514
- */
2515
- PgConnection._loadBalanceRng = null;
140
+ * COPY-OUT backpressure thresholds (bytes). When buffered data exceeds the
141
+ * high-water mark we pause the socket; the consumer's `next()` resumes it once
142
+ * it drains below the low-water mark. Bounds RSS for a server that produces
143
+ * rows faster than the sink consumes them (review item #11).
144
+ */
145
+ const COPY_OUT_HWM = 8388608;
146
+ const COPY_OUT_LWM = 1048576;
147
+ var PgConnection = class PgConnection {
148
+ /**
149
+ * Queue one COPY-FROM-STDIN data block. Call once per expected
150
+ * CopyInResponse, in the order they will arrive during the upcoming
151
+ * `execSimple`. The mainloop owns the parsing of `\.`-terminated stdin
152
+ * blocks; we just buffer the raw bytes and ship them when the server is
153
+ * ready.
154
+ */
155
+ queueCopyInData(data) {
156
+ this.copyInMidBatchQueue.push(data);
157
+ }
158
+ /** Drop any queued COPY-FROM-STDIN data blocks. */
159
+ clearCopyInDataQueue() {
160
+ this.copyInMidBatchQueue.length = 0;
161
+ }
162
+ constructor(socket, opts, channelBindingData) {
163
+ this.parser = new MessageParser();
164
+ this.serverVersion = 0;
165
+ this.params = /* @__PURE__ */ new Map();
166
+ this.processId = 0;
167
+ this.secretKey = 0;
168
+ this.txStatus = "I";
169
+ this.state = "auth";
170
+ this.pendingQuery = null;
171
+ this.extDriver = null;
172
+ this._extPipelineActive = false;
173
+ this.copyIn = null;
174
+ this.copyOut = null;
175
+ this.copyStartResolve = null;
176
+ this.copyStartReject = null;
177
+ this.lastCopyTag = null;
178
+ this.copyInMidBatchQueue = [];
179
+ this.copyOutMidBatchSink = null;
180
+ this.copyOutMidBatchActive = false;
181
+ this.passwordUsed = false;
182
+ this.notify = new NoticeMultiplexer();
183
+ this.scram = null;
184
+ this.authChallengeSeen = false;
185
+ this.saslFinalSeen = false;
186
+ this.socketError = null;
187
+ this.startupResolve = null;
188
+ this.startupReject = null;
189
+ this.socket = socket;
190
+ this.opts = opts;
191
+ this.channelBindingData = channelBindingData;
192
+ this._password = opts.password ?? null;
193
+ socket.on("data", (chunk) => {
194
+ this.onData(chunk);
195
+ });
196
+ socket.on("error", (err) => {
197
+ this.socketError = err;
198
+ this.failPending(err);
199
+ });
200
+ socket.on("close", () => {
201
+ if (this.state !== "closed") {
202
+ this.state = "closed";
203
+ this.failPending(this.socketError ?? /* @__PURE__ */ new Error("Socket closed"));
204
+ }
205
+ });
206
+ }
207
+ static {
208
+ this._loadBalanceRng = null;
209
+ }
210
+ static {
211
+ this._dnsLookupAll = null;
212
+ }
213
+ /**
214
+ * Open a Postgres connection. Supports multi-host (`opts.hosts`) with
215
+ * sequential or random iteration, a `target_session_attrs` filter, and the
216
+ * libpq-style `prefer-standby` two-pass fallback.
217
+ *
218
+ * Iteration semantics:
219
+ * 1. Build the candidate list from `opts.hosts` (preferred) or
220
+ * `[{host: opts.host, port: opts.port}]`.
221
+ * 2. If `loadBalanceHosts === 'random'`, Fisher-Yates shuffle in place.
222
+ * 3. For each candidate (in order), attempt: openSocket → TLS → auth →
223
+ * startup. On failure, record the error and try the next.
224
+ * 4. On successful handshake, if `target_session_attrs` is restrictive,
225
+ * run `SELECT pg_is_in_recovery()`. If the role doesn't match, close
226
+ * this connection and try the next.
227
+ * 5. `prefer-standby` runs TWO passes: first accepting only standbys,
228
+ * second falling back to any host.
229
+ * 6. If no candidate succeeds, throw the LAST encountered error
230
+ * (preserves the most-recent failure mode for diagnostics).
231
+ */
232
+ static async connect(opts) {
233
+ const seed = opts.hosts !== void 0 && opts.hosts.length > 0 ? [...opts.hosts] : [{
234
+ host: opts.host,
235
+ port: opts.port
236
+ }];
237
+ const candidates = opts.hostaddr !== void 0 && opts.hostaddr !== "" ? seed.map((c) => ({
238
+ host: c.host,
239
+ address: opts.hostaddr,
240
+ port: c.port
241
+ })) : await expandHostsViaDns(seed);
242
+ if (opts.loadBalanceHosts === "random") shuffleInPlace(candidates, PgConnection._loadBalanceRng ?? Math.random);
243
+ const tsa = opts.targetSessionAttrs ?? "any";
244
+ const passes = tsa === "prefer-standby" ? ["standby", "any"] : [tsa];
245
+ let lastErr = null;
246
+ for (const passTsa of passes) for (const candidate of candidates) {
247
+ const candidateOpts = {
248
+ ...opts,
249
+ host: candidate.host,
250
+ port: candidate.port
251
+ };
252
+ let conn;
253
+ try {
254
+ conn = await PgConnection.connectSingle(candidateOpts, candidate.address);
255
+ } catch (err) {
256
+ lastErr = err;
257
+ continue;
258
+ }
259
+ if (await checkSessionAttrs(conn, passTsa)) return conn;
260
+ try {
261
+ await conn.close();
262
+ } catch {}
263
+ lastErr = /* @__PURE__ */ new Error(`target_session_attrs=${String(passTsa)} did not match host ${candidate.host}:${String(candidate.port)}`);
264
+ }
265
+ if (lastErr !== null) {
266
+ if (lastErr instanceof Error) throw lastErr;
267
+ let message;
268
+ if (typeof lastErr === "object" && lastErr !== null && "message" in lastErr && typeof lastErr.message === "string") message = lastErr.message;
269
+ else if (typeof lastErr === "string" || typeof lastErr === "number" || typeof lastErr === "boolean") message = String(lastErr);
270
+ else message = "PgConnection.connect: unknown error";
271
+ const wrapped = new Error(message);
272
+ wrapped.cause = lastErr;
273
+ throw wrapped;
274
+ }
275
+ throw new Error("PgConnection.connect: no candidate hosts configured");
276
+ }
277
+ /**
278
+ * Per-host connect attempt: open the socket, negotiate TLS, run the auth
279
+ * dance, complete startup. Same shape as the pre-multihost `connect()`;
280
+ * the multi-host orchestrator above wraps this for each candidate.
281
+ */
282
+ static async connectSingle(opts, addressOverride) {
283
+ if (isUnixSocketHost(opts.host) && (opts.ssl === "require" || opts.ssl === "verify-ca" || opts.ssl === "verify-full")) throw new Error(`sslmode=${opts.ssl} is not supported over Unix-domain sockets (host=${opts.host})`);
284
+ if (isUnixSocketHost(opts.host) && opts.requirepeer !== void 0 && opts.requirepeer !== "") throw new Error(`requirepeer="${opts.requirepeer}" cannot be enforced: Node provides no Unix-domain socket peer-credential API; refusing to connect rather than skip the check`);
285
+ const rawSocket = await openSocket(opts, addressOverride);
286
+ let socket = rawSocket;
287
+ let channelBindingData = null;
288
+ try {
289
+ const servername = tlsServername(opts);
290
+ const tlsConnectionOptions = {
291
+ ...servername !== void 0 ? { servername } : {},
292
+ rejectUnauthorized: opts.ssl === "verify-ca" || opts.ssl === "verify-full",
293
+ ALPNProtocols: ["postgresql"]
294
+ };
295
+ if (opts.ssl !== "verify-full") tlsConnectionOptions.checkServerIdentity = () => void 0;
296
+ else if (opts.sslsni === false) tlsConnectionOptions.checkServerIdentity = (_host, cert) => tls.checkServerIdentity(opts.host, cert);
297
+ applyTlsProtocolVersionRange(tlsConnectionOptions, opts);
298
+ const tlsResult = await negotiateTls(rawSocket, isUnixSocketHost(opts.host) ? "disable" : opts.ssl, tlsConnectionOptions, {
299
+ sslcert: opts.sslcert,
300
+ sslkey: opts.sslkey,
301
+ sslcertmode: opts.sslcertmode,
302
+ sslpassword: opts.sslpassword,
303
+ sslrootcert: opts.sslrootcert,
304
+ sslcrl: opts.sslcrl,
305
+ sslcrldir: opts.sslcrldir,
306
+ sslkeylogfile: opts.sslkeylogfile
307
+ }, isUnixSocketHost(opts.host) ? "postgres" : opts.sslnegotiation ?? "postgres");
308
+ if (tlsResult.kind === "tls") {
309
+ socket = tlsResult.socket;
310
+ channelBindingData = tlsResult.channelBindingData;
311
+ } else socket = tlsResult.socket;
312
+ } catch (err) {
313
+ try {
314
+ rawSocket.destroy();
315
+ } catch {}
316
+ throw err;
317
+ }
318
+ const conn = new PgConnection(socket, opts, channelBindingData);
319
+ await conn.startup();
320
+ return conn;
321
+ }
322
+ parameterStatus(name) {
323
+ return this.params.get(name);
324
+ }
325
+ /**
326
+ * Expose the connection target as `meta.database` / `meta.user` / `meta.host`
327
+ * / `meta.port` / `meta.pid` so the prompt renderer (which duck-types these
328
+ * via `MaybeWithMeta`) can render `%/`, `%n`, `%m`, `%>`, `%p` without
329
+ * additional plumbing. Postgres doesn't emit a `database` ParameterStatus,
330
+ * so these come from the connect opts / BackendKeyData.
331
+ */
332
+ get database() {
333
+ return this.opts.database;
334
+ }
335
+ get user() {
336
+ return this.opts.user;
337
+ }
338
+ get host() {
339
+ return this.opts.host;
340
+ }
341
+ get port() {
342
+ return this.opts.port;
343
+ }
344
+ get pid() {
345
+ return this.processId;
346
+ }
347
+ /**
348
+ * The password supplied at connect time (or `null`). Mirrors libpq's
349
+ * retention of the password on the live `PGconn` so `\c <newdb>` can
350
+ * reconnect transparently. Read-only by design — the field is set once in
351
+ * the constructor and never mutated.
352
+ */
353
+ get password() {
354
+ return this._password;
355
+ }
356
+ /**
357
+ * If the connection was upgraded to TLS during negotiation, return the
358
+ * cipher info for the active session. Returns `null` for plain-text
359
+ * connections. Used by the startup banner to render an `SSL connection
360
+ * (protocol: …, cipher: …)` line that mirrors upstream psql, and by
361
+ * `\conninfo` (PG18 connection-information table) to fill the SSL rows.
362
+ */
363
+ getTlsInfo() {
364
+ const s = this.socket;
365
+ if (typeof s.getCipher !== "function") return null;
366
+ try {
367
+ const cipher = s.getCipher();
368
+ const protocol = s.getProtocol?.() ?? cipher.version ?? "unknown";
369
+ if (!cipher.name) return null;
370
+ const compression = "off";
371
+ const alpnRaw = s.alpnProtocol;
372
+ const alpn = typeof alpnRaw === "string" && alpnRaw.length > 0 ? alpnRaw : null;
373
+ return {
374
+ protocol: String(protocol),
375
+ cipher: cipher.standardName ?? cipher.name,
376
+ standardName: cipher.standardName,
377
+ compression,
378
+ alpn,
379
+ library: "OpenSSL",
380
+ keyBits: sslKeyBitsFromCipher(cipher.standardName ?? cipher.name)
381
+ };
382
+ } catch {
383
+ return null;
384
+ }
385
+ }
386
+ /**
387
+ * Static connection facts for the PG18 `\conninfo` table, sourced from the
388
+ * connect opts and the live socket (no SQL is issued):
389
+ *
390
+ * - `host` / `port` / `options`: from the connect opts.
391
+ * - `hostaddr`: the resolved peer IP — `opts.hostaddr` if the caller
392
+ * fixed one, else the TCP socket's `remoteAddress` (undefined for a
393
+ * Unix-domain socket → `null`).
394
+ * - `backendPid`: the backend process id from BackendKeyData.
395
+ * - `passwordUsed`: whether a password was actually sent during auth.
396
+ * - `gssapiUsed`: always `false` — we have no GSSAPI support.
397
+ */
398
+ getConnectionInfo() {
399
+ const remote = this.opts.hostaddr !== void 0 && this.opts.hostaddr !== "" ? this.opts.hostaddr : this.socket.remoteAddress ?? null;
400
+ return {
401
+ host: this.opts.host,
402
+ hostaddr: remote,
403
+ port: this.opts.port,
404
+ options: this.opts.options ?? null,
405
+ backendPid: this.processId,
406
+ passwordUsed: this.passwordUsed,
407
+ gssapiUsed: false
408
+ };
409
+ }
410
+ async query(sql, params) {
411
+ if (params === void 0) {
412
+ const sets = await this.execSimple(sql);
413
+ if (sets.length === 0) throw new Error("PgConnection.query: server returned no result sets");
414
+ return sets[sets.length - 1];
415
+ }
416
+ this.ensureIdle();
417
+ const encoded = encodeParams(params);
418
+ this.startExtendedBatch();
419
+ const parseP = this.enqueueParse();
420
+ const bindP = this.enqueueBind();
421
+ const descP = this.enqueueDescribePortalIntoNextExecute();
422
+ const execP = this.enqueueExecute();
423
+ const syncP = this.enqueueSync();
424
+ this.socket.write(Parse("", sql, []));
425
+ this.socket.write(Bind("", "", [], encoded, [0]));
426
+ this.socket.write(Describe("P", ""));
427
+ this.socket.write(Execute("", 0));
428
+ this.socket.write(Sync());
429
+ let firstErr = null;
430
+ const cap = (e) => {
431
+ if (firstErr === null) firstErr = e;
432
+ };
433
+ parseP.catch(cap);
434
+ bindP.catch(cap);
435
+ descP.catch(cap);
436
+ let result = null;
437
+ execP.then((rs) => {
438
+ result = rs;
439
+ }, cap);
440
+ await syncP.catch(cap);
441
+ if (firstErr !== null) throw asThrowable(firstErr);
442
+ if (result === null) throw new Error("PgConnection.query: server returned no result");
443
+ return result;
444
+ }
445
+ async execSimple(sql) {
446
+ this.ensureIdle();
447
+ return new Promise((resolve, reject) => {
448
+ this.pendingQuery = {
449
+ resolve,
450
+ reject,
451
+ current: null,
452
+ finished: [],
453
+ notices: [],
454
+ error: null
455
+ };
456
+ this.state = "in-query";
457
+ this.socket.write(Query(sql));
458
+ });
459
+ }
460
+ async prepare(name, sql, paramTypes) {
461
+ this.ensureIdle();
462
+ const oids = paramTypes ?? [];
463
+ this.startExtendedBatch();
464
+ const parseP = this.enqueueParse();
465
+ const descP = this.enqueueDescribeStatement();
466
+ const syncP = this.enqueueSync();
467
+ this.socket.write(Parse(name, sql, oids));
468
+ this.socket.write(Describe("S", name));
469
+ this.socket.write(Sync());
470
+ let firstErr = null;
471
+ const cap = (e) => {
472
+ if (firstErr === null) firstErr = e;
473
+ };
474
+ parseP.catch(cap);
475
+ let descResult = null;
476
+ descP.then((r) => {
477
+ descResult = r;
478
+ }, cap);
479
+ await syncP.catch(cap);
480
+ if (firstErr !== null) throw asThrowable(firstErr);
481
+ if (descResult === null) throw new Error("PgConnection.prepare: server returned no parameter description");
482
+ const { paramOids, fields } = descResult;
483
+ const conn = this;
484
+ return {
485
+ name,
486
+ paramTypes: paramOids,
487
+ async bind(values, paramFormats) {
488
+ conn.ensureIdle();
489
+ const encoded = encodeParams(values);
490
+ conn.startExtendedBatch();
491
+ const bP = conn.enqueueBind();
492
+ const sP = conn.enqueueSync();
493
+ conn.socket.write(Bind("", name, paramFormats ?? [], encoded, [0]));
494
+ conn.socket.write(Sync());
495
+ let err = null;
496
+ bP.catch((e) => {
497
+ if (err === null) err = e;
498
+ });
499
+ await sP.catch((e) => {
500
+ if (err === null) err = e;
501
+ });
502
+ if (err !== null) throw asThrowable(err);
503
+ },
504
+ describe() {
505
+ return Promise.resolve(fields);
506
+ },
507
+ async execute(maxRows) {
508
+ conn.ensureIdle();
509
+ conn.startExtendedBatch();
510
+ const eP = conn.enqueueExecuteWithFields(fields);
511
+ const sP = conn.enqueueSync();
512
+ conn.socket.write(Execute("", maxRows ?? 0));
513
+ conn.socket.write(Sync());
514
+ let err = null;
515
+ let rs = null;
516
+ eP.then((r) => {
517
+ rs = r;
518
+ }, (e) => {
519
+ if (err === null) err = e;
520
+ });
521
+ await sP.catch((e) => {
522
+ if (err === null) err = e;
523
+ });
524
+ if (err !== null) throw asThrowable(err);
525
+ if (rs === null) throw new Error("PgConnection.prepare.execute: server returned no result");
526
+ return rs;
527
+ },
528
+ async bindAndExecute(values, maxRows, paramFormats) {
529
+ conn.ensureIdle();
530
+ const encoded = encodeParams(values);
531
+ conn.startExtendedBatch();
532
+ const bP = conn.enqueueBind();
533
+ const eP = conn.enqueueExecuteWithFields(fields);
534
+ const sP = conn.enqueueSync();
535
+ conn.socket.write(Bind("", name, paramFormats ?? [], encoded, [0]));
536
+ conn.socket.write(Execute("", maxRows ?? 0));
537
+ conn.socket.write(Sync());
538
+ let err = null;
539
+ let rs = null;
540
+ bP.catch((e) => {
541
+ if (err === null) err = e;
542
+ });
543
+ eP.then((r) => {
544
+ rs = r;
545
+ }, (e) => {
546
+ if (err === null) err = e;
547
+ });
548
+ await sP.catch((e) => {
549
+ if (err === null) err = e;
550
+ });
551
+ if (err !== null) throw asThrowable(err);
552
+ if (rs === null) throw new Error("PgConnection.prepare.bindAndExecute: server returned no result");
553
+ return rs;
554
+ },
555
+ async close() {
556
+ conn.ensureIdle();
557
+ conn.startExtendedBatch();
558
+ const cP = conn.enqueueClose();
559
+ const sP = conn.enqueueSync();
560
+ conn.socket.write(Close("S", name));
561
+ conn.socket.write(Sync());
562
+ let err = null;
563
+ cP.catch((e) => {
564
+ if (err === null) err = e;
565
+ });
566
+ await sP.catch((e) => {
567
+ if (err === null) err = e;
568
+ });
569
+ if (err !== null) throw asThrowable(err);
570
+ }
571
+ };
572
+ }
573
+ /**
574
+ * Issue `Close('S', name) + Sync` directly, without a preceding Parse.
575
+ * The server responds with CloseComplete + ReadyForQuery (even when
576
+ * the named statement doesn't exist — PG treats unknown-name Close as
577
+ * a no-op). Used by `\close_prepared NAME` so we don't have to fake a
578
+ * Parse just to reach the Close step.
579
+ */
580
+ async closePreparedStatement(name) {
581
+ this.ensureIdle();
582
+ this.startExtendedBatch();
583
+ const cP = this.enqueueClose();
584
+ const sP = this.enqueueSync();
585
+ this.socket.write(Close("S", name));
586
+ this.socket.write(Sync());
587
+ let err = null;
588
+ cP.catch((e) => {
589
+ if (err === null) err = e;
590
+ });
591
+ await sP.catch((e) => {
592
+ if (err === null) err = e;
593
+ });
594
+ if (err !== null) throw asThrowable(err);
595
+ }
596
+ startCopyIn(sql) {
597
+ this.ensureIdle();
598
+ if (this._extPipelineActive) {
599
+ this.abortForCopyInPipeline();
600
+ return Promise.reject(Object.assign(/* @__PURE__ */ new Error("COPY in a pipeline is not supported, aborting connection"), { severity: "FATAL" }));
601
+ }
602
+ return new Promise((resolve, reject) => {
603
+ this.copyIn = {
604
+ resolveDone: null,
605
+ rejectDone: null,
606
+ error: null,
607
+ commandTag: null,
608
+ closed: false
609
+ };
610
+ this.copyStartResolve = () => {
611
+ resolve(this.makeCopyInStream());
612
+ };
613
+ this.copyStartReject = reject;
614
+ this.state = "in-query";
615
+ this.socket.write(Query(sql));
616
+ });
617
+ }
618
+ startCopyOut(sql) {
619
+ this.ensureIdle();
620
+ if (this._extPipelineActive) {
621
+ this.abortForCopyInPipeline();
622
+ return Promise.reject(Object.assign(/* @__PURE__ */ new Error("COPY in a pipeline is not supported, aborting connection"), { severity: "FATAL" }));
623
+ }
624
+ return new Promise((resolve, reject) => {
625
+ this.copyOut = {
626
+ queue: [],
627
+ queuedBytes: 0,
628
+ waker: null,
629
+ done: false,
630
+ abandoned: false,
631
+ error: null,
632
+ commandTag: null
633
+ };
634
+ this.copyStartResolve = () => {
635
+ resolve(this.makeCopyOutStream());
636
+ };
637
+ this.copyStartReject = reject;
638
+ this.state = "in-query";
639
+ this.socket.write(Query(sql));
640
+ });
641
+ }
642
+ pipeline() {
643
+ this.ensureIdle();
644
+ return new PipelineSession(this);
645
+ }
646
+ /**
647
+ * Cancel whatever the connection is currently doing.
648
+ *
649
+ * The routing is state-aware so the mainloop SIGINT handler can call this
650
+ * blindly without knowing the protocol phase:
651
+ *
652
+ * - `in-copy-in`: we hold the writing end of the data stream, so the
653
+ * correct action is a client-initiated CopyFail on the *same* socket.
654
+ * Sending a side CancelRequest would race with our own pending writes;
655
+ * CopyFail is the spec-blessed abort path. The server replies with
656
+ * ErrorResponse + ReadyForQuery and we transition back to idle.
657
+ * - `in-copy-out`: the server is pushing data at us. CopyFail is not a
658
+ * valid client message here, so we fall back to the side CancelRequest
659
+ * path that normal queries use. PG will surface an ErrorResponse and
660
+ * tear the COPY down.
661
+ * - everything else: side CancelRequest, the historical behaviour.
662
+ *
663
+ * Best-effort. We don't reject if BackendKeyData hasn't arrived yet
664
+ * during the auth dance — there's nothing to cancel; we just return.
665
+ */
666
+ async cancel() {
667
+ if (this.state === "in-copy-in" && this.copyIn && !this.copyIn.closed) {
668
+ this.copyIn.closed = true;
669
+ try {
670
+ this.socket.write(CopyFail("canceled by user"));
671
+ } catch {}
672
+ return;
673
+ }
674
+ if (this.processId === 0) return;
675
+ const cancelSocket = await openSocket(this.opts, this.opts.hostaddr !== void 0 && this.opts.hostaddr !== "" ? this.opts.hostaddr : void 0);
676
+ let writeSocket = cancelSocket;
677
+ try {
678
+ const cancelServername = tlsServername(this.opts);
679
+ const cancelTlsOpts = {
680
+ ...cancelServername !== void 0 ? { servername: cancelServername } : {},
681
+ rejectUnauthorized: this.opts.ssl === "verify-ca" || this.opts.ssl === "verify-full",
682
+ ALPNProtocols: ["postgresql"]
683
+ };
684
+ if (this.opts.ssl !== "verify-full") cancelTlsOpts.checkServerIdentity = () => void 0;
685
+ else if (this.opts.sslsni === false) {
686
+ const host = this.opts.host;
687
+ cancelTlsOpts.checkServerIdentity = (_h, cert) => tls.checkServerIdentity(host, cert);
688
+ }
689
+ applyTlsProtocolVersionRange(cancelTlsOpts, this.opts);
690
+ const t = await negotiateTls(cancelSocket, isUnixSocketHost(this.opts.host) ? "disable" : this.opts.ssl, cancelTlsOpts, {
691
+ sslcert: this.opts.sslcert,
692
+ sslkey: this.opts.sslkey,
693
+ sslcertmode: this.opts.sslcertmode,
694
+ sslpassword: this.opts.sslpassword,
695
+ sslrootcert: this.opts.sslrootcert,
696
+ sslcrl: this.opts.sslcrl,
697
+ sslcrldir: this.opts.sslcrldir,
698
+ sslkeylogfile: this.opts.sslkeylogfile
699
+ }, isUnixSocketHost(this.opts.host) ? "postgres" : this.opts.sslnegotiation ?? "postgres");
700
+ writeSocket = t.kind === "tls" ? t.socket : t.socket;
701
+ await new Promise((resolve, reject) => {
702
+ writeSocket.write(CancelRequest(this.processId, this.secretKey), (err) => {
703
+ if (err) reject(err);
704
+ else resolve();
705
+ });
706
+ });
707
+ } finally {
708
+ try {
709
+ writeSocket.end();
710
+ } catch {}
711
+ try {
712
+ cancelSocket.destroy();
713
+ } catch {}
714
+ }
715
+ }
716
+ escapeIdentifier(value) {
717
+ return "\"" + value.replace(/"/g, "\"\"") + "\"";
718
+ }
719
+ escapeLiteral(value) {
720
+ const doubled = value.replace(/'/g, "''");
721
+ if (value.includes("\\")) return "E'" + doubled.replace(/\\/g, "\\\\") + "'";
722
+ return "'" + doubled + "'";
723
+ }
724
+ async setClientEncoding(name) {
725
+ await this.execSimple(`SET client_encoding TO ${this.escapeLiteral(name)}`);
726
+ }
727
+ onNotice(handler) {
728
+ return this.notify.onNotice(handler);
729
+ }
730
+ onNotification(handler) {
731
+ return this.notify.onNotification(handler);
732
+ }
733
+ async close() {
734
+ if (this.state === "closed") return;
735
+ try {
736
+ this.socket.write(Terminate());
737
+ } catch {}
738
+ this.state = "closed";
739
+ this.notify.clear();
740
+ await new Promise((resolve) => {
741
+ this.socket.once("close", () => {
742
+ resolve();
743
+ });
744
+ try {
745
+ this.socket.end();
746
+ } catch {
747
+ resolve();
748
+ }
749
+ });
750
+ }
751
+ isClosed() {
752
+ return this.state === "closed";
753
+ }
754
+ startup() {
755
+ return new Promise((resolve, reject) => {
756
+ const params = {
757
+ user: this.opts.user,
758
+ database: this.opts.database,
759
+ client_encoding: this.opts.clientEncoding ?? "UTF8"
760
+ };
761
+ if (this.opts.applicationName !== void 0) params.application_name = this.opts.applicationName;
762
+ if (this.opts.options !== void 0) params.options = this.opts.options;
763
+ if (this.opts.replication !== void 0) params.replication = this.opts.replication;
764
+ this.startupResolve = resolve;
765
+ this.startupReject = reject;
766
+ this.socket.write(StartupMessage(params));
767
+ });
768
+ }
769
+ /**
770
+ * Enforce `require_auth` against an observed server-requested method.
771
+ * Returns true when the connection should proceed; false (with
772
+ * {@link failStartup} already called) when the policy was violated.
773
+ */
774
+ checkRequireAuth(observed) {
775
+ const policy = this.opts.requireAuth;
776
+ if (policy === void 0) return true;
777
+ const hit = policy.methods.has(observed);
778
+ if (!(policy.negated ? !hit : hit)) {
779
+ this.failStartup(/* @__PURE__ */ new Error(`auth method "${observed}" requirement failed`));
780
+ return false;
781
+ }
782
+ return true;
783
+ }
784
+ handleAuthMessage(msg) {
785
+ switch (msg.type) {
786
+ case "AuthenticationOk":
787
+ if (this.opts.channelBinding === "require" && (this.scram === null || this.scram.mechanism !== "SCRAM-SHA-256-PLUS")) {
788
+ this.failStartup(/* @__PURE__ */ new Error("channel binding required, but server authenticated client without channel binding"));
789
+ return;
790
+ }
791
+ if (this.scram !== null && !this.saslFinalSeen) {
792
+ this.failStartup(/* @__PURE__ */ new Error("server sent AuthenticationOk without completing SCRAM authentication (server signature not verified)"));
793
+ return;
794
+ }
795
+ if (!this.authChallengeSeen && !this.checkRequireAuth("none")) return;
796
+ this.state = "await-ready";
797
+ return;
798
+ case "AuthenticationCleartextPassword":
799
+ this.authChallengeSeen = true;
800
+ if (this.opts.channelBinding === "require") {
801
+ this.failStartup(/* @__PURE__ */ new Error("channel binding required but not supported by server's authentication request"));
802
+ return;
803
+ }
804
+ if (!this.checkRequireAuth("password")) return;
805
+ if (this.opts.password === void 0) {
806
+ this.failStartup(/* @__PURE__ */ new Error("Server requested cleartext password but no password was provided"));
807
+ return;
808
+ }
809
+ this.passwordUsed = true;
810
+ this.socket.write(PasswordMessage(this.opts.password));
811
+ return;
812
+ case "AuthenticationMD5Password": {
813
+ this.authChallengeSeen = true;
814
+ if (this.opts.channelBinding === "require") {
815
+ this.failStartup(/* @__PURE__ */ new Error("channel binding required but not supported by server's authentication request"));
816
+ return;
817
+ }
818
+ if (!this.checkRequireAuth("md5")) return;
819
+ if (this.opts.password === void 0) {
820
+ this.failStartup(/* @__PURE__ */ new Error("Server requested MD5 password but no password was provided"));
821
+ return;
822
+ }
823
+ const payload = md5AuthPayload(this.opts.user, this.opts.password, msg.salt);
824
+ this.passwordUsed = true;
825
+ this.socket.write(PasswordMessage(payload));
826
+ return;
827
+ }
828
+ case "AuthenticationSASL":
829
+ this.authChallengeSeen = true;
830
+ if (this.opts.password === void 0) {
831
+ this.failStartup(/* @__PURE__ */ new Error("Server requested SASL auth but no password was provided"));
832
+ return;
833
+ }
834
+ if (!this.checkRequireAuth("scram-sha-256")) return;
835
+ if (this.opts.channelBinding === "require" && !msg.mechanisms.includes("SCRAM-SHA-256-PLUS")) {
836
+ this.failStartup(/* @__PURE__ */ new Error("channel binding required but not supported by server's authentication request"));
837
+ return;
838
+ }
839
+ if (this.opts.channelBinding === "require" && this.channelBindingData === null) {
840
+ this.failStartup(/* @__PURE__ */ new Error("channel binding required but not supported by server's authentication request"));
841
+ return;
842
+ }
843
+ try {
844
+ this.scram = createScramClient({
845
+ user: this.opts.user,
846
+ password: this.opts.password,
847
+ mechanisms: msg.mechanisms,
848
+ channelBinding: this.channelBindingData !== null && this.opts.channelBinding !== "disable" ? {
849
+ type: "tls-server-end-point",
850
+ data: this.channelBindingData
851
+ } : void 0
852
+ });
853
+ const { mechanism, clientFirstMessage } = this.scram.start();
854
+ this.passwordUsed = true;
855
+ this.socket.write(SASLInitialResponse(mechanism, clientFirstMessage));
856
+ } catch (err) {
857
+ this.failStartup(err);
858
+ }
859
+ return;
860
+ case "AuthenticationSASLContinue":
861
+ if (!this.scram) {
862
+ this.failStartup(/* @__PURE__ */ new Error("Received AuthenticationSASLContinue without an active SCRAM client"));
863
+ return;
864
+ }
865
+ try {
866
+ const reply = this.scram.continue(msg.data);
867
+ this.socket.write(SASLResponse(reply));
868
+ } catch (err) {
869
+ this.failStartup(err);
870
+ }
871
+ return;
872
+ case "AuthenticationSASLFinal":
873
+ if (!this.scram) {
874
+ this.failStartup(/* @__PURE__ */ new Error("Received AuthenticationSASLFinal without an active SCRAM client"));
875
+ return;
876
+ }
877
+ try {
878
+ this.scram.finish(msg.data);
879
+ this.saslFinalSeen = true;
880
+ } catch (err) {
881
+ this.failStartup(err);
882
+ }
883
+ return;
884
+ case "ErrorResponse":
885
+ this.failStartup(fieldsToConnectError(msg.fields));
886
+ return;
887
+ case "NoticeResponse":
888
+ this.notify.emit(fieldsToNotice(msg.fields));
889
+ return;
890
+ default:
891
+ this.handleAwaitReady(msg);
892
+ return;
893
+ }
894
+ }
895
+ handleAwaitReady(msg) {
896
+ switch (msg.type) {
897
+ case "ParameterStatus":
898
+ this.params.set(msg.name, msg.value);
899
+ if (msg.name === "server_version") this.serverVersion = parseServerVersion(msg.value);
900
+ return;
901
+ case "BackendKeyData":
902
+ this.processId = msg.processId;
903
+ this.secretKey = msg.secretKey;
904
+ return;
905
+ case "ReadyForQuery":
906
+ this.txStatus = msg.status;
907
+ this.state = "idle";
908
+ if (this.startupResolve) {
909
+ const r = this.startupResolve;
910
+ this.startupResolve = null;
911
+ this.startupReject = null;
912
+ r();
913
+ }
914
+ return;
915
+ case "ErrorResponse":
916
+ this.failStartup(fieldsToConnectError(msg.fields));
917
+ return;
918
+ case "NoticeResponse":
919
+ this.notify.emit(fieldsToNotice(msg.fields));
920
+ return;
921
+ default:
922
+ this.failStartup(/* @__PURE__ */ new Error(`Unexpected message ${msg.type} during connection startup`));
923
+ return;
924
+ }
925
+ }
926
+ failStartup(err) {
927
+ if (this.startupReject) {
928
+ const r = this.startupReject;
929
+ this.startupResolve = null;
930
+ this.startupReject = null;
931
+ r(err);
932
+ }
933
+ try {
934
+ this.socket.destroy();
935
+ } catch {}
936
+ this.state = "closed";
937
+ }
938
+ makeCopyInStream() {
939
+ const driver = this.copyIn;
940
+ if (!driver) throw new Error("PgConnection: makeCopyInStream called without driver");
941
+ return {
942
+ write: (chunk) => {
943
+ if (this.state === "closed") return Promise.reject(/* @__PURE__ */ new Error("Connection closed"));
944
+ if (driver.closed) return Promise.reject(/* @__PURE__ */ new Error("CopyInStream already closed"));
945
+ const data = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
946
+ return new Promise((resolve, reject) => {
947
+ this.socket.write(CopyData(data), (err) => {
948
+ if (err) reject(err);
949
+ else resolve();
950
+ });
951
+ });
952
+ },
953
+ end: () => {
954
+ if (driver.closed) return Promise.reject(/* @__PURE__ */ new Error("CopyInStream already closed"));
955
+ driver.closed = true;
956
+ return new Promise((resolve, reject) => {
957
+ driver.resolveDone = resolve;
958
+ driver.rejectDone = reject;
959
+ this.socket.write(CopyDone());
960
+ });
961
+ },
962
+ fail: (reason) => {
963
+ if (driver.closed) return Promise.reject(/* @__PURE__ */ new Error("CopyInStream already closed"));
964
+ driver.closed = true;
965
+ return new Promise((resolve, reject) => {
966
+ driver.resolveDone = () => {
967
+ resolve();
968
+ };
969
+ driver.rejectDone = reject;
970
+ this.socket.write(CopyFail(reason));
971
+ });
972
+ }
973
+ };
974
+ }
975
+ makeCopyOutStream() {
976
+ const driver = this.copyOut;
977
+ if (!driver) throw new Error("PgConnection: makeCopyOutStream called without driver");
978
+ const isClosed = () => this.state === "closed";
979
+ const resumeSocket = () => {
980
+ this.socket.resume?.();
981
+ };
982
+ return { [Symbol.asyncIterator]() {
983
+ return {
984
+ async next() {
985
+ for (;;) {
986
+ if (driver.queue.length > 0) {
987
+ const next = driver.queue.shift();
988
+ if (next === void 0) continue;
989
+ driver.queuedBytes -= next.length;
990
+ if (driver.queuedBytes <= COPY_OUT_LWM) resumeSocket();
991
+ return {
992
+ value: next,
993
+ done: false
994
+ };
995
+ }
996
+ if (driver.error) {
997
+ const ce = driver.error;
998
+ const wrapped = new Error(ce.message);
999
+ wrapped.cause = ce;
1000
+ throw wrapped;
1001
+ }
1002
+ if (driver.done) return {
1003
+ value: void 0,
1004
+ done: true
1005
+ };
1006
+ if (isClosed()) throw new Error("Connection closed mid-COPY-OUT");
1007
+ await new Promise((resolve) => {
1008
+ driver.waker = resolve;
1009
+ });
1010
+ }
1011
+ },
1012
+ return() {
1013
+ driver.abandoned = true;
1014
+ driver.queue.length = 0;
1015
+ driver.queuedBytes = 0;
1016
+ resumeSocket();
1017
+ return Promise.resolve({
1018
+ value: void 0,
1019
+ done: true
1020
+ });
1021
+ }
1022
+ };
1023
+ } };
1024
+ }
1025
+ /**
1026
+ * Handle messages arriving in `in-query` state when there is no
1027
+ * `pendingQuery` — i.e. the caller invoked `startCopyIn` / `startCopyOut`
1028
+ * and is waiting for the server to switch into copy mode.
1029
+ */
1030
+ handleCopyStartMessage(msg) {
1031
+ switch (msg.type) {
1032
+ case "CopyInResponse":
1033
+ if (this._extPipelineActive) {
1034
+ this.abortForCopyInPipeline();
1035
+ return;
1036
+ }
1037
+ if (this.copyIn) {
1038
+ this.state = "in-copy-in";
1039
+ const r = this.copyStartResolve;
1040
+ this.copyStartResolve = null;
1041
+ this.copyStartReject = null;
1042
+ if (r) r();
1043
+ }
1044
+ return;
1045
+ case "CopyOutResponse":
1046
+ if (this._extPipelineActive) {
1047
+ this.abortForCopyInPipeline();
1048
+ return;
1049
+ }
1050
+ if (this.copyOut) {
1051
+ this.state = "in-copy-out";
1052
+ const r = this.copyStartResolve;
1053
+ this.copyStartResolve = null;
1054
+ this.copyStartReject = null;
1055
+ if (r) r();
1056
+ }
1057
+ return;
1058
+ case "ErrorResponse": {
1059
+ const err = fieldsToConnectError(msg.fields);
1060
+ if (this.copyIn) {
1061
+ if (this.copyStartReject) this.copyStartReject(err);
1062
+ this.copyStartResolve = null;
1063
+ this.copyStartReject = null;
1064
+ this.copyIn = null;
1065
+ }
1066
+ if (this.copyOut) {
1067
+ if (this.copyStartReject) this.copyStartReject(err);
1068
+ this.copyStartResolve = null;
1069
+ this.copyStartReject = null;
1070
+ this.copyOut = null;
1071
+ }
1072
+ return;
1073
+ }
1074
+ case "ReadyForQuery":
1075
+ this.txStatus = msg.status;
1076
+ this.state = "idle";
1077
+ return;
1078
+ case "NoticeResponse":
1079
+ this.notify.emit(fieldsToNotice(msg.fields));
1080
+ return;
1081
+ case "ParameterStatus":
1082
+ this.params.set(msg.name, msg.value);
1083
+ return;
1084
+ default:
1085
+ if (this.copyStartReject) this.copyStartReject(/* @__PURE__ */ new Error(`Unexpected backend message before COPY response: ${msg.type}`));
1086
+ this.copyStartResolve = null;
1087
+ this.copyStartReject = null;
1088
+ return;
1089
+ }
1090
+ }
1091
+ handleCopyInMessage(msg) {
1092
+ const driver = this.copyIn;
1093
+ if (!driver) return;
1094
+ switch (msg.type) {
1095
+ case "CommandComplete":
1096
+ driver.commandTag = msg.tag;
1097
+ this.lastCopyTag = msg.tag;
1098
+ return;
1099
+ case "ErrorResponse":
1100
+ driver.error = fieldsToConnectError(msg.fields);
1101
+ return;
1102
+ case "NoticeResponse":
1103
+ this.notify.emit(fieldsToNotice(msg.fields));
1104
+ return;
1105
+ case "ParameterStatus":
1106
+ this.params.set(msg.name, msg.value);
1107
+ return;
1108
+ case "ReadyForQuery":
1109
+ this.txStatus = msg.status;
1110
+ this.state = "idle";
1111
+ this.copyIn = null;
1112
+ if (driver.error) {
1113
+ if (driver.rejectDone) driver.rejectDone(driver.error);
1114
+ } else if (driver.resolveDone) driver.resolveDone();
1115
+ return;
1116
+ default:
1117
+ driver.error = {
1118
+ severity: "ERROR",
1119
+ message: `Unexpected backend message during COPY IN: ${msg.type}`
1120
+ };
1121
+ return;
1122
+ }
1123
+ }
1124
+ handleCopyOutMessage(msg) {
1125
+ const driver = this.copyOut;
1126
+ if (!driver) return;
1127
+ switch (msg.type) {
1128
+ case "CopyData":
1129
+ if (driver.abandoned) return;
1130
+ driver.queue.push(msg.data);
1131
+ driver.queuedBytes += msg.data.length;
1132
+ if (driver.queuedBytes >= COPY_OUT_HWM) this.socket.pause?.();
1133
+ if (driver.waker) {
1134
+ const w = driver.waker;
1135
+ driver.waker = null;
1136
+ w();
1137
+ }
1138
+ return;
1139
+ case "CopyDone": return;
1140
+ case "CommandComplete":
1141
+ driver.commandTag = msg.tag;
1142
+ this.lastCopyTag = msg.tag;
1143
+ return;
1144
+ case "ErrorResponse":
1145
+ driver.error = fieldsToConnectError(msg.fields);
1146
+ return;
1147
+ case "NoticeResponse":
1148
+ this.notify.emit(fieldsToNotice(msg.fields));
1149
+ return;
1150
+ case "ParameterStatus":
1151
+ this.params.set(msg.name, msg.value);
1152
+ return;
1153
+ case "ReadyForQuery":
1154
+ this.txStatus = msg.status;
1155
+ this.state = "idle";
1156
+ driver.done = true;
1157
+ this.copyOut = null;
1158
+ if (driver.waker) {
1159
+ const w = driver.waker;
1160
+ driver.waker = null;
1161
+ w();
1162
+ }
1163
+ return;
1164
+ default:
1165
+ driver.error = {
1166
+ severity: "ERROR",
1167
+ message: `Unexpected backend message during COPY OUT: ${msg.type}`
1168
+ };
1169
+ if (driver.waker) {
1170
+ const w = driver.waker;
1171
+ driver.waker = null;
1172
+ w();
1173
+ }
1174
+ return;
1175
+ }
1176
+ }
1177
+ handleQueryMessage(msg) {
1178
+ const q = this.pendingQuery;
1179
+ if (!q) {
1180
+ this.handleCopyStartMessage(msg);
1181
+ return;
1182
+ }
1183
+ switch (msg.type) {
1184
+ case "RowDescription":
1185
+ q.current = {
1186
+ command: "",
1187
+ rowCount: null,
1188
+ oid: null,
1189
+ fields: msg.fields,
1190
+ rows: [],
1191
+ notices: []
1192
+ };
1193
+ return;
1194
+ case "DataRow":
1195
+ if (!q.current) q.current = {
1196
+ command: "",
1197
+ rowCount: null,
1198
+ oid: null,
1199
+ fields: [],
1200
+ rows: [],
1201
+ notices: []
1202
+ };
1203
+ q.current.rows.push(decodeDataRow(msg.values, q.current.fields));
1204
+ return;
1205
+ case "CommandComplete": {
1206
+ const { command, rowCount, oid } = parseCommandTag(msg.tag);
1207
+ const set = q.current ?? {
1208
+ command,
1209
+ rowCount,
1210
+ oid,
1211
+ fields: [],
1212
+ rows: [],
1213
+ notices: []
1214
+ };
1215
+ set.command = command;
1216
+ set.rowCount = rowCount;
1217
+ set.oid = oid;
1218
+ set.notices = q.notices.splice(0);
1219
+ q.finished.push(set);
1220
+ q.current = null;
1221
+ return;
1222
+ }
1223
+ case "EmptyQueryResponse": {
1224
+ const set = {
1225
+ command: "",
1226
+ rowCount: null,
1227
+ oid: null,
1228
+ fields: [],
1229
+ rows: [],
1230
+ notices: q.notices.splice(0)
1231
+ };
1232
+ q.finished.push(set);
1233
+ q.current = null;
1234
+ return;
1235
+ }
1236
+ case "ParameterStatus":
1237
+ this.params.set(msg.name, msg.value);
1238
+ if (msg.name === "server_version") this.serverVersion = parseServerVersion(msg.value);
1239
+ return;
1240
+ case "NoticeResponse": {
1241
+ const notice = fieldsToNotice(msg.fields);
1242
+ q.notices.push(notice);
1243
+ this.notify.emit(notice);
1244
+ return;
1245
+ }
1246
+ case "NotificationResponse":
1247
+ this.notify.emitNotification(msg.channel, msg.payload, msg.processId);
1248
+ return;
1249
+ case "ErrorResponse":
1250
+ q.error = fieldsToConnectError(msg.fields);
1251
+ return;
1252
+ case "ReadyForQuery":
1253
+ this.txStatus = msg.status;
1254
+ this.state = "idle";
1255
+ this.pendingQuery = null;
1256
+ if (q.error) {
1257
+ const err = asThrowable(q.error);
1258
+ err.partialResults = q.finished;
1259
+ q.reject(err);
1260
+ } else q.resolve(q.finished);
1261
+ return;
1262
+ case "CopyInResponse": {
1263
+ if (this._extPipelineActive) {
1264
+ this.abortForCopyInPipeline();
1265
+ return;
1266
+ }
1267
+ const data = this.copyInMidBatchQueue.shift();
1268
+ if (data !== void 0) {
1269
+ try {
1270
+ if (data.length > 0) this.socket.write(CopyData(data));
1271
+ this.socket.write(CopyDone());
1272
+ } catch {}
1273
+ return;
1274
+ }
1275
+ q.error = {
1276
+ severity: "ERROR",
1277
+ message: "COPY FROM STDIN not supported via execSimple — use \\copy or startCopyIn"
1278
+ };
1279
+ try {
1280
+ this.socket.write(CopyFail("COPY FROM STDIN not driven by client"));
1281
+ } catch {}
1282
+ return;
1283
+ }
1284
+ case "CopyOutResponse":
1285
+ if (this._extPipelineActive) {
1286
+ this.abortForCopyInPipeline();
1287
+ return;
1288
+ }
1289
+ this.copyOutMidBatchActive = true;
1290
+ q.current = q.current ?? {
1291
+ command: "",
1292
+ rowCount: null,
1293
+ oid: null,
1294
+ fields: [],
1295
+ rows: [],
1296
+ notices: []
1297
+ };
1298
+ q.current.copyOutBytes = q.current.copyOutBytes ?? [];
1299
+ return;
1300
+ case "CopyData":
1301
+ if (this.copyOutMidBatchActive) {
1302
+ const cur = q.current;
1303
+ if (cur) {
1304
+ cur.copyOutBytes = cur.copyOutBytes ?? [];
1305
+ cur.copyOutBytes.push(msg.data);
1306
+ }
1307
+ return;
1308
+ }
1309
+ q.error = {
1310
+ severity: "ERROR",
1311
+ message: "Unexpected backend message during query: CopyData"
1312
+ };
1313
+ return;
1314
+ case "CopyDone":
1315
+ if (this.copyOutMidBatchActive) {
1316
+ this.copyOutMidBatchActive = false;
1317
+ return;
1318
+ }
1319
+ q.error = {
1320
+ severity: "ERROR",
1321
+ message: "Unexpected backend message during query: CopyDone"
1322
+ };
1323
+ return;
1324
+ case "CopyBothResponse": {
1325
+ const cbErr = {
1326
+ severity: "ERROR",
1327
+ code: "0A000",
1328
+ message: "syntax error: unexpected CopyBothResponse from server (replication streaming is not supported by this client)"
1329
+ };
1330
+ q.error = cbErr;
1331
+ q.reject(asThrowable(cbErr));
1332
+ this.pendingQuery = null;
1333
+ this.socketError = new Error(cbErr.message);
1334
+ try {
1335
+ this.socket.destroy();
1336
+ } catch {}
1337
+ this.state = "closed";
1338
+ return;
1339
+ }
1340
+ default:
1341
+ q.error = {
1342
+ severity: "ERROR",
1343
+ message: `Unexpected backend message during query: ${msg.type}`
1344
+ };
1345
+ return;
1346
+ }
1347
+ }
1348
+ onData(chunk) {
1349
+ let messages;
1350
+ try {
1351
+ messages = this.parser.feed(chunk);
1352
+ } catch (err) {
1353
+ this.socketError = err instanceof Error ? err : new Error(String(err));
1354
+ this.failPending(this.socketError);
1355
+ try {
1356
+ this.socket.destroy();
1357
+ } catch {}
1358
+ this.state = "closed";
1359
+ return;
1360
+ }
1361
+ for (const msg of messages) {
1362
+ this.dispatch(msg);
1363
+ if (this.state === "closed") break;
1364
+ }
1365
+ }
1366
+ dispatch(msg) {
1367
+ if (msg.type === "NotificationResponse") {
1368
+ this.notify.emitNotification(msg.channel, msg.payload, msg.processId);
1369
+ return;
1370
+ }
1371
+ switch (this.state) {
1372
+ case "auth":
1373
+ this.handleAuthMessage(msg);
1374
+ return;
1375
+ case "await-ready":
1376
+ this.handleAwaitReady(msg);
1377
+ return;
1378
+ case "idle":
1379
+ if (msg.type === "ParameterStatus") {
1380
+ this.params.set(msg.name, msg.value);
1381
+ if (msg.name === "server_version") this.serverVersion = parseServerVersion(msg.value);
1382
+ return;
1383
+ }
1384
+ if (msg.type === "NoticeResponse") {
1385
+ this.notify.emit(fieldsToNotice(msg.fields));
1386
+ return;
1387
+ }
1388
+ this.socketError = /* @__PURE__ */ new Error(`Unexpected ${msg.type} in idle state`);
1389
+ try {
1390
+ this.socket.destroy();
1391
+ } catch {}
1392
+ this.state = "closed";
1393
+ return;
1394
+ case "in-query":
1395
+ this.handleQueryMessage(msg);
1396
+ return;
1397
+ case "in-extended":
1398
+ this.handleExtendedMessage(msg);
1399
+ return;
1400
+ case "in-copy-in":
1401
+ this.handleCopyInMessage(msg);
1402
+ return;
1403
+ case "in-copy-out":
1404
+ this.handleCopyOutMessage(msg);
1405
+ return;
1406
+ case "closed": return;
1407
+ }
1408
+ }
1409
+ failPending(err) {
1410
+ if (this.pendingQuery) {
1411
+ const q = this.pendingQuery;
1412
+ this.pendingQuery = null;
1413
+ q.reject(q.error != null ? asThrowable(q.error) : err);
1414
+ }
1415
+ if (this.extDriver) {
1416
+ const d = this.extDriver;
1417
+ this.extDriver = null;
1418
+ for (const op of d.queue) op.reject(err);
1419
+ }
1420
+ if (this.startupReject) {
1421
+ const r = this.startupReject;
1422
+ this.startupResolve = null;
1423
+ this.startupReject = null;
1424
+ r(err);
1425
+ }
1426
+ if (this.copyStartReject) {
1427
+ const r = this.copyStartReject;
1428
+ this.copyStartResolve = null;
1429
+ this.copyStartReject = null;
1430
+ r(err);
1431
+ }
1432
+ if (this.copyIn) {
1433
+ const d = this.copyIn;
1434
+ this.copyIn = null;
1435
+ if (d.rejectDone) d.rejectDone(err);
1436
+ }
1437
+ if (this.copyOut) {
1438
+ const d = this.copyOut;
1439
+ this.copyOut = null;
1440
+ d.error = err instanceof Error ? {
1441
+ severity: "ERROR",
1442
+ message: err.message
1443
+ } : {
1444
+ severity: "ERROR",
1445
+ message: String(err)
1446
+ };
1447
+ if (d.waker) {
1448
+ const w = d.waker;
1449
+ d.waker = null;
1450
+ w();
1451
+ }
1452
+ }
1453
+ }
1454
+ ensureIdle() {
1455
+ if (this.state === "closed") throw new Error("PgConnection: connection is closed");
1456
+ if (this.state !== "idle" && this.state !== "in-extended") throw new Error(`PgConnection: cannot start query in state ${this.state}`);
1457
+ }
1458
+ startExtendedBatch() {
1459
+ if (this.state === "idle") {
1460
+ this.state = "in-extended";
1461
+ this.extDriver = {
1462
+ queue: [],
1463
+ error: null
1464
+ };
1465
+ } else if (this.state !== "in-extended") throw new Error(`PgConnection: cannot start extended batch in state ${this.state}`);
1466
+ else if (!this.extDriver) this.extDriver = {
1467
+ queue: [],
1468
+ error: null
1469
+ };
1470
+ }
1471
+ writeRaw(buf) {
1472
+ this.socket.write(buf);
1473
+ }
1474
+ enqueueParse() {
1475
+ return this.enqueueOp({
1476
+ kind: "parse",
1477
+ resolve: () => void 0,
1478
+ reject: () => void 0
1479
+ });
1480
+ }
1481
+ enqueueBind() {
1482
+ return this.enqueueOp({
1483
+ kind: "bind",
1484
+ resolve: () => void 0,
1485
+ reject: () => void 0
1486
+ });
1487
+ }
1488
+ enqueueDescribeStatement() {
1489
+ return this.enqueueOp({
1490
+ kind: "describeS",
1491
+ resolve: () => void 0,
1492
+ reject: () => void 0,
1493
+ paramOids: null
1494
+ });
1495
+ }
1496
+ enqueueDescribePortal() {
1497
+ return this.enqueueOp({
1498
+ kind: "describeP",
1499
+ resolve: () => void 0,
1500
+ reject: () => void 0
1501
+ });
1502
+ }
1503
+ /**
1504
+ * Variant of {@link enqueueDescribePortal} that pipes the resolved fields
1505
+ * onto the very next `execute` op already (or yet to be) on the queue.
1506
+ */
1507
+ enqueueDescribePortalIntoNextExecute() {
1508
+ const driver = this.extDriver;
1509
+ if (!driver) return Promise.reject(/* @__PURE__ */ new Error("enqueueDescribePortalIntoNextExecute: not in extended state"));
1510
+ return new Promise((resolve, reject) => {
1511
+ driver.queue.push({
1512
+ kind: "describeP",
1513
+ resolve: (v) => {
1514
+ const fields = v;
1515
+ for (const op of driver.queue) if (op.kind === "execute" && op.fields === null) {
1516
+ op.fields = fields;
1517
+ break;
1518
+ }
1519
+ resolve();
1520
+ },
1521
+ reject
1522
+ });
1523
+ });
1524
+ }
1525
+ enqueueExecute() {
1526
+ return this.enqueueOp({
1527
+ kind: "execute",
1528
+ resolve: () => void 0,
1529
+ reject: () => void 0,
1530
+ current: null,
1531
+ notices: [],
1532
+ fields: null
1533
+ });
1534
+ }
1535
+ enqueueExecuteWithFields(fields) {
1536
+ return this.enqueueOp({
1537
+ kind: "execute",
1538
+ resolve: () => void 0,
1539
+ reject: () => void 0,
1540
+ current: null,
1541
+ notices: [],
1542
+ fields
1543
+ });
1544
+ }
1545
+ enqueueClose() {
1546
+ return this.enqueueOp({
1547
+ kind: "close",
1548
+ resolve: () => void 0,
1549
+ reject: () => void 0
1550
+ });
1551
+ }
1552
+ enqueueSync() {
1553
+ return this.enqueueOp({
1554
+ kind: "sync",
1555
+ resolve: () => void 0,
1556
+ reject: () => void 0
1557
+ });
1558
+ }
1559
+ enqueueOp(opSkeleton) {
1560
+ if (!this.extDriver) return Promise.reject(/* @__PURE__ */ new Error("enqueueOp: not in extended state"));
1561
+ const driver = this.extDriver;
1562
+ return new Promise((resolve, reject) => {
1563
+ const op = opSkeleton;
1564
+ op.resolve = resolve;
1565
+ op.reject = reject;
1566
+ driver.queue.push(op);
1567
+ });
1568
+ }
1569
+ handleExtendedMessage(msg) {
1570
+ const driver = this.extDriver;
1571
+ if (!driver) return;
1572
+ if (msg.type === "ParameterStatus") {
1573
+ this.params.set(msg.name, msg.value);
1574
+ if (msg.name === "server_version") this.serverVersion = parseServerVersion(msg.value);
1575
+ return;
1576
+ }
1577
+ if (msg.type === "NoticeResponse") {
1578
+ const notice = fieldsToNotice(msg.fields);
1579
+ this.notify.emit(notice);
1580
+ const head = driver.queue[0];
1581
+ if (head && head.kind === "execute") head.notices.push(notice);
1582
+ return;
1583
+ }
1584
+ if (msg.type === "NotificationResponse") {
1585
+ this.notify.emitNotification(msg.channel, msg.payload, msg.processId);
1586
+ return;
1587
+ }
1588
+ if (msg.type === "ErrorResponse") {
1589
+ driver.error = fieldsToConnectError(msg.fields);
1590
+ let first = true;
1591
+ while (driver.queue.length > 0) {
1592
+ const head = driver.queue[0];
1593
+ if (head.kind === "sync") break;
1594
+ driver.queue.shift();
1595
+ if (first) {
1596
+ head.reject(driver.error);
1597
+ first = false;
1598
+ } else head.reject(pipelineAbortedError());
1599
+ }
1600
+ return;
1601
+ }
1602
+ if (msg.type === "CopyInResponse" || msg.type === "CopyOutResponse") {
1603
+ this.abortForCopyInPipeline();
1604
+ return;
1605
+ }
1606
+ while (driver.error !== null) {
1607
+ const head = driver.queue[0];
1608
+ if (!head || head.kind === "sync") break;
1609
+ driver.queue.shift();
1610
+ head.reject(pipelineAbortedError());
1611
+ }
1612
+ const head = driver.queue[0];
1613
+ if (!head) {
1614
+ this.protocolFail(/* @__PURE__ */ new Error(`Unexpected backend message ${msg.type} in in-extended`));
1615
+ return;
1616
+ }
1617
+ switch (msg.type) {
1618
+ case "ParseComplete":
1619
+ if (head.kind !== "parse") {
1620
+ this.protocolFail(/* @__PURE__ */ new Error("ParseComplete arrived but head op is " + head.kind));
1621
+ return;
1622
+ }
1623
+ driver.queue.shift();
1624
+ head.resolve(void 0);
1625
+ return;
1626
+ case "BindComplete":
1627
+ if (head.kind !== "bind") {
1628
+ this.protocolFail(/* @__PURE__ */ new Error("BindComplete arrived but head op is " + head.kind));
1629
+ return;
1630
+ }
1631
+ driver.queue.shift();
1632
+ head.resolve(void 0);
1633
+ return;
1634
+ case "CloseComplete":
1635
+ if (head.kind !== "close") {
1636
+ this.protocolFail(/* @__PURE__ */ new Error("CloseComplete arrived but head op is " + head.kind));
1637
+ return;
1638
+ }
1639
+ driver.queue.shift();
1640
+ head.resolve(void 0);
1641
+ return;
1642
+ case "ParameterDescription":
1643
+ if (head.kind !== "describeS") {
1644
+ this.protocolFail(/* @__PURE__ */ new Error("ParameterDescription arrived but head op is " + head.kind));
1645
+ return;
1646
+ }
1647
+ head.paramOids = msg.oids;
1648
+ return;
1649
+ case "RowDescription":
1650
+ if (head.kind === "describeS") {
1651
+ driver.queue.shift();
1652
+ head.resolve({
1653
+ paramOids: head.paramOids ?? [],
1654
+ fields: msg.fields
1655
+ });
1656
+ return;
1657
+ }
1658
+ if (head.kind === "describeP") {
1659
+ driver.queue.shift();
1660
+ head.resolve(msg.fields);
1661
+ return;
1662
+ }
1663
+ if (head.kind === "execute") {
1664
+ head.current = {
1665
+ command: "",
1666
+ rowCount: null,
1667
+ oid: null,
1668
+ fields: msg.fields,
1669
+ rows: [],
1670
+ notices: []
1671
+ };
1672
+ head.fields = msg.fields;
1673
+ return;
1674
+ }
1675
+ this.protocolFail(/* @__PURE__ */ new Error("Unexpected RowDescription at head op " + head.kind));
1676
+ return;
1677
+ case "NoData":
1678
+ if (head.kind === "describeS") {
1679
+ driver.queue.shift();
1680
+ head.resolve({
1681
+ paramOids: head.paramOids ?? [],
1682
+ fields: []
1683
+ });
1684
+ return;
1685
+ }
1686
+ if (head.kind === "describeP") {
1687
+ driver.queue.shift();
1688
+ head.resolve([]);
1689
+ return;
1690
+ }
1691
+ this.protocolFail(/* @__PURE__ */ new Error("Unexpected NoData at head op " + head.kind));
1692
+ return;
1693
+ case "DataRow": {
1694
+ if (head.kind !== "execute") {
1695
+ this.protocolFail(/* @__PURE__ */ new Error("DataRow at head op " + head.kind));
1696
+ return;
1697
+ }
1698
+ const fields = head.fields ?? head.current?.fields ?? [];
1699
+ if (!head.current) head.current = {
1700
+ command: "",
1701
+ rowCount: null,
1702
+ oid: null,
1703
+ fields,
1704
+ rows: [],
1705
+ notices: []
1706
+ };
1707
+ head.current.rows.push(decodeDataRow(msg.values, fields));
1708
+ return;
1709
+ }
1710
+ case "CommandComplete": {
1711
+ if (head.kind !== "execute") {
1712
+ this.protocolFail(/* @__PURE__ */ new Error("CommandComplete at head op " + head.kind));
1713
+ return;
1714
+ }
1715
+ const { command, rowCount, oid } = parseCommandTag(msg.tag);
1716
+ const set = head.current ?? {
1717
+ command,
1718
+ rowCount,
1719
+ oid,
1720
+ fields: head.fields ?? [],
1721
+ rows: [],
1722
+ notices: []
1723
+ };
1724
+ set.command = command;
1725
+ set.rowCount = rowCount;
1726
+ set.oid = oid;
1727
+ set.notices = head.notices.splice(0);
1728
+ driver.queue.shift();
1729
+ head.resolve(set);
1730
+ return;
1731
+ }
1732
+ case "EmptyQueryResponse": {
1733
+ if (head.kind !== "execute") {
1734
+ this.protocolFail(/* @__PURE__ */ new Error("EmptyQueryResponse at head op " + head.kind));
1735
+ return;
1736
+ }
1737
+ const set = {
1738
+ command: "",
1739
+ rowCount: null,
1740
+ oid: null,
1741
+ fields: [],
1742
+ rows: [],
1743
+ notices: head.notices.splice(0)
1744
+ };
1745
+ driver.queue.shift();
1746
+ head.resolve(set);
1747
+ return;
1748
+ }
1749
+ case "PortalSuspended": {
1750
+ if (head.kind !== "execute") {
1751
+ this.protocolFail(/* @__PURE__ */ new Error("PortalSuspended at head op " + head.kind));
1752
+ return;
1753
+ }
1754
+ const set = head.current ?? {
1755
+ command: "",
1756
+ rowCount: null,
1757
+ oid: null,
1758
+ fields: head.fields ?? [],
1759
+ rows: [],
1760
+ notices: head.notices.splice(0)
1761
+ };
1762
+ set.notices = head.notices.splice(0);
1763
+ driver.queue.shift();
1764
+ head.resolve(set);
1765
+ return;
1766
+ }
1767
+ case "ReadyForQuery": {
1768
+ this.txStatus = msg.status;
1769
+ if (head.kind !== "sync") {
1770
+ this.protocolFail(/* @__PURE__ */ new Error("ReadyForQuery but head op is " + head.kind));
1771
+ return;
1772
+ }
1773
+ driver.queue.shift();
1774
+ const stickyErr = driver.error;
1775
+ driver.error = null;
1776
+ if (stickyErr) head.reject(stickyErr);
1777
+ else head.resolve(void 0);
1778
+ if (driver.queue.length === 0 && !this._extPipelineActive) {
1779
+ this.state = "idle";
1780
+ this.extDriver = null;
1781
+ }
1782
+ return;
1783
+ }
1784
+ default:
1785
+ this.protocolFail(/* @__PURE__ */ new Error(`Unexpected ${msg.type} in in-extended state`));
1786
+ return;
1787
+ }
1788
+ }
1789
+ protocolFail(err) {
1790
+ this.socketError = err;
1791
+ this.failPending(err);
1792
+ try {
1793
+ this.socket.destroy();
1794
+ } catch {}
1795
+ this.state = "closed";
1796
+ }
1797
+ /**
1798
+ * Abort the connection because the server replied with CopyInResponse /
1799
+ * CopyOutResponse while a pipeline (`_extPipelineActive`) was active.
1800
+ * Upstream libpq emits the exact diagnostic
1801
+ * `"COPY in a pipeline is not supported, aborting connection"` and tears
1802
+ * the socket down — we mirror that. Pending operations are rejected; the
1803
+ * connection is left in `closed` so subsequent commands fail cleanly
1804
+ * (matching the "aborting connection" promise).
1805
+ */
1806
+ abortForCopyInPipeline() {
1807
+ const err = {
1808
+ severity: "FATAL",
1809
+ message: "COPY in a pipeline is not supported, aborting connection"
1810
+ };
1811
+ this.socketError = new Error(err.message);
1812
+ this.failPending(err);
1813
+ try {
1814
+ this.socket.destroy();
1815
+ } catch {}
1816
+ this.state = "closed";
1817
+ }
1818
+ };
2516
1819
  /**
2517
- * Pluggable DNS resolver for the multi-IP host fan-out (libpq's
2518
- * `getaddrinfo`-then-iterate behaviour, exercised by upstream's
2519
- * `004_load_balance_dns.pl`). Tests inject a fake to drive a hostname
2520
- * through a fixed IP set without touching the real resolver; production
2521
- * code leaves it `null` and falls back to `dns.lookup(host, {all: true})`.
2522
- * Returning an empty array signals "treat as unresolvable" and the
2523
- * candidate is dropped from the iteration set (matching libpq's "no
2524
- * results from getaddrinfo" path).
2525
- */
2526
- PgConnection._dnsLookupAll = null;
2527
- // ---------------------------------------------------------------------------
2528
- // Socket open helper. Supports TCP (default) and Unix-domain sockets when
2529
- // `opts.host` starts with `/` — matching libpq's `pqUnixSocketPath()` which
2530
- // reads the directory from PGHOST and builds `<dir>/.s.PGSQL.<port>` as the
2531
- // actual filesystem socket path.
2532
- // ---------------------------------------------------------------------------
2533
- /**
2534
- * `true` if the host value should be interpreted as a Unix-domain socket
2535
- * directory. libpq's rule: any value starting with `/` is a path.
2536
- */
2537
- export function isUnixSocketHost(host) {
2538
- return host.startsWith("/");
1820
+ * `true` if the host value should be interpreted as a Unix-domain socket
1821
+ * directory. libpq's rule: any value starting with `/` is a path.
1822
+ */
1823
+ function isUnixSocketHost(host) {
1824
+ return host.startsWith("/");
2539
1825
  }
2540
1826
  /**
2541
- * Build the actual filesystem path Postgres listens on under a socket
2542
- * directory: `<dir>/.s.PGSQL.<port>`. Mirrors the libpq layout so any
2543
- * server started with `unix_socket_directories=<dir>` is reachable.
2544
- */
2545
- export function unixSocketPath(dir, port) {
2546
- return `${dir}/.s.PGSQL.${String(port)}`;
1827
+ * Build the actual filesystem path Postgres listens on under a socket
1828
+ * directory: `<dir>/.s.PGSQL.<port>`. Mirrors the libpq layout so any
1829
+ * server started with `unix_socket_directories=<dir>` is reachable.
1830
+ */
1831
+ function unixSocketPath(dir, port) {
1832
+ return `${dir}/.s.PGSQL.${String(port)}`;
2547
1833
  }
2548
1834
  /**
2549
- * Expand the configured (host, port) list by resolving each hostname to
2550
- * its full set of A/AAAA records. Mirrors libpq's `getaddrinfo`-then-
2551
- * iterate-all behaviour exercised by upstream's
2552
- * `src/interfaces/libpq/t/004_load_balance_dns.pl`. Without this step a
2553
- * single hostname that resolves to N IPs would only ever produce one
2554
- * candidate (Node's `net.connect({host})` picks one address from the
2555
- * lookup result), so `load_balance_hosts=random` couldn't shuffle across
2556
- * the DNS-returned set.
2557
- *
2558
- * - Unix-domain socket paths (`/var/run/postgres`) are passed through
2559
- * unchanged — they don't participate in DNS at all.
2560
- * - IPv4/IPv6 literals are passed through unchanged — DNS resolution
2561
- * would just round-trip them.
2562
- * - Hostnames are resolved via `dns.lookup(host, {all: true})`. The
2563
- * test seam `PgConnection._dnsLookupAll` overrides the resolver so
2564
- * unit tests can drive a hostname through a fixed IP set without
2565
- * touching the real DNS.
2566
- * - A hostname that fails to resolve (or returns zero records) is
2567
- * dropped from the iteration set. The connect loop's `lastErr`
2568
- * surfaces the original error if every host fails.
2569
- */
1835
+ * Expand the configured (host, port) list by resolving each hostname to
1836
+ * its full set of A/AAAA records. Mirrors libpq's `getaddrinfo`-then-
1837
+ * iterate-all behaviour exercised by upstream's
1838
+ * `src/interfaces/libpq/t/004_load_balance_dns.pl`. Without this step a
1839
+ * single hostname that resolves to N IPs would only ever produce one
1840
+ * candidate (Node's `net.connect({host})` picks one address from the
1841
+ * lookup result), so `load_balance_hosts=random` couldn't shuffle across
1842
+ * the DNS-returned set.
1843
+ *
1844
+ * - Unix-domain socket paths (`/var/run/postgres`) are passed through
1845
+ * unchanged — they don't participate in DNS at all.
1846
+ * - IPv4/IPv6 literals are passed through unchanged — DNS resolution
1847
+ * would just round-trip them.
1848
+ * - Hostnames are resolved via `dns.lookup(host, {all: true})`. The
1849
+ * test seam `PgConnection._dnsLookupAll` overrides the resolver so
1850
+ * unit tests can drive a hostname through a fixed IP set without
1851
+ * touching the real DNS.
1852
+ * - A hostname that fails to resolve (or returns zero records) is
1853
+ * dropped from the iteration set. The connect loop's `lastErr`
1854
+ * surfaces the original error if every host fails.
1855
+ */
2570
1856
  async function expandHostsViaDns(seed) {
2571
- const out = [];
2572
- for (const c of seed) {
2573
- if (isUnixSocketHost(c.host) || net.isIP(c.host) !== 0) {
2574
- // Unix-domain socket paths and IP literals don't go through DNS.
2575
- // Leave `address` undefined so `openSocket` uses `host` directly.
2576
- out.push({ host: c.host, port: c.port });
2577
- continue;
2578
- }
2579
- let addrs;
2580
- try {
2581
- addrs = PgConnection._dnsLookupAll
2582
- ? await PgConnection._dnsLookupAll(c.host)
2583
- : await dns.lookup(c.host, { all: true, family: 0 });
2584
- }
2585
- catch {
2586
- // dns.lookup rejects with ENOTFOUND / EAI_AGAIN / EAI_NONAME on
2587
- // resolution failure. Skip this host; the outer connect loop will
2588
- // surface the failure via `lastErr` if every candidate is dropped.
2589
- continue;
2590
- }
2591
- for (const a of addrs) {
2592
- // Keep the ORIGINAL hostname on `host` so TLS SNI / verify-full
2593
- // and `conn.host` see the user-typed name. The IP goes on
2594
- // `address`, used only by `openSocket` for the actual TCP connect.
2595
- out.push({ host: c.host, address: a.address, port: c.port });
2596
- }
2597
- }
2598
- return out;
1857
+ const out = [];
1858
+ for (const c of seed) {
1859
+ if (isUnixSocketHost(c.host) || net.isIP(c.host) !== 0) {
1860
+ out.push({
1861
+ host: c.host,
1862
+ port: c.port
1863
+ });
1864
+ continue;
1865
+ }
1866
+ let addrs;
1867
+ try {
1868
+ addrs = PgConnection._dnsLookupAll ? await PgConnection._dnsLookupAll(c.host) : await dns.lookup(c.host, {
1869
+ all: true,
1870
+ family: 0
1871
+ });
1872
+ } catch {
1873
+ continue;
1874
+ }
1875
+ for (const a of addrs) out.push({
1876
+ host: c.host,
1877
+ address: a.address,
1878
+ port: c.port
1879
+ });
1880
+ }
1881
+ return out;
2599
1882
  }
2600
1883
  /**
2601
- * Map a libpq protocol-version string (`TLSv1` / `TLSv1.1` / `TLSv1.2` /
2602
- * `TLSv1.3`) to Node's `SecureVersion` literal. Returns `undefined` for
2603
- * unset / empty input. Unknown values shouldn't reach here (the parsing
2604
- * layer in `index.ts` validates them), but we return `undefined` rather than
2605
- * casting so a stray value can't smuggle a bogus literal into Node's TLS
2606
- * options.
2607
- */
1884
+ * Map a libpq protocol-version string (`TLSv1` / `TLSv1.1` / `TLSv1.2` /
1885
+ * `TLSv1.3`) to Node's `SecureVersion` literal. Returns `undefined` for
1886
+ * unset / empty input. Unknown values shouldn't reach here (the parsing
1887
+ * layer in `index.ts` validates them), but we return `undefined` rather than
1888
+ * casting so a stray value can't smuggle a bogus literal into Node's TLS
1889
+ * options.
1890
+ */
2608
1891
  function toSecureVersion(value) {
2609
- switch (value) {
2610
- case "TLSv1":
2611
- case "TLSv1.1":
2612
- case "TLSv1.2":
2613
- case "TLSv1.3":
2614
- return value;
2615
- default:
2616
- return undefined;
2617
- }
1892
+ switch (value) {
1893
+ case "TLSv1":
1894
+ case "TLSv1.1":
1895
+ case "TLSv1.2":
1896
+ case "TLSv1.3": return value;
1897
+ default: return;
1898
+ }
2618
1899
  }
2619
1900
  /**
2620
- * Map libpq's `ssl_min_protocol_version` / `ssl_max_protocol_version` onto
2621
- * Node's `tls.connect` `minVersion` / `maxVersion`. Unset values leave Node's
2622
- * compiled-in defaults in place.
2623
- */
1901
+ * Map libpq's `ssl_min_protocol_version` / `ssl_max_protocol_version` onto
1902
+ * Node's `tls.connect` `minVersion` / `maxVersion`. Unset values leave Node's
1903
+ * compiled-in defaults in place.
1904
+ */
2624
1905
  function applyTlsProtocolVersionRange(tlsOpts, opts) {
2625
- const min = toSecureVersion(opts.sslMinProtocolVersion);
2626
- if (min !== undefined)
2627
- tlsOpts.minVersion = min;
2628
- const max = toSecureVersion(opts.sslMaxProtocolVersion);
2629
- if (max !== undefined)
2630
- tlsOpts.maxVersion = max;
1906
+ const min = toSecureVersion(opts.sslMinProtocolVersion);
1907
+ if (min !== void 0) tlsOpts.minVersion = min;
1908
+ const max = toSecureVersion(opts.sslMaxProtocolVersion);
1909
+ if (max !== void 0) tlsOpts.maxVersion = max;
2631
1910
  }
2632
1911
  /**
2633
- * libpq `sslsni`: the TLS SNI servername to send, or `undefined` to suppress
2634
- * the SNI extension entirely. `sslsni=0` (`opts.sslsni === false`) suppresses
2635
- * it; unset / `sslsni=1` sends `opts.host` (libpq's default). Returned value
2636
- * is spread into the `tls.connect` options as `servername`.
2637
- *
2638
- * Exported for unit tests.
2639
- */
2640
- export function tlsServername(opts) {
2641
- return opts.sslsni === false ? undefined : opts.host;
1912
+ * libpq `sslsni`: the TLS SNI servername to send, or `undefined` to suppress
1913
+ * the SNI extension entirely. `sslsni=0` (`opts.sslsni === false`) suppresses
1914
+ * it; unset / `sslsni=1` sends `opts.host` (libpq's default). Returned value
1915
+ * is spread into the `tls.connect` options as `servername`.
1916
+ *
1917
+ * Exported for unit tests.
1918
+ */
1919
+ function tlsServername(opts) {
1920
+ return opts.sslsni === false ? void 0 : opts.host;
2642
1921
  }
2643
1922
  /**
2644
- * Translate libpq's `keepalives` / `keepalives_idle` into the arguments for
2645
- * Node's `socket.setKeepAlive(enable, initialDelay)`:
2646
- * - `enable`: `false` only when `keepalives === false` (libpq `keepalives=0`);
2647
- * unset / `true` keeps keepalives on (libpq default).
2648
- * - `initialDelayMs`: `keepalives_idle` seconds → milliseconds, or
2649
- * `undefined` to leave the OS default.
2650
- *
2651
- * libpq's `keepalives_interval` and `keepalives_count` have NO Node net API
2652
- * equivalent (`setKeepAlive` exposes only enable + initial delay), so they are
2653
- * intentionally not represented here.
2654
- *
2655
- * Exported for unit tests.
2656
- */
2657
- export function keepAliveArgs(opts) {
2658
- return {
2659
- enable: opts.keepalives !== false,
2660
- initialDelayMs: opts.keepalivesIdle !== undefined
2661
- ? opts.keepalivesIdle * 1000
2662
- : undefined,
2663
- };
1923
+ * Translate libpq's `keepalives` / `keepalives_idle` into the arguments for
1924
+ * Node's `socket.setKeepAlive(enable, initialDelay)`:
1925
+ * - `enable`: `false` only when `keepalives === false` (libpq `keepalives=0`);
1926
+ * unset / `true` keeps keepalives on (libpq default).
1927
+ * - `initialDelayMs`: `keepalives_idle` seconds → milliseconds, or
1928
+ * `undefined` to leave the OS default.
1929
+ *
1930
+ * libpq's `keepalives_interval` and `keepalives_count` have NO Node net API
1931
+ * equivalent (`setKeepAlive` exposes only enable + initial delay), so they are
1932
+ * intentionally not represented here.
1933
+ *
1934
+ * Exported for unit tests.
1935
+ */
1936
+ function keepAliveArgs(opts) {
1937
+ return {
1938
+ enable: opts.keepalives !== false,
1939
+ initialDelayMs: opts.keepalivesIdle !== void 0 ? opts.keepalivesIdle * 1e3 : void 0
1940
+ };
2664
1941
  }
2665
1942
  /**
2666
- * Derive the symmetric key length (in bits) from a negotiated TLS cipher
2667
- * name, for the PG18 `\conninfo` "SSL Key Bits" row. Node's TLS API doesn't
2668
- * expose the key length directly, so we parse it out of the cipher name the
2669
- * way upstream's `SSL_CIPHER_get_bits` effectively reports it:
2670
- *
2671
- * - `AES_256` / `CHACHA20` → 256
2672
- * - `AES_128` → 128
2673
- * - otherwise, the first run of digits in the standard name (`…128…` etc.)
2674
- * - `null` when no length can be determined.
2675
- *
2676
- * Both the OpenSSL standard name (`TLS_AES_256_GCM_SHA384`,
2677
- * `ECDHE-RSA-AES128-GCM-SHA256`) and Node's IANA-style name are handled by
2678
- * uppercasing and matching the keyword forms first. Exported for unit tests.
2679
- */
2680
- export function sslKeyBitsFromCipher(name) {
2681
- const upper = name.toUpperCase();
2682
- if (upper.includes("CHACHA20"))
2683
- return 256;
2684
- if (upper.includes("AES_256") || upper.includes("AES256"))
2685
- return 256;
2686
- if (upper.includes("AES_128") || upper.includes("AES128"))
2687
- return 128;
2688
- if (upper.includes("AES_192") || upper.includes("AES192"))
2689
- return 192;
2690
- const digits = /(\d{2,4})/.exec(upper);
2691
- if (digits) {
2692
- const n = parseInt(digits[1], 10);
2693
- if (Number.isFinite(n) && n > 0)
2694
- return n;
2695
- }
2696
- return null;
1943
+ * Derive the symmetric key length (in bits) from a negotiated TLS cipher
1944
+ * name, for the PG18 `\conninfo` "SSL Key Bits" row. Node's TLS API doesn't
1945
+ * expose the key length directly, so we parse it out of the cipher name the
1946
+ * way upstream's `SSL_CIPHER_get_bits` effectively reports it:
1947
+ *
1948
+ * - `AES_256` / `CHACHA20` → 256
1949
+ * - `AES_128` → 128
1950
+ * - otherwise, the first run of digits in the standard name (`…128…` etc.)
1951
+ * - `null` when no length can be determined.
1952
+ *
1953
+ * Both the OpenSSL standard name (`TLS_AES_256_GCM_SHA384`,
1954
+ * `ECDHE-RSA-AES128-GCM-SHA256`) and Node's IANA-style name are handled by
1955
+ * uppercasing and matching the keyword forms first. Exported for unit tests.
1956
+ */
1957
+ function sslKeyBitsFromCipher(name) {
1958
+ const upper = name.toUpperCase();
1959
+ if (upper.includes("CHACHA20")) return 256;
1960
+ if (upper.includes("AES_256") || upper.includes("AES256")) return 256;
1961
+ if (upper.includes("AES_128") || upper.includes("AES128")) return 128;
1962
+ if (upper.includes("AES_192") || upper.includes("AES192")) return 192;
1963
+ const digits = /(\d{2,4})/.exec(upper);
1964
+ if (digits) {
1965
+ const n = parseInt(digits[1], 10);
1966
+ if (Number.isFinite(n) && n > 0) return n;
1967
+ }
1968
+ return null;
2697
1969
  }
2698
- function openSocket(opts,
2699
- /**
2700
- * Pre-resolved IP. When set, used for `net.connect({host})` instead
2701
- * of `opts.host` — lets DNS fan-out direct the TCP connect to a
2702
- * specific A record while keeping the user-typed hostname elsewhere
2703
- * (TLS SNI / `conn.host`). Ignored for Unix-domain socket paths,
2704
- * which take their address from `opts.host` directly.
2705
- */
2706
- addressOverride) {
2707
- return new Promise((resolve, reject) => {
2708
- const isUnix = isUnixSocketHost(opts.host);
2709
- const socket = isUnix
2710
- ? net.connect({ path: unixSocketPath(opts.host, opts.port) })
2711
- : net.connect({
2712
- host: addressOverride ?? opts.host,
2713
- port: opts.port,
2714
- });
2715
- // libpq TCP keepalives (no-op for Unix-domain sockets, matching libpq,
2716
- // which only applies SO_KEEPALIVE on TCP).
2717
- if (!isUnix) {
2718
- const { enable, initialDelayMs } = keepAliveArgs(opts);
2719
- if (initialDelayMs !== undefined) {
2720
- socket.setKeepAlive(enable, initialDelayMs);
2721
- }
2722
- else {
2723
- socket.setKeepAlive(enable);
2724
- }
2725
- }
2726
- const timeout = opts.connectTimeoutMs;
2727
- let timer = null;
2728
- if (timeout !== undefined && timeout > 0) {
2729
- timer = setTimeout(() => {
2730
- socket.destroy(new Error(`Connect timed out after ${String(timeout)} ms`));
2731
- }, timeout);
2732
- }
2733
- const cleanup = () => {
2734
- if (timer)
2735
- clearTimeout(timer);
2736
- socket.removeListener("error", onError);
2737
- socket.removeListener("connect", onConnect);
2738
- };
2739
- const onError = (err) => {
2740
- cleanup();
2741
- reject(err);
2742
- };
2743
- const onConnect = () => {
2744
- cleanup();
2745
- resolve(socket);
2746
- };
2747
- socket.once("error", onError);
2748
- socket.once("connect", onConnect);
2749
- });
1970
+ function openSocket(opts, addressOverride) {
1971
+ return new Promise((resolve, reject) => {
1972
+ const isUnix = isUnixSocketHost(opts.host);
1973
+ const socket = isUnix ? net.connect({ path: unixSocketPath(opts.host, opts.port) }) : net.connect({
1974
+ host: addressOverride ?? opts.host,
1975
+ port: opts.port
1976
+ });
1977
+ if (!isUnix) {
1978
+ const { enable, initialDelayMs } = keepAliveArgs(opts);
1979
+ if (initialDelayMs !== void 0) socket.setKeepAlive(enable, initialDelayMs);
1980
+ else socket.setKeepAlive(enable);
1981
+ }
1982
+ const timeout = opts.connectTimeoutMs;
1983
+ let timer = null;
1984
+ if (timeout !== void 0 && timeout > 0) timer = setTimeout(() => {
1985
+ socket.destroy(/* @__PURE__ */ new Error(`Connect timed out after ${String(timeout)} ms`));
1986
+ }, timeout);
1987
+ const cleanup = () => {
1988
+ if (timer) clearTimeout(timer);
1989
+ socket.removeListener("error", onError);
1990
+ socket.removeListener("connect", onConnect);
1991
+ };
1992
+ const onError = (err) => {
1993
+ cleanup();
1994
+ reject(err);
1995
+ };
1996
+ const onConnect = () => {
1997
+ cleanup();
1998
+ resolve(socket);
1999
+ };
2000
+ socket.once("error", onError);
2001
+ socket.once("connect", onConnect);
2002
+ });
2750
2003
  }
2751
- // ---------------------------------------------------------------------------
2752
- // Result decoding helpers
2753
- // ---------------------------------------------------------------------------
2754
2004
  /**
2755
- * Decode a wire-protocol DataRow into JS values. We follow the simple psql
2756
- * policy: text format → utf-8 string, binary format → Buffer. Type-aware
2757
- * decoding (timestamps, arrays, etc.) is the caller's responsibility — that
2758
- * matches `psql` which prints raw server text.
2759
- */
2005
+ * Decode a wire-protocol DataRow into JS values. We follow the simple psql
2006
+ * policy: text format → utf-8 string, binary format → Buffer. Type-aware
2007
+ * decoding (timestamps, arrays, etc.) is the caller's responsibility — that
2008
+ * matches `psql` which prints raw server text.
2009
+ */
2760
2010
  function decodeDataRow(values, fields) {
2761
- const out = new Array(values.length);
2762
- for (let i = 0; i < values.length; i++) {
2763
- const v = values[i];
2764
- if (v === null) {
2765
- out[i] = null;
2766
- continue;
2767
- }
2768
- const fmt = fields[i]?.format ?? 0;
2769
- out[i] = fmt === 1 ? v : v.toString("utf8");
2770
- }
2771
- return out;
2011
+ const out = new Array(values.length);
2012
+ for (let i = 0; i < values.length; i++) {
2013
+ const v = values[i];
2014
+ if (v === null) {
2015
+ out[i] = null;
2016
+ continue;
2017
+ }
2018
+ const fmt = fields[i]?.format ?? 0;
2019
+ out[i] = fmt === 1 ? v : v.toString("utf8");
2020
+ }
2021
+ return out;
2772
2022
  }
2773
2023
  /**
2774
- * Parse the CommandComplete tag — examples:
2775
- * "SELECT 17"
2776
- * "INSERT 0 1" (oid is 0 in modern PG; second number is rowCount)
2777
- * "UPDATE 3"
2778
- * "CREATE TABLE" (no rowCount)
2779
- *
2780
- * Anything that doesn't match → command = the whole tag, rowCount = null.
2781
- */
2024
+ * Parse the CommandComplete tag — examples:
2025
+ * "SELECT 17"
2026
+ * "INSERT 0 1" (oid is 0 in modern PG; second number is rowCount)
2027
+ * "UPDATE 3"
2028
+ * "CREATE TABLE" (no rowCount)
2029
+ *
2030
+ * Anything that doesn't match → command = the whole tag, rowCount = null.
2031
+ */
2782
2032
  function parseCommandTag(tag) {
2783
- const trimmed = tag.trim();
2784
- // INSERT is the only tag with the legacy oid + rowCount layout.
2785
- const insertMatch = /^INSERT (\d+) (\d+)$/.exec(trimmed);
2786
- if (insertMatch) {
2787
- return {
2788
- command: "INSERT",
2789
- oid: parseInt(insertMatch[1], 10),
2790
- rowCount: parseInt(insertMatch[2], 10),
2791
- };
2792
- }
2793
- const m = /^([A-Z][A-Z ]*?)(?: (\d+))?$/.exec(trimmed);
2794
- if (!m)
2795
- return { command: trimmed, rowCount: null, oid: null };
2796
- return {
2797
- command: m[1],
2798
- rowCount: m[2] !== undefined ? parseInt(m[2], 10) : null,
2799
- oid: null,
2800
- };
2033
+ const trimmed = tag.trim();
2034
+ const insertMatch = /^INSERT (\d+) (\d+)$/.exec(trimmed);
2035
+ if (insertMatch) return {
2036
+ command: "INSERT",
2037
+ oid: parseInt(insertMatch[1], 10),
2038
+ rowCount: parseInt(insertMatch[2], 10)
2039
+ };
2040
+ const m = /^([A-Z][A-Z ]*?)(?: (\d+))?$/.exec(trimmed);
2041
+ if (!m) return {
2042
+ command: trimmed,
2043
+ rowCount: null,
2044
+ oid: null
2045
+ };
2046
+ return {
2047
+ command: m[1],
2048
+ rowCount: m[2] !== void 0 ? parseInt(m[2], 10) : null,
2049
+ oid: null
2050
+ };
2801
2051
  }
2802
2052
  /**
2803
- * Encode JS values into the (Buffer | string | null)[] format that
2804
- * {@link Bind} accepts. Text-format only — server coerces. Matches psql's
2805
- * default `\bind` behaviour.
2806
- */
2807
- export function encodeParams(values) {
2808
- return values.map((v) => {
2809
- if (v === null || v === undefined)
2810
- return null;
2811
- if (Buffer.isBuffer(v))
2812
- return v;
2813
- if (typeof v === "string")
2814
- return v;
2815
- if (typeof v === "boolean")
2816
- return v ? "t" : "f";
2817
- if (typeof v === "number" || typeof v === "bigint")
2818
- return v.toString();
2819
- try {
2820
- return JSON.stringify(v);
2821
- }
2822
- catch {
2823
- return "";
2824
- }
2825
- });
2053
+ * Encode JS values into the (Buffer | string | null)[] format that
2054
+ * {@link Bind} accepts. Text-format only — server coerces. Matches psql's
2055
+ * default `\bind` behaviour.
2056
+ */
2057
+ function encodeParams(values) {
2058
+ return values.map((v) => {
2059
+ if (v === null || v === void 0) return null;
2060
+ if (Buffer.isBuffer(v)) return v;
2061
+ if (typeof v === "string") return v;
2062
+ if (typeof v === "boolean") return v ? "t" : "f";
2063
+ if (typeof v === "number" || typeof v === "bigint") return v.toString();
2064
+ try {
2065
+ return JSON.stringify(v);
2066
+ } catch {
2067
+ return "";
2068
+ }
2069
+ });
2826
2070
  }
2827
2071
  /**
2828
- * Coerce arbitrary rejection values to a thrown `Error`.
2829
- *
2830
- * For our `ConnectError` shape (`{ severity, code, message, … }`), the
2831
- * resulting Error preserves every enumerable field of the source object as
2832
- * own properties — so callers can still read `.code`, `.severity`, `.hint`,
2833
- * `.position`, etc. directly off the thrown value while also getting a proper
2834
- * `Error` instance (so `instanceof Error` works and `.message` / `.stack` are
2835
- * populated for generic loggers).
2836
- */
2072
+ * Coerce arbitrary rejection values to a thrown `Error`.
2073
+ *
2074
+ * For our `ConnectError` shape (`{ severity, code, message, … }`), the
2075
+ * resulting Error preserves every enumerable field of the source object as
2076
+ * own properties — so callers can still read `.code`, `.severity`, `.hint`,
2077
+ * `.position`, etc. directly off the thrown value while also getting a proper
2078
+ * `Error` instance (so `instanceof Error` works and `.message` / `.stack` are
2079
+ * populated for generic loggers).
2080
+ */
2837
2081
  function asThrowable(v) {
2838
- if (v instanceof Error)
2839
- return v;
2840
- if (typeof v === "object" &&
2841
- v !== null &&
2842
- "message" in v &&
2843
- typeof v.message === "string") {
2844
- const source = v;
2845
- const err = new Error(source.message);
2846
- // Copy every own enumerable field (severity, code, detail, hint, …) onto
2847
- // the Error so structural consumers keep working.
2848
- for (const key of Object.keys(source)) {
2849
- if (key === "message")
2850
- continue;
2851
- err[key] = source[key];
2852
- }
2853
- err.cause = v;
2854
- return err;
2855
- }
2856
- return new Error(String(v));
2082
+ if (v instanceof Error) return v;
2083
+ if (typeof v === "object" && v !== null && "message" in v && typeof v.message === "string") {
2084
+ const source = v;
2085
+ const err = new Error(source.message);
2086
+ for (const key of Object.keys(source)) {
2087
+ if (key === "message") continue;
2088
+ err[key] = source[key];
2089
+ }
2090
+ err.cause = v;
2091
+ return err;
2092
+ }
2093
+ return new Error(String(v));
2857
2094
  }
2095
+ //#endregion
2096
+ export { PgConnection, encodeParams, isUnixSocketHost, keepAliveArgs, sslKeyBitsFromCipher, tlsServername, unixSocketPath };