@prisma/orm-target-mysql 8.0.0-rc.22-dev.10 → 8.0.0-rc.22-dev.11

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 (42) hide show
  1. package/README.md +1 -1
  2. package/dist/{adapter-_WBomuTw.d.mts → adapter-Dl4Wwz1I.d.mts} +4 -2
  3. package/dist/adapter-Dl4Wwz1I.d.mts.map +1 -0
  4. package/dist/adapter.d.mts +1 -1
  5. package/dist/adapter.mjs +2 -2
  6. package/dist/adapter__adapter.d.mts +1 -1
  7. package/dist/adapter__adapter.mjs +1 -1
  8. package/dist/adapter__control.mjs +1 -1
  9. package/dist/adapter__runtime.mjs +74 -10
  10. package/dist/adapter__runtime.mjs.map +1 -1
  11. package/dist/{contract-contributions-B9njRJbt-ClEtrK2L.mjs → contract-contributions-ChiX6W0F-CAng98bZ.mjs} +5 -4
  12. package/dist/contract-contributions-ChiX6W0F-CAng98bZ.mjs.map +1 -0
  13. package/dist/{control-6wRZXIJ7.mjs → control-CgeXsxCR.mjs} +2 -2
  14. package/dist/{control-6wRZXIJ7.mjs.map → control-CgeXsxCR.mjs.map} +1 -1
  15. package/dist/{control-WTY7Jpxk.mjs → control-H2sjKXXS.mjs} +2 -2
  16. package/dist/{control-WTY7Jpxk.mjs.map → control-H2sjKXXS.mjs.map} +1 -1
  17. package/dist/{descriptor-meta-BurjcSiU-Cbk2L1jf.mjs → descriptor-meta-D7tc0iue-C4W7o0Av.mjs} +3 -3
  18. package/dist/{descriptor-meta-BurjcSiU-Cbk2L1jf.mjs.map → descriptor-meta-D7tc0iue-C4W7o0Av.mjs.map} +1 -1
  19. package/dist/{descriptor-meta-runtime-Bk_00UrX-BLN4Ahpj.mjs → descriptor-meta-runtime-Cur6umHo-C_4rsCGC.mjs} +7 -4
  20. package/dist/descriptor-meta-runtime-Cur6umHo-C_4rsCGC.mjs.map +1 -0
  21. package/dist/driver.mjs +1 -1
  22. package/dist/driver__control.mjs +1 -1
  23. package/dist/driver__runtime.mjs +275 -126
  24. package/dist/driver__runtime.mjs.map +1 -1
  25. package/dist/{marker-read-aEaHihYj-Cyg16yn1.mjs → marker-read-Cf80LSbG-BZaJ8Ll6.mjs} +3 -3
  26. package/dist/{marker-read-aEaHihYj-Cyg16yn1.mjs.map → marker-read-Cf80LSbG-BZaJ8Ll6.mjs.map} +1 -1
  27. package/dist/{pack-YVG-iBMQ.d.mts → pack-DXsOvRJn.d.mts} +2 -1
  28. package/dist/{pack-YVG-iBMQ.d.mts.map → pack-DXsOvRJn.d.mts.map} +1 -1
  29. package/dist/{statement-kfMRMKIt-D6cAOKys.mjs → statement-DC62oiSx-DTx1JaP7.mjs} +17 -11
  30. package/dist/statement-DC62oiSx-DTx1JaP7.mjs.map +1 -0
  31. package/dist/target.d.mts +1 -1
  32. package/dist/target__control.mjs +1 -1
  33. package/dist/target__control.mjs.map +1 -1
  34. package/dist/target__pack.d.mts +1 -1
  35. package/dist/target__pack.mjs +1 -1
  36. package/dist/target__runtime.mjs +1 -1
  37. package/dist/target__runtime.mjs.map +1 -1
  38. package/package.json +9 -9
  39. package/dist/adapter-_WBomuTw.d.mts.map +0 -1
  40. package/dist/contract-contributions-B9njRJbt-ClEtrK2L.mjs.map +0 -1
  41. package/dist/descriptor-meta-runtime-Bk_00UrX-BLN4Ahpj.mjs.map +0 -1
  42. package/dist/statement-kfMRMKIt-D6cAOKys.mjs.map +0 -1
@@ -1,4 +1,4 @@
1
- import { a as connectingCannotSucceed, c as isFatal, d as mysqlPoolOptions, f as normalizeMysqlError, l as mysqlConnectionOptions, m as statementFor, n as SessionSettingsGuard, r as acquireClientQueryLock, t as DEFAULT_CONNECT_TIMEOUT_MILLIS, u as mysqlDriverDescriptorMeta } from "./statement-kfMRMKIt-D6cAOKys.mjs";
1
+ import { c as mysqlConnectionOptions, d as mysqlPoolOptions, f as normalizeMysqlError, i as connectingCannotSucceed, l as mysqlDriverDescriptorMeta, m as statementFor, n as acquireClientQueryLock, s as isFatal, t as SessionSettingsGuard, u as mysqlPoolIdleTimeoutMillis } from "./statement-DC62oiSx-DTx1JaP7.mjs";
2
2
  import { blindCast } from "@prisma/orm-framework/utils/casts";
3
3
  import { ifDefined } from "@prisma/orm-framework/utils/defined";
4
4
  import { errorRuntime } from "@prisma/orm-framework/errors/execution";
@@ -6,6 +6,100 @@ import { createConnection, createPool } from "mariadb";
6
6
  import { EventEmitter } from "node:events";
7
7
  import { finished } from "node:stream/promises";
8
8
  //#region ../../../3-targets/mysql/3-driver/dist/runtime.mjs
9
+ /** How long the canceller waits to open its connection, and for MySQL to answer a `KILL QUERY`. */
10
+ const CANCEL_TIMEOUT_MILLIS = 2e3;
11
+ function timeout(millis) {
12
+ let timer;
13
+ return {
14
+ expired: new Promise((resolve) => {
15
+ timer = setTimeout(() => resolve("expired"), millis);
16
+ }),
17
+ clear: () => clearTimeout(timer)
18
+ };
19
+ }
20
+ /**
21
+ * Cancels statements with `KILL QUERY`, which MySQL takes only from another connection. It keeps one connection of its own, outside the pool, so a cancel neither waits for a pooled connection nor normally opens one; it opens it on first need, and ends it once it has been idle for the pool's idle timeout, as the pool ends its own; with a timeout of 0 it keeps it open, as the pool keeps its own. Opening it and each `KILL QUERY` are bounded by two seconds. The connection is never destroyed: the package's `destroy()` sends a `KILL` of its own whose failure it does not handle (`mariadb` 3.5.4). A connection that stops answering is set aside and ended with `end()` once its statement settles.
22
+ */
23
+ var Canceller = class {
24
+ #url;
25
+ #idleTimeoutMillis;
26
+ #connection;
27
+ #pending = /* @__PURE__ */ new WeakMap();
28
+ #closed = false;
29
+ #idleTimer;
30
+ constructor(url, idleTimeoutMillis) {
31
+ this.#url = url;
32
+ this.#idleTimeoutMillis = idleTimeoutMillis;
33
+ }
34
+ /** Sends `KILL QUERY` for `threadId`. Never rejects. A kept connection that is no longer valid is replaced once. */
35
+ async cancel(threadId, replaced = false) {
36
+ if (this.#closed || threadId === null) return "unsent";
37
+ clearTimeout(this.#idleTimer);
38
+ const opening = this.#connection ?? this.#open();
39
+ this.#connection = opening;
40
+ const connection = await opening;
41
+ if (connection === void 0) {
42
+ this.#setAside(opening);
43
+ return "unsent";
44
+ }
45
+ if (!connection.isValid()) {
46
+ this.#setAside(opening);
47
+ return replaced ? "unsent" : this.cancel(threadId, true);
48
+ }
49
+ this.#pending.set(connection, this.#pendingOn(connection) + 1);
50
+ const killing = connection.query("KILL QUERY ?", [threadId]).then(() => "sent", (error) => isFatal(error) ? "unconfirmed" : "unsent");
51
+ killing.then(() => {
52
+ this.#pending.set(connection, this.#pendingOn(connection) - 1);
53
+ this.#idleLater(opening, connection);
54
+ });
55
+ const limit = timeout(CANCEL_TIMEOUT_MILLIS);
56
+ const result = await Promise.race([killing, limit.expired]);
57
+ limit.clear();
58
+ if (result === "sent" || result === "unsent") return result;
59
+ this.#setAside(opening);
60
+ connection.end().catch(() => void 0);
61
+ return "unconfirmed";
62
+ }
63
+ /** Ends the connection: `end()` sends `QUIT` once any `KILL QUERY` on it settles. Waits for that only when no `KILL QUERY` is pending on it, so a connection that stopped answering cannot hold `close()` up. */
64
+ async close() {
65
+ this.#closed = true;
66
+ clearTimeout(this.#idleTimer);
67
+ const connection = await this.#connection;
68
+ this.#connection = void 0;
69
+ if (connection === void 0) return;
70
+ const ending = connection.end().catch(() => void 0);
71
+ if (this.#pendingOn(connection) === 0) await ending;
72
+ }
73
+ #idleLater(opening, connection) {
74
+ if (this.#idleTimeoutMillis === 0 || this.#pendingOn(connection) > 0 || this.#connection !== opening) return;
75
+ clearTimeout(this.#idleTimer);
76
+ this.#idleTimer = setTimeout(() => {
77
+ if (this.#pendingOn(connection) > 0 || this.#connection !== opening) return;
78
+ this.#connection = void 0;
79
+ connection.end().catch(() => void 0);
80
+ }, this.#idleTimeoutMillis);
81
+ this.#idleTimer.unref();
82
+ }
83
+ #pendingOn(connection) {
84
+ return this.#pending.get(connection) ?? 0;
85
+ }
86
+ #setAside(opening) {
87
+ if (this.#connection === opening) this.#connection = void 0;
88
+ }
89
+ async #open() {
90
+ try {
91
+ const connection = await createConnection(mysqlConnectionOptions(this.#url, { connectTimeoutMs: CANCEL_TIMEOUT_MILLIS }));
92
+ connection.on("error", () => void 0);
93
+ if (this.#closed) {
94
+ await connection.end().catch(() => void 0);
95
+ return;
96
+ }
97
+ return connection;
98
+ } catch {
99
+ return;
100
+ }
101
+ }
102
+ };
9
103
  function notConnected(message) {
10
104
  return errorRuntime("DRIVER.NOT_CONNECTED", message, { fix: "Call connect(binding) before running queries, and do not use the driver after close()." });
11
105
  }
@@ -23,8 +117,15 @@ function startStream(client, request, outcome) {
23
117
  throw settle(error, outcome);
24
118
  }
25
119
  }
26
- function affectedRows(result) {
27
- return typeof result === "object" && result !== null && "affectedRows" in result && typeof result.affectedRows === "number" ? result.affectedRows : 0;
120
+ /** What a statement's result reports: the rows it changed, and, when it changed one row, the value MySQL numbered for it. MySQL reports 0 when it numbered none. */
121
+ function statementStats(result) {
122
+ if (typeof result !== "object" || result === null) return { affectedRows: 0 };
123
+ const affectedRows = "affectedRows" in result && typeof result.affectedRows === "number" ? result.affectedRows : 0;
124
+ const insertId = "insertId" in result ? result.insertId : void 0;
125
+ return affectedRows === 1 && (typeof insertId === "bigint" || typeof insertId === "number") && insertId > 0 ? {
126
+ affectedRows,
127
+ insertId: String(insertId)
128
+ } : { affectedRows };
28
129
  }
29
130
  /** The error to report for `error`, after noting in `outcome` whether it left the client unusable. */
30
131
  function settle(error, outcome) {
@@ -38,56 +139,58 @@ function resultReceived(rows) {
38
139
  const state = Reflect.get(rows, "_readableState");
39
140
  return typeof state === "object" && state !== null && "ended" in state && state.ended === true;
40
141
  }
41
- /** Reads and drops the rest of `rows` and settles once it has ended or failed. A caller that abandoned its loop over the stream leaves the loop's `'readable'` listener behind, which would keep the stream paused, so it is removed. */
142
+ /**
143
+ * Reads and drops what is left of a stream's result. The package's `close()` stops it keeping new rows; the rows already buffered are read here with `read()`, which works whether or not the caller's iterator is still attached to the stream, so an iterator the caller abandoned cannot hold the drain up.
144
+ */
42
145
  async function dropRest(rows) {
43
146
  rows.close?.();
44
- rows.removeAllListeners("readable");
45
- rows.resume();
46
- await finished(rows);
47
- }
48
- /** The package can report an error on a stream after the caller stopped reading it, such as the one for a connection destroyed mid-result; the generator has already settled by then. */
49
- function ignoreAfterStop() {}
50
- /** Waiting for a statement to end needs only that it settled; its own caller handles how. */
51
- function settledEither() {}
52
- /** Waits for `work` to settle, but no longer than `ms`. */
53
- async function settledWithin(work, ms) {
54
- let timer;
55
- const elapsed = new Promise((resolve) => {
56
- timer = setTimeout(resolve, ms);
57
- });
147
+ const drain = () => {
148
+ while (rows.read() !== null);
149
+ };
150
+ rows.on("readable", drain);
58
151
  try {
59
- await Promise.race([work.then(settledEither, settledEither), elapsed]);
152
+ drain();
153
+ await finished(rows);
60
154
  } finally {
61
- clearTimeout(timer);
155
+ rows.off("readable", drain);
62
156
  }
63
157
  }
64
- function noConnection() {}
65
- /** The server may have run a `KILL` whose reply was lost, so the statement may still end; the caller waits for it either way. */
66
- function killReplyLost() {}
67
- /**
68
- * Sends `KILL QUERY` for the statement running on the connection with id `threadId`, over a short-lived connection of its own, because MySQL cannot interrupt a statement from the connection that runs it. That connection waits for the reply no longer than it waits to connect. Resolves `true` once the `KILL` was sent, whatever the reply, and `false` when the connection could not be opened. Never rejects.
69
- *
70
- * The package does the same when it destroys a connection with a statement still running, but leaves a failure of its `KILL` unhandled, which ends a Node.js process. So the driver interrupts the statement itself and destroys the connection only once the statement has ended, when the package has nothing left to interrupt.
71
- */
72
- async function interruptStatement(options, threadId) {
73
- const connection = await createConnection({
74
- ...options,
75
- ...ifDefined("socketTimeout", options.connectTimeout)
76
- }).catch(noConnection);
77
- if (connection === void 0) return false;
78
- await connection.query("KILL QUERY ?", [threadId]).catch(killReplyLost);
79
- connection.destroy();
80
- return true;
158
+ function cancelNotConfirmed() {
159
+ return errorRuntime("DRIVER.NOT_CONNECTED", "A cancel on this MySQL connection could not be confirmed, so the connection can no longer be used", {
160
+ why: "The driver cancelled a stream the caller stopped early, and MySQL did not answer the KILL QUERY in time. It may still arrive and would interrupt whatever statement the connection runs then.",
161
+ fix: "Release the connection, which closes it; MySQL rolls back an open transaction on it. Acquire another connection and run the work again."
162
+ });
163
+ }
164
+ function connectionReleased() {
165
+ return errorRuntime("DRIVER.NOT_CONNECTED", "MySQL connection was released", { fix: "Send statements on a connection only until you release it, and await each one before release()." });
166
+ }
167
+ /** The package can report an error on a stream after the caller stopped reading it, such as MySQL's for a statement the driver cancelled; the generator has already settled by then. */
168
+ function ignoreAfterStop() {}
169
+ /** MySQL's error for a statement `KILL QUERY` cancelled (`ER_QUERY_INTERRUPTED`). The package marks it fatal when it arrives in the middle of a result, but the connection stays usable. */
170
+ function isQueryInterrupted(error) {
171
+ return error instanceof Error && "errno" in error && error.errno === 1317;
81
172
  }
82
173
  var MysqlQueryable = class {
83
174
  streams;
84
- /** Called with each stream when it starts and with `undefined` when it ends, so a held connection can end a stream its caller abandoned. */
85
- streamChanged(_rows) {}
86
- /** Called with each buffered statement as it starts, so a held connection can wait for it to end. */
87
- statementRunning(_running) {}
88
175
  constructor(streams) {
89
176
  this.streams = streams;
90
177
  }
178
+ /** Records a stream that is open, and returns the call that records its end. */
179
+ streamOpened(_rows) {
180
+ return () => {};
181
+ }
182
+ /** Takes the client's lock, which keeps statements on it one at a time in the order sent. */
183
+ lockClient(client) {
184
+ return acquireClientQueryLock(client);
185
+ }
186
+ /** Runs one statement that is not a stream, under the client's lock. */
187
+ runStatement(work) {
188
+ return work();
189
+ }
190
+ /** Whether the client went back to the pool under this queryable, so it must send nothing more on it. */
191
+ get handedBack() {
192
+ return false;
193
+ }
91
194
  async *query(request) {
92
195
  if (this.streams) {
93
196
  yield* this.#streamRows(request);
@@ -97,17 +200,15 @@ var MysqlQueryable = class {
97
200
  for (const row of Array.isArray(result) ? result : []) yield blindCast(row);
98
201
  }
99
202
  async execute(request) {
100
- return { affectedRows: affectedRows(await this.#runReleasing(request)) };
203
+ return statementStats(await this.#runReleasing(request));
101
204
  }
102
205
  async #runReleasing(request) {
103
206
  const client = await this.acquireClient();
104
207
  const outcome = { destroy: false };
105
208
  try {
106
- const releaseLock = await acquireClientQueryLock(client);
209
+ const releaseLock = await this.lockClient(client);
107
210
  try {
108
- const running = run(client, request);
109
- this.statementRunning(running);
110
- return await running;
211
+ return await this.runStatement(() => run(client, request));
111
212
  } finally {
112
213
  releaseLock();
113
214
  }
@@ -118,17 +219,22 @@ var MysqlQueryable = class {
118
219
  }
119
220
  }
120
221
  /**
121
- * Streams the rows of `request` as they arrive; the package pauses the socket while the caller is busy. A caller that stops reading early is handled by {@link MysqlQueryable.#stopEarly}, or by {@link MysqlQueryable.#interruptAndEnd} for a pooled client whose result is still arriving, which then gives the client back in the background. A fatal error, such as a lost connection, destroys the client.
222
+ * Streams the rows of `request` as they arrive; the package pauses the socket while the caller is busy. A caller that stops reading early is handled by {@link MysqlQueryable.#stopEarly}. A fatal error, such as a lost connection, destroys the client.
122
223
  */
123
224
  async *#streamRows(request) {
124
225
  const client = await this.acquireClient();
125
226
  const outcome = { destroy: false };
126
- const releaseLock = await acquireClientQueryLock(client);
127
- let ending;
227
+ let releaseLock;
228
+ try {
229
+ releaseLock = await this.lockClient(client);
230
+ } catch (error) {
231
+ await this.releaseClient(client, outcome);
232
+ throw error;
233
+ }
128
234
  try {
129
235
  const rows = startStream(client, request, outcome);
130
236
  rows.on("error", ignoreAfterStop);
131
- this.streamChanged(rows);
237
+ const streamClosed = this.streamOpened(rows);
132
238
  let settled = false;
133
239
  try {
134
240
  for await (const row of rows.iterator({ destroyOnReturn: false })) yield blindCast(row);
@@ -137,94 +243,126 @@ var MysqlQueryable = class {
137
243
  settled = true;
138
244
  throw settle(error, outcome);
139
245
  } finally {
140
- if (!settled && !this.holdsClient && !resultReceived(rows)) {
141
- outcome.destroy = true;
142
- ending = this.#interruptAndEnd(client, rows);
143
- } else if (!settled) await this.#stopEarly(rows, outcome);
144
- if (outcome.destroy && ending === void 0) rows.destroy();
145
- this.streamChanged(void 0);
246
+ if (!settled && !this.handedBack) await this.#stopEarly(client, rows, outcome);
247
+ if (outcome.destroy) rows.destroy();
248
+ streamClosed();
146
249
  }
147
250
  } finally {
148
- if (ending === void 0) {
149
- releaseLock();
150
- await this.releaseClient(client, outcome);
151
- } else ending.then(() => {
152
- releaseLock();
153
- return this.releaseClient(client, outcome);
154
- }).catch(ignoreAfterStop);
251
+ releaseLock();
252
+ await this.releaseClient(client, outcome);
155
253
  }
156
254
  }
157
- /** Ends a stream the caller stopped reading early whose client survives it: a held client, whose session must survive, or a pooled one whose whole result has arrived. The rest is read and dropped. */
158
- async #stopEarly(rows, outcome) {
255
+ /**
256
+ * Ends a stream the caller stopped reading early, and keeps the client usable. While its result is still arriving, the statement is first cancelled with `KILL QUERY`, so a large result is not downloaded only to be thrown away; MySQL does not say how many rows remain. On a held connection or in a transaction, as on a pooled one: `KILL QUERY` ends only the statement, the session survives, and in a transaction MySQL rolls back only that statement. The rest of the result is then read and dropped, MySQL's error for the cancelled statement among it. The client is never destroyed while its statement runs: the package's `destroy()` would send `KILL` itself, on a connection whose failure it does not handle.
257
+ */
258
+ async #stopEarly(client, rows, outcome) {
259
+ const cancel = resultReceived(rows) ? "unsent" : await this.cancelStatement(client.threadId);
159
260
  await dropRest(rows).catch((error) => {
160
- if (isFatal(error)) outcome.destroy = true;
261
+ if (isFatal(error) && !(cancel !== "unsent" && isQueryInterrupted(error))) outcome.destroy = true;
161
262
  });
263
+ if (cancel === "unconfirmed") this.cancelUnconfirmed(outcome);
162
264
  }
163
265
  /**
164
- * Ends a stream the caller stopped reading early on a pooled client whose result is still arriving. The client is destroyed rather than kept, so a large result is not downloaded only to be thrown away, since MySQL does not say how many rows remain; before that, its statement is interrupted and the rest of the result dropped, so the package finds nothing left to interrupt. Settles once the result has ended or the wait for it is over.
266
+ * A cancel that went out without an answer may still reach MySQL and interrupt the next statement on the client, so the client must not serve another caller. Once its rest has been read, nothing is receiving on it, so closing it sends no `KILL` of the package's own; a late `KILL QUERY` then finds no thread.
165
267
  */
166
- async #interruptAndEnd(client, rows) {
167
- await this.endStatement(client, dropRest(rows).catch(ignoreAfterStop));
168
- rows.destroy();
268
+ cancelUnconfirmed(outcome) {
269
+ outcome.destroy = true;
169
270
  }
170
271
  };
272
+ function handedBackState() {
273
+ let refuse = () => {};
274
+ const signal = new Promise((_, reject) => {
275
+ refuse = () => reject(connectionReleased());
276
+ });
277
+ signal.catch(() => void 0);
278
+ const state = {
279
+ done: false,
280
+ signal,
281
+ refuse: () => {
282
+ state.done = true;
283
+ refuse();
284
+ }
285
+ };
286
+ return state;
287
+ }
171
288
  /**
172
289
  * A queryable on a connection the caller holds, whose statements all run on the same session, in the order they are sent, under the client's lock. A statement sent while a stream on the connection is open waits for that stream to end; one awaited inside the stream's own loop never finishes, as on Postgres with a cursor.
173
290
  */
174
291
  var MysqlHeldQueryable = class extends MysqlQueryable {
175
292
  client;
176
293
  held;
177
- end;
178
294
  holdsClient = true;
179
- constructor(client, held, streams, end) {
180
- super(streams);
181
- this.client = client;
182
- this.held = held;
183
- this.end = end;
295
+ get cancelStatement() {
296
+ return this.held.cancel;
297
+ }
298
+ streamOpened(rows) {
299
+ this.held.streams.add(rows);
300
+ return () => this.held.streams.delete(rows);
301
+ }
302
+ get handedBack() {
303
+ return this.held.handedBack.done;
304
+ }
305
+ cancelUnconfirmed() {
306
+ this.held.cancelUnconfirmed = true;
184
307
  }
185
- endStatement(client, ended) {
186
- return this.end(client, ended);
308
+ /** Takes the client's lock, or fails once the connection is handed back, whether the statement was sent after that or was still waiting for the lock. A lock taken after that is given straight back. */
309
+ async lockClient(client) {
310
+ this.#refuseIfUnusable();
311
+ const locking = acquireClientQueryLock(client);
312
+ try {
313
+ const releaseLock = await Promise.race([locking, this.held.handedBack.signal]);
314
+ if (!this.held.handedBack.done && !this.held.cancelUnconfirmed) return releaseLock;
315
+ releaseLock();
316
+ } catch {
317
+ locking.then((releaseLock) => releaseLock());
318
+ }
319
+ this.#refuseIfUnusable();
320
+ throw connectionReleased();
187
321
  }
188
- streamChanged(rows) {
189
- this.held.stream = rows;
322
+ /** Refuses a statement on a connection that was handed back, or whose cancel went out without an answer: a late `KILL QUERY` would interrupt it. */
323
+ #refuseIfUnusable() {
324
+ if (this.held.handedBack.done) throw connectionReleased();
325
+ if (this.held.cancelUnconfirmed) throw cancelNotConfirmed();
190
326
  }
191
- statementRunning(running) {
192
- const settled = running.then(settledEither, settledEither);
193
- this.held.statements.add(settled);
194
- settled.then(() => this.held.statements.delete(settled));
327
+ runStatement(work) {
328
+ const running = work();
329
+ this.held.running.add(running);
330
+ running.then(() => this.held.running.delete(running), () => this.held.running.delete(running));
331
+ return running;
332
+ }
333
+ constructor(client, held, streams) {
334
+ super(streams);
335
+ this.client = client;
336
+ this.held = held;
195
337
  }
196
338
  async acquireClient() {
197
- this.held.running += 1;
339
+ this.#refuseIfUnusable();
198
340
  return this.client;
199
341
  }
200
- async releaseClient() {
201
- this.held.running -= 1;
202
- }
342
+ async releaseClient() {}
203
343
  /** Runs one of the package's transaction calls under the client's lock, taken as a statement takes it, so it keeps its place among the statements sent. */
204
344
  async underLock(call) {
205
345
  const client = await this.acquireClient();
346
+ const releaseLock = await this.lockClient(client);
206
347
  try {
207
- const releaseLock = await acquireClientQueryLock(client);
208
- try {
209
- await call();
210
- } finally {
211
- releaseLock();
212
- }
348
+ await this.runStatement(call);
213
349
  } catch (error) {
214
350
  throw normalizeMysqlError(error);
215
351
  } finally {
216
- await this.releaseClient();
352
+ releaseLock();
217
353
  }
218
354
  }
219
355
  };
220
356
  var MysqlConnectionImpl = class extends MysqlHeldQueryable {
221
357
  #onRelease;
222
- constructor(client, streams, end, onRelease) {
358
+ constructor(client, streams, cancel, onRelease) {
223
359
  super(client, {
224
- running: 0,
225
- stream: void 0,
226
- statements: /* @__PURE__ */ new Set()
227
- }, streams, end);
360
+ streams: /* @__PURE__ */ new Set(),
361
+ cancelUnconfirmed: false,
362
+ running: /* @__PURE__ */ new Set(),
363
+ cancel,
364
+ handedBack: handedBackState()
365
+ }, streams);
228
366
  this.#onRelease = onRelease;
229
367
  }
230
368
  #released() {
@@ -234,28 +372,46 @@ var MysqlConnectionImpl = class extends MysqlHeldQueryable {
234
372
  }
235
373
  async beginTransaction() {
236
374
  await this.underLock(() => this.client.beginTransaction());
237
- return new MysqlTransactionImpl(this.client, this.held, this.streams, this.end);
375
+ return new MysqlTransactionImpl(this.client, this.held, this.streams);
238
376
  }
239
377
  /**
240
- * Returns the connection to the pool, as the Postgres driver's `release()` does. A connection with a statement still running, such as a stream the caller dropped, is destroyed instead: the package's release sends `ROLLBACK` behind that statement and waits for it, which would never finish, where `pg`'s release does not wait on the server.
378
+ * Returns the connection to the pool, as the Postgres driver's `release()` does. From then on the connection refuses every statement, those still waiting for its lock included, so none runs later in another caller's session. A statement that is not a stream and is already running finishes, as on Postgres. A stream still open, such as one the caller dropped, is cancelled and read to its end by the driver: the package's release sends `ROLLBACK` behind that statement and waits for it, which would otherwise never finish, where `pg`'s release does not wait on the server.
241
379
  */
242
380
  async release() {
243
- if (this.held.running > 0) return this.destroy();
381
+ if (this.held.handedBack.done) return;
382
+ this.held.handedBack.refuse();
244
383
  try {
245
- await this.client.release();
384
+ if (this.held.streams.size > 0) await this.#cancelRunning();
385
+ await this.#runningSettled();
386
+ if (this.held.cancelUnconfirmed) await this.#close();
387
+ else await this.client.release();
246
388
  } finally {
247
389
  this.#released();
248
390
  }
249
391
  }
250
- /** Destroys the connection. A statement still running on it, buffered or a stream its caller abandoned, is interrupted first, and the connection destroyed once it has ended; see {@link interruptStatement}. */
392
+ /** Closes the connection instead of returning it. It refuses every statement from then on, cancels whatever still runs and waits for it to end, so the package's `destroy()` never sends a `KILL` of its own. */
251
393
  async destroy(_reason) {
252
- if (this.held.running > 0) {
253
- const stream = this.held.stream;
254
- const ended = Promise.all([...stream === void 0 ? [] : [dropRest(stream).catch(ignoreAfterStop)], ...this.held.statements]);
255
- await this.endStatement(this.client, ended);
394
+ if (this.held.handedBack.done) return;
395
+ this.held.handedBack.refuse();
396
+ try {
397
+ if (this.held.streams.size > 0 || this.held.running.size > 0) await this.#cancelRunning();
398
+ await this.#runningSettled();
399
+ await this.#close();
400
+ } finally {
401
+ this.#released();
256
402
  }
403
+ }
404
+ /** Closes the client once the `ping` queued behind anything left on it has answered, so nothing is receiving and the package's `destroy()` sends no `KILL` of its own. */
405
+ async #close() {
406
+ await this.client.ping().catch(() => void 0);
257
407
  this.client.destroy();
258
- this.#released();
408
+ }
409
+ async #cancelRunning() {
410
+ if (await this.held.cancel(this.client.threadId) === "unconfirmed") this.held.cancelUnconfirmed = true;
411
+ await Promise.all([...this.held.streams].map((rows) => dropRest(rows).catch(() => void 0)));
412
+ }
413
+ async #runningSettled() {
414
+ await Promise.allSettled([...this.held.running]);
259
415
  }
260
416
  };
261
417
  var MysqlTransactionImpl = class extends MysqlHeldQueryable {
@@ -319,9 +475,9 @@ var MysqlDriver = class extends MysqlQueryable {
319
475
  targetId = "mysql";
320
476
  holdsClient = false;
321
477
  #state = { kind: "unbound" };
322
- /** The options of the short-lived connection that interrupts a statement, and how long to wait for an interrupted statement to end: as long as a connection may take to open, or the default when `connectionTimeoutMillis` is 0, which the package reads as no limit. Set by `connect()` and kept through `close()`, which waits for statements still ending. */
323
- #interrupt;
324
478
  #inUse = new ClientsInUse();
479
+ #canceller;
480
+ cancelStatement = (threadId) => this.#canceller?.cancel(threadId) ?? Promise.resolve("unsent");
325
481
  #options;
326
482
  #sessionSettings;
327
483
  constructor(options = {}) {
@@ -340,33 +496,23 @@ var MysqlDriver = class extends MysqlQueryable {
340
496
  logger: { error: (error) => waiting.reported(error) }
341
497
  });
342
498
  if (pool instanceof EventEmitter) pool.on("error", ignoreConnectionFailure);
343
- const connectionTimeout = this.#options.poolOptions?.connectionTimeoutMillis;
344
- const interruptTimeoutMs = connectionTimeout !== void 0 && connectionTimeout > 0 ? connectionTimeout : DEFAULT_CONNECT_TIMEOUT_MILLIS;
345
- this.#interrupt = {
346
- options: mysqlConnectionOptions(binding.url, { connectTimeoutMs: interruptTimeoutMs }),
347
- waitMs: interruptTimeoutMs
348
- };
349
499
  this.#state = {
350
500
  kind: "connected",
351
501
  pool,
352
502
  waiting
353
503
  };
354
- }
355
- /** See {@link EndStatement}. When the `KILL` cannot be sent at all, there is no wait: the client is destroyed at once, and the package interrupts the statement itself. */
356
- async endStatement(client, ended) {
357
- const settled = ended.then(settledEither, settledEither);
358
- const interrupt = this.#interrupt;
359
- if (interrupt === void 0 || client.threadId === null) return;
360
- if (await interruptStatement(interrupt.options, client.threadId)) await settledWithin(settled, interrupt.waitMs);
504
+ this.#canceller = new Canceller(binding.url, mysqlPoolIdleTimeoutMillis(this.#options.poolOptions));
361
505
  }
362
506
  async acquireConnection() {
363
- return new MysqlConnectionImpl(await this.acquireClient(), this.streams, (held, ended) => this.endStatement(held, ended), () => this.#inUse.released());
507
+ return new MysqlConnectionImpl(await this.acquireClient(), this.streams, this.cancelStatement, () => this.#inUse.released());
364
508
  }
365
509
  async close() {
366
510
  if (this.#state.kind !== "connected") return;
367
511
  const { pool } = this.#state;
368
512
  this.#state = { kind: "closed" };
369
513
  await this.#inUse.none();
514
+ await this.#canceller?.close();
515
+ this.#canceller = void 0;
370
516
  await pool.end();
371
517
  }
372
518
  async acquireClient() {
@@ -391,6 +537,9 @@ var MysqlDriver = class extends MysqlQueryable {
391
537
  }
392
538
  return client;
393
539
  }
540
+ /**
541
+ * Destroys a client only after an error the package marks fatal, or after a cancel that went out without an answer once the rest of its result was read (`cancelUnconfirmed`). The package marks an error fatal when the connection's socket is gone or can no longer be trusted, and it has then already set the connection closed and failed every command on it (`fatalError` in its `connection.js`), so no command is receiving and its `destroy()` sends no `KILL`. A statement the caller stopped early is cancelled and read to its end instead (`#stopEarly`).
542
+ */
394
543
  async releaseClient(client, { destroy }) {
395
544
  try {
396
545
  if (destroy) client.destroy();