@mason-data/cli 0.0.0-stage → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +268 -2
  3. package/dist/src/api/error.js +18 -0
  4. package/dist/src/api/postgres/catalog-worker.js +92 -0
  5. package/dist/src/api/postgres/catalog.js +477 -0
  6. package/dist/src/api/postgres/encode.js +541 -0
  7. package/dist/src/api/postgres/explain.js +143 -0
  8. package/dist/src/api/postgres/listener.js +60 -0
  9. package/dist/src/api/postgres/parser-worker.js +58 -0
  10. package/dist/src/api/postgres/parser.js +121 -0
  11. package/dist/src/api/postgres/plain.js +33 -0
  12. package/dist/src/api/postgres/protocol.js +404 -0
  13. package/dist/src/api/postgres/schema.js +54 -0
  14. package/dist/src/api/postgres/server.js +1771 -0
  15. package/dist/src/api/postgres/translate.js +248 -0
  16. package/dist/src/api/request-log.js +50 -0
  17. package/dist/src/cdc/pg-wire.js +588 -0
  18. package/dist/src/cdc/pgoutput.js +107 -0
  19. package/dist/src/cdc/poll.js +67 -0
  20. package/dist/src/cdc/source.js +65 -0
  21. package/dist/src/cli/background.js +198 -0
  22. package/dist/src/cli/command.js +173 -0
  23. package/dist/src/cli/commands.js +1277 -0
  24. package/dist/src/cli/control.js +36 -0
  25. package/dist/src/cli/demo.js +149 -0
  26. package/dist/src/cli/dev.js +669 -0
  27. package/dist/src/cli/doctor.js +260 -0
  28. package/dist/src/cli/guides.js +158 -0
  29. package/dist/src/cli/init.js +366 -0
  30. package/dist/src/cli/machine.js +162 -0
  31. package/dist/src/cli/main.js +83 -0
  32. package/dist/src/cli/mcp.js +726 -0
  33. package/dist/src/cli/pg-tls.js +100 -0
  34. package/dist/src/cli/pg-url.js +52 -0
  35. package/dist/src/cli/pin.js +36 -0
  36. package/dist/src/cli/ports.js +118 -0
  37. package/dist/src/cli/preflight.js +347 -0
  38. package/dist/src/cli/project.js +129 -0
  39. package/dist/src/cli/readiness.js +76 -0
  40. package/dist/src/cli/registry.js +220 -0
  41. package/dist/src/cli/replication-checks.js +120 -0
  42. package/dist/src/cli/replication-mode.js +148 -0
  43. package/dist/src/cli/run-server.js +311 -0
  44. package/dist/src/cli/runs.js +120 -0
  45. package/dist/src/cli/security.js +86 -0
  46. package/dist/src/cli/self-signed-cert.js +64 -0
  47. package/dist/src/cli/serve.js +301 -0
  48. package/dist/src/cli/source-edit.js +116 -0
  49. package/dist/src/cli/sources.js +564 -0
  50. package/dist/src/cli/state.js +58 -0
  51. package/dist/src/cli/style.js +187 -0
  52. package/dist/src/cli/supervise.js +248 -0
  53. package/dist/src/cli/sync.js +94 -0
  54. package/dist/src/cli/ui.js +107 -0
  55. package/dist/src/cli/user.js +229 -0
  56. package/dist/src/cli/version.js +23 -0
  57. package/dist/src/demo/seed.js +237 -0
  58. package/dist/src/demo/source.js +655 -0
  59. package/dist/src/duck/analyze.js +113 -0
  60. package/dist/src/duck/cancel.js +49 -0
  61. package/dist/src/duck/distinct-key.js +228 -0
  62. package/dist/src/duck/executor-worker.js +268 -0
  63. package/dist/src/duck/executor.js +567 -0
  64. package/dist/src/duck/explain.js +238 -0
  65. package/dist/src/duck/functions.js +819 -0
  66. package/dist/src/duck/gaps.js +94 -0
  67. package/dist/src/duck/jsonb-order.js +175 -0
  68. package/dist/src/duck/jsonb.js +188 -0
  69. package/dist/src/duck/kept-route.js +299 -0
  70. package/dist/src/duck/kept.js +964 -0
  71. package/dist/src/duck/late.js +221 -0
  72. package/dist/src/duck/libc-order.js +190 -0
  73. package/dist/src/duck/pg-eval-worker.js +34 -0
  74. package/dist/src/duck/pg-eval.js +59 -0
  75. package/dist/src/duck/pgtree.js +378 -0
  76. package/dist/src/duck/plan.js +259 -0
  77. package/dist/src/duck/publication.js +207 -0
  78. package/dist/src/duck/requests.js +209 -0
  79. package/dist/src/duck/resolved.js +248 -0
  80. package/dist/src/duck/rewrite.js +5101 -0
  81. package/dist/src/duck/store.js +907 -0
  82. package/dist/src/duck/stream.js +518 -0
  83. package/dist/src/duck/sync.js +2929 -0
  84. package/dist/src/duck/to-char.js +482 -0
  85. package/dist/src/duck/token.js +31 -0
  86. package/dist/src/duck/types.js +427 -0
  87. package/dist/src/duck/users.js +231 -0
  88. package/dist/src/duck/verified.js +1001 -0
  89. package/dist/src/duck/views.js +150 -0
  90. package/dist/src/lib/database-url.js +74 -0
  91. package/dist/src/lib/did-you-mean.js +43 -0
  92. package/dist/src/lib/dotenv.js +72 -0
  93. package/dist/src/lib/exit-when-written.js +20 -0
  94. package/dist/src/lib/float-equal.js +69 -0
  95. package/dist/src/lib/http-json.js +22 -0
  96. package/dist/src/lib/log.js +161 -0
  97. package/dist/src/lib/pg-numeric.js +489 -0
  98. package/dist/src/lib/port.js +22 -0
  99. package/dist/src/lib/project-file.js +49 -0
  100. package/dist/src/lib/project-files.js +43 -0
  101. package/dist/src/lib/project-version.js +76 -0
  102. package/dist/src/lib/runnable.js +14 -0
  103. package/dist/src/lib/shell-word.js +4 -0
  104. package/dist/src/lib/timezone.js +237 -0
  105. package/dist/src/lib/zone-names.js +114 -0
  106. package/dist/src/web/app.css +76 -0
  107. package/dist/src/web/app.js +491 -0
  108. package/dist/src/web/index.html +44 -0
  109. package/package.json +22 -4
@@ -0,0 +1,567 @@
1
+ /**
2
+ * THE EXECUTOR WORKER (the test server: a JSON read through a Mason function over 22 M rows took 98 s, and
3
+ * for those 98 s the whole process was silent — the other sessions, the stream, the copy, the commands, even the
4
+ * cancel of a client that had left). Every `__mason_*` function is JavaScript, and DuckDB's executor threads call it
5
+ * on the thread whose Node environment REGISTERED it, one vector (2,048 rows) a call; with several executor threads
6
+ * calling, that thread's event loop runs a thousand calls between two turns of anything else. So DuckDB lives on a
7
+ * thread of its own, this module's worker (src/duck/executor-worker.ts): the instance, every connection, the
8
+ * functions; and this process's main thread — the wire listener, the catalog, the commands, the sync's own logic —
9
+ * never runs a function call.
10
+ *
11
+ * WHY EVERYTHING DuckDB MOVES, the writers' connections included: a DuckDB instance is a Node object of the thread that
12
+ * made it, never handed to another, and `DuckDBInstanceCache` is a cache per JavaScript object (each thread's singleton
13
+ * makes its own), so a worker cannot reach the main thread's instance. A SECOND instance of the same file is not
14
+ * refused inside one process (DuckDB's file lock is per process) and loses writes silently: measured, an
15
+ * instance's committed and checkpointed rows were gone once the other checkpointed (docs/DESIGN.md "The executor
16
+ * worker"). One instance per file, on the worker; the sync, the stream, the request log and the tests reach it through
17
+ * `DuckConnection`, a connection's methods posted to the worker (`RemoteConnection`), and the listener's sessions
18
+ * through `RemoteRunner`.
19
+ *
20
+ * WHAT CROSSES: a statement's text and parameters (structured clone), a materialized result's rows as DuckDB's JS and
21
+ * Json forms, a column's DuckDB type as its JSON (`typeFromJson`), a streamed result's rows ENCODED on the worker (the
22
+ * wire's DataRows, src/duck/store.ts encodeRows) and moved as one transferred buffer a chunk, an error as its SQLSTATE
23
+ * and words (a StatementError's fields) or DuckDB's message. A cancel does NOT cross as a message: the worker may be
24
+ * busy for seconds with a statement's calls, so the listener writes the CANCEL TABLE (src/duck/cancel.ts) the calls
25
+ * read, in memory both threads share; the message that follows only does what the calls cannot (an interrupt of a
26
+ * statement that calls none).
27
+ *
28
+ * A WORKER THAT DIES (V8 out of heap on that thread, an uncaught error) fails every request in flight and every later
29
+ * one: a session's statement is told 57P02 with the reason and to reconnect, and the run is told (`onCrash`) — it exits
30
+ * with the reason, and its supervisor starts it again (src/cli/dev.ts, src/cli/supervise.ts); DuckDB recovers the copy
31
+ * from its write-ahead log. A native abort ends the whole process, as it always did. The worker is not restarted in
32
+ * place: every connection the sync holds would be gone under it.
33
+ */
34
+ import { Worker } from "node:worker_threads";
35
+ import { fileURLToPath } from "node:url";
36
+ import * as duck from "@duckdb/node-api";
37
+ import { runnable } from "../lib/runnable.js";
38
+ import { StatementError } from "../api/postgres/server.js";
39
+ import { EncodeError } from "../api/postgres/encode.js";
40
+ import { NumericError } from "../lib/pg-numeric.js";
41
+ import { CANCEL_SLOTS, cancelStatement, cancelTable, resetSlot } from "./cancel.js";
42
+ // a cycle (store.ts opens the executor): read at run time only
43
+ import { failureOf } from "./store.js";
44
+ /** A DuckDB type from its `toJson()` (the worker's), as `@duckdb/node-api`'s own classes. */
45
+ export function typeFromJson(j) {
46
+ const o = j;
47
+ const id = Number(o.typeId);
48
+ const alias = typeof o.alias === "string" ? o.alias : undefined;
49
+ const T = duck.DuckDBTypeId;
50
+ switch (id) {
51
+ case T.DECIMAL: return new duck.DuckDBDecimalType(Number(o.width), Number(o.scale), alias);
52
+ case T.ENUM: return new duck.DuckDBEnumType(o.values, Number(o.internalTypeId), alias);
53
+ case T.LIST: return new duck.DuckDBListType(typeFromJson(o.valueType), alias);
54
+ case T.ARRAY: return new duck.DuckDBArrayType(typeFromJson(o.valueType), Number(o.length), alias);
55
+ case T.MAP: return new duck.DuckDBMapType(typeFromJson(o.keyType), typeFromJson(o.valueType), alias);
56
+ case T.STRUCT: return new duck.DuckDBStructType(o.entryNames, o.entryTypes.map(typeFromJson), alias);
57
+ case T.UNION: return new duck.DuckDBUnionType(o.memberTags, o.memberTypes.map(typeFromJson), alias);
58
+ case T.GEOMETRY: return duck.DuckDBGeometryType.create(alias, typeof o.crs === "string" ? o.crs : undefined);
59
+ }
60
+ const simple = SIMPLE_TYPES.get(id);
61
+ if (simple === undefined)
62
+ throw new Error(`a DuckDB type Mason cannot carry between threads: ${JSON.stringify(j)} (Mason bug: report it)`);
63
+ return simple(alias);
64
+ }
65
+ /** every type of no parameters, by its id: each class's `create(alias)` */
66
+ const SIMPLE_TYPES = new Map(Object.values(duck).flatMap((v) => {
67
+ const c = v;
68
+ return typeof v === "function" && typeof c.instance?.typeId === "number" && typeof c.create === "function"
69
+ ? [[c.instance.typeId, (alias) => c.create(alias)]] : [];
70
+ }));
71
+ /** the class of a DuckDB value of each type id that has one (the rest are plain: a string, a number, a bigint …) */
72
+ const VALUE_CLASS = new Map([
73
+ [duck.DuckDBTypeId.DATE, duck.DuckDBDateValue.prototype], [duck.DuckDBTypeId.TIME, duck.DuckDBTimeValue.prototype],
74
+ [duck.DuckDBTypeId.TIME_NS, duck.DuckDBTimeNSValue.prototype], [duck.DuckDBTypeId.TIME_TZ, duck.DuckDBTimeTZValue.prototype],
75
+ [duck.DuckDBTypeId.TIMESTAMP, duck.DuckDBTimestampValue.prototype], [duck.DuckDBTypeId.TIMESTAMP_S, duck.DuckDBTimestampSecondsValue.prototype],
76
+ [duck.DuckDBTypeId.TIMESTAMP_MS, duck.DuckDBTimestampMillisecondsValue.prototype], [duck.DuckDBTypeId.TIMESTAMP_NS, duck.DuckDBTimestampNanosecondsValue.prototype],
77
+ [duck.DuckDBTypeId.TIMESTAMP_TZ, duck.DuckDBTimestampTZValue.prototype], [duck.DuckDBTypeId.TIMESTAMP_TZ_NS, duck.DuckDBTimestampTZNanosecondsValue.prototype],
78
+ [duck.DuckDBTypeId.INTERVAL, duck.DuckDBIntervalValue.prototype], [duck.DuckDBTypeId.DECIMAL, duck.DuckDBDecimalValue.prototype],
79
+ [duck.DuckDBTypeId.BLOB, duck.DuckDBBlobValue.prototype], [duck.DuckDBTypeId.UUID, duck.DuckDBUUIDValue.prototype],
80
+ [duck.DuckDBTypeId.BIT, duck.DuckDBBitValue.prototype], [duck.DuckDBTypeId.GEOMETRY, duck.DuckDBGeometryValue.prototype],
81
+ [duck.DuckDBTypeId.LIST, duck.DuckDBListValue.prototype], [duck.DuckDBTypeId.ARRAY, duck.DuckDBArrayValue.prototype],
82
+ [duck.DuckDBTypeId.STRUCT, duck.DuckDBStructValue.prototype], [duck.DuckDBTypeId.MAP, duck.DuckDBMapValue.prototype],
83
+ [duck.DuckDBTypeId.UNION, duck.DuckDBUnionValue.prototype],
84
+ ]);
85
+ /** A DuckDB value as structured clone carried it (its fields, every one public, without its class), given its class
86
+ * back — and its parts theirs — by its type. */
87
+ export function valueFromClone(v, t) {
88
+ if (v === null || typeof v !== "object")
89
+ return v;
90
+ if (t.typeId === duck.DuckDBTypeId.VARIANT)
91
+ throw new Error("a VARIANT value cannot be carried between threads (Mason bug: report it)");
92
+ const proto = VALUE_CLASS.get(t.typeId);
93
+ if (proto === undefined)
94
+ return v;
95
+ const o = v;
96
+ if (t instanceof duck.DuckDBListType || t instanceof duck.DuckDBArrayType)
97
+ o.items = o.items.map((x) => valueFromClone(x, t.valueType));
98
+ else if (t instanceof duck.DuckDBStructType) {
99
+ const entries = o.entries;
100
+ t.entryNames.forEach((n, i) => { if (n in entries)
101
+ entries[n] = valueFromClone(entries[n], t.entryTypes[i]); });
102
+ }
103
+ else if (t instanceof duck.DuckDBMapType) {
104
+ o.entries = o.entries.map((e) => ({ key: valueFromClone(e.key, t.keyType), value: valueFromClone(e.value, t.valueType) }));
105
+ }
106
+ else if (t instanceof duck.DuckDBUnionType) {
107
+ const at = t.memberTags.indexOf(String(o.tag));
108
+ if (at >= 0)
109
+ o.value = valueFromClone(o.value, t.memberTypes[at]);
110
+ }
111
+ return Object.setPrototypeOf(o, proto);
112
+ }
113
+ export function wireError(e) {
114
+ if (e instanceof StatementError)
115
+ return { kind: "statement", fields: e.fields, sql: e.sql };
116
+ if (e instanceof EncodeError)
117
+ return { kind: "encode", message: e.message, code: e.code };
118
+ if (e instanceof NumericError)
119
+ return { kind: "numeric", message: e.message, sqlstate: e.sqlstate };
120
+ return { kind: "error", message: e instanceof Error ? e.message : String(e), name: e instanceof Error ? e.name : "Error" };
121
+ }
122
+ /** the worker is gone: what every request in flight, and every later one, is answered. `stopped`: by the run's own stop
123
+ * (`close`), not a crash */
124
+ export class ExecutorGone extends Error {
125
+ stopped;
126
+ constructor(reason, stopped = false) { super(reason); this.stopped = stopped; }
127
+ }
128
+ /** What a session's statement is told when the run stops under it (mason stop, a shutdown): Postgres's own words for a
129
+ * server shutting down (57P01), from the worker's stop (executor-worker.ts `shutdown`) or the closed executor alike */
130
+ export const STOPPING = { code: "57P01", message: "terminating connection due to administrator command",
131
+ detail: "Mason is stopping (mason stop, or a shutdown)", hint: "connect again once Mason runs again (mason status says whether it does)" };
132
+ const errorOf = (e) => {
133
+ if (e.kind === "statement")
134
+ return new StatementError(e.fields, e.sql);
135
+ if (e.kind === "encode")
136
+ return new EncodeError(e.message, e.code);
137
+ if (e.kind === "numeric")
138
+ return new NumericError(e.message, e.sqlstate);
139
+ const err = new Error(e.message);
140
+ err.name = e.name;
141
+ return err;
142
+ };
143
+ /** a value a statement may be given across threads: plain, never a DuckDB value object (its class would be lost) */
144
+ function plainValues(values) {
145
+ if (values === undefined)
146
+ return undefined;
147
+ for (const v of values) {
148
+ if (v === null || typeof v !== "object" || Array.isArray(v) || ArrayBuffer.isView(v) || v instanceof Date)
149
+ continue;
150
+ if (Object.getPrototypeOf(v) !== Object.prototype)
151
+ throw new Error(`a ${v.constructor.name} cannot be sent to DuckDB's thread: pass its text or a plain value (Mason bug: report it)`);
152
+ }
153
+ return [...values];
154
+ }
155
+ /**
156
+ * EVERY CALL TO THE WORKER SETTLES (under load the worker deadlocked inside DuckDB, src/duck/store.ts
157
+ * `cleanUp`, and every request to it — the session's statements, the store's close, the test's after-hook — waited for
158
+ * ever, 0 % CPU). Two bounds:
159
+ * - THE WORKER MUST BE HEARD: while a request is pending the executor pings it every `PING_MS`, and a worker that has
160
+ * said nothing (no answer, no log line, no pong) for `SILENT_MS` is declared stopped — every request in flight and
161
+ * later is told 57P02, and the run is told (`onCrash`), as for a crash. Not shorter: a statement calling a Mason
162
+ * function holds the worker's turn for up to a thousand calls at a time (seconds on a loaded machine), and closing a
163
+ * call-free result waits for DuckDB to honour an interrupt (~10 s for one join, docs/DESIGN.md "The executor
164
+ * worker"); a deadlock is for ever, so a longer bound only says it later.
165
+ * - THE CLOSE IS BOUNDED: `close` gives the worker's shutdown `CLOSE_GRACE_MS` (a calling statement stops at its next
166
+ * call, milliseconds; a call-free one at DuckDB's interrupt; and the whole stop must fit inside a platform's 10 s
167
+ * before SIGKILL and `mason stop`'s 15 s), then declares it stopped the same way and resolves; the thread is ended
168
+ * once its shutdown answers (below).
169
+ * What neither can do: end a thread blocked inside native code. Such a process does not exit by itself, nor on
170
+ * process.exit (measured); its supervisor's SIGKILL does. The copy is not opened again in this process
171
+ * (`abandoned`): a second instance of the file would lose writes.
172
+ *
173
+ * NOTHING ENDS THE WORKER'S JAVASCRIPT UNDER A PENDING CALL OF DuckDB'S (a test file under load aborted at its exit,
174
+ * `libc++abi: terminating due to uncaught exception of type Napi::Error`: its close outlasted the grace, and the
175
+ * executor TERMINATED the worker while DuckDB's threads were inside a Mason function call — the node API's callback met
176
+ * a dying environment and threw where nothing catches. Reproduced every time by a close with a 1 ms grace under a
177
+ * calling statement, tests/duck-executor.test.ts). So a worker given up on (the grace, the silence) is told nothing
178
+ * more and asked to SHUT DOWN (`shutDown`: posted once, its answer awaited outside `pending`), and is ended only once
179
+ * that shutdown has answered — DuckDB's statements stopped, its connections and instance closed — or it exits by
180
+ * itself; until then it stays REFERENCED, so the process cannot end with the worker mid-call either (a process
181
+ * exiting tears its workers down the same way). A drained answer is a request for the same reason (`answer.close`):
182
+ * DuckDB may call into the worker after the listener let go of the statement. A thread that never answers its
183
+ * shutdown is the deadlock above: its supervisor's SIGKILL.
184
+ */
185
+ export const PING_MS = 5_000;
186
+ export const SILENT_MS = 60_000;
187
+ export const CLOSE_GRACE_MS = 5_000;
188
+ export class Executor {
189
+ worker;
190
+ log;
191
+ pending = new Map();
192
+ nextId = 1;
193
+ /** why the worker is gone, once it is */
194
+ gone = null;
195
+ closing = false;
196
+ exited;
197
+ crashed = [];
198
+ /** the cancel table, shared with the worker (src/duck/cancel.ts), and its free slots */
199
+ cancels = cancelTable();
200
+ free = Array.from({ length: CANCEL_SLOTS }, (_, i) => CANCEL_SLOTS - 1 - i);
201
+ /** when the worker last said anything (performance.now()) */
202
+ heard = performance.now();
203
+ watch;
204
+ silentMs;
205
+ /** why the worker was given up on without exiting (`close`'s bound, the silence), once it was: its instance may
206
+ * still hold the file, which this process must never open again */
207
+ abandoned = null;
208
+ /** the shutdown, once posted (`shutDown`): settled when the worker answered it, or exited */
209
+ shutdown = null;
210
+ shutdownId = 0;
211
+ shutdownAnswered = () => undefined;
212
+ hasExited = false;
213
+ constructor(o) {
214
+ this.silentMs = o.silentMs ?? SILENT_MS;
215
+ this.log = o.log;
216
+ this.worker = new Worker(runnable(o.workerFile ?? fileURLToPath(new URL("./executor-worker.ts", import.meta.url))), { workerData: { cancels: this.cancels } });
217
+ // the process may end while nothing is asked of it (a script, a test that forgot to close): referenced only while
218
+ // a request is in flight (`call`)
219
+ this.worker.unref();
220
+ this.worker.on("message", (m) => {
221
+ this.heard = performance.now();
222
+ if ("pong" in m)
223
+ return;
224
+ if ("log" in m) {
225
+ this.log(m.log);
226
+ return;
227
+ }
228
+ if (m.id === this.shutdownId) {
229
+ if (!m.ok)
230
+ this.log(`closing the copy failed: ${errorOf(m.error).message}`);
231
+ this.shutdownAnswered();
232
+ return;
233
+ }
234
+ const p = this.pending.get(m.id);
235
+ if (p === undefined)
236
+ return;
237
+ this.settle(m.id);
238
+ if (m.ok)
239
+ p.resolve(m.value);
240
+ else
241
+ p.reject(errorOf(m.error));
242
+ });
243
+ let why = null;
244
+ this.worker.on("error", (e) => {
245
+ why = e.code === "ERR_WORKER_OUT_OF_MEMORY" ? `its thread ran out of JavaScript heap (${e.message})` : `it failed: ${e.message}`;
246
+ });
247
+ this.exited = new Promise((resolve) => this.worker.once("exit", (code) => {
248
+ this.hasExited = true;
249
+ this.die(this.closing && why === null ? "it was closed" : why ?? `it exited with code ${code}`);
250
+ this.shutdownAnswered();
251
+ resolve();
252
+ }));
253
+ // the watch (see SILENT_MS): only while a request is pending — an idle worker has nothing to answer
254
+ this.watch = setInterval(() => {
255
+ if (this.gone !== null || this.pending.size === 0) {
256
+ this.heard = performance.now();
257
+ return;
258
+ }
259
+ if (performance.now() - this.heard > this.silentMs) {
260
+ this.abandon(`it answered nothing for ${Math.round(this.silentMs / 1000)} s with ${this.pending.size} request(s) waiting (stuck inside DuckDB: Mason bug, report it with mason logs)`);
261
+ return;
262
+ }
263
+ this.worker.postMessage({ op: "ping" });
264
+ }, Math.min(PING_MS, this.silentMs / 4));
265
+ this.watch.unref();
266
+ }
267
+ /** the worker given up on without its exit: every request told so (57P02), the run told, and its shutdown asked —
268
+ * NEVER a terminate, which under a pending call of DuckDB's aborts the process (NOTHING ENDS THE WORKER'S
269
+ * JAVASCRIPT above): it is ended once its shutdown answers, and holds the process until then */
270
+ abandon(reason) {
271
+ if (this.gone !== null)
272
+ return;
273
+ this.abandoned = reason;
274
+ this.die(reason, false);
275
+ void this.shutDown();
276
+ }
277
+ /** Asks the worker to stop (executor-worker.ts `shutdown`: every statement stopped, every connection and the instance
278
+ * closed once nothing runs on them), once; settles when it answered, or exited. Only then is it terminated: nothing
279
+ * of DuckDB's calls into it any more. The worker stays referenced meanwhile, so the process waits for it. */
280
+ shutDown() {
281
+ if (this.shutdown !== null)
282
+ return this.shutdown;
283
+ this.shutdown = new Promise((resolve) => { this.shutdownAnswered = resolve; });
284
+ if (this.hasExited)
285
+ this.shutdownAnswered();
286
+ else {
287
+ this.shutdownId = this.nextId++;
288
+ this.worker.ref();
289
+ this.worker.postMessage({ op: "shutdown", id: this.shutdownId });
290
+ }
291
+ void this.shutdown.then(async () => { if (!this.hasExited)
292
+ await this.worker.terminate(); })
293
+ .catch((e) => this.log(`ending the copy's thread failed: ${e.message}`));
294
+ return this.shutdown;
295
+ }
296
+ static async start(o) {
297
+ const e = new Executor(o);
298
+ try {
299
+ await e.call({ op: "open", file: o.file, settings: o.settings });
300
+ }
301
+ catch (err) {
302
+ await e.close();
303
+ throw err;
304
+ }
305
+ return e;
306
+ }
307
+ /** called once if the worker dies while the run uses it (never after `close`) */
308
+ onCrash(f) { this.crashed.push(f); }
309
+ die(reason, stopped = this.closing) {
310
+ if (this.gone !== null)
311
+ return;
312
+ this.gone = reason;
313
+ clearInterval(this.watch);
314
+ const err = new ExecutorGone(reason, stopped);
315
+ for (const [id, p] of [...this.pending]) {
316
+ this.settle(id);
317
+ p.reject(err);
318
+ }
319
+ if (!stopped) {
320
+ this.log(`the copy's engine stopped: ${reason}`);
321
+ if (!this.closing)
322
+ for (const f of this.crashed)
323
+ f(reason);
324
+ }
325
+ }
326
+ settle(id) {
327
+ this.pending.delete(id);
328
+ // a shutdown in flight holds the process until the worker ends (`shutDown`)
329
+ if (this.pending.size === 0 && this.shutdown === null)
330
+ this.worker.unref();
331
+ }
332
+ /** a request, answered by the worker */
333
+ call(m, transfer = []) {
334
+ if (this.gone !== null)
335
+ return Promise.reject(new ExecutorGone(this.gone, this.closing));
336
+ const id = this.nextId++;
337
+ return new Promise((resolve, reject) => {
338
+ if (this.pending.size === 0)
339
+ this.worker.ref();
340
+ this.pending.set(id, { resolve: resolve, reject });
341
+ this.worker.postMessage({ ...m, id }, transfer);
342
+ });
343
+ }
344
+ /** a message needing no answer (an interrupt, a connection's close); nothing once the worker is gone */
345
+ post(m) {
346
+ if (this.gone === null)
347
+ this.worker.postMessage(m);
348
+ }
349
+ /** a request whose answer nobody awaits (an answer's close: the worker answers once DuckDB handed it back), kept in
350
+ * `pending` so the process does not end while the worker still drains it; a failure is logged, a stop is not one */
351
+ background(m) {
352
+ this.call(m).catch((e) => { if (!(e instanceof ExecutorGone))
353
+ this.log(`${String(m.op)} failed: ${e.message}`); });
354
+ }
355
+ /** a connection of the instance */
356
+ async connect() { return new RemoteConnection(this, await this.call({ op: "connect" })); }
357
+ writer() { return new RemoteConnection(this, 0); }
358
+ /** the libc order's two functions (src/duck/functions.ts registerLibcOrder), registered on the worker */
359
+ async registerLibcOrder(order) { await this.call({ op: "libc", order }); }
360
+ /** how many vectors each Mason function was called with so far (src/duck/functions.ts FUNCTION_CALLS: the worker's
361
+ * count, the functions being its) — what a test or a measurement reads to know whether a statement reached one */
362
+ async functionCalls() { return new Map(Object.entries(await this.call({ op: "calls" }))); }
363
+ /** how many rows each Mason function was handed so far, across its calls (src/duck/functions.ts FUNCTION_ROWS) */
364
+ async functionRows() { return new Map(Object.entries(await this.call({ op: "function rows" }))); }
365
+ /** a wire session's connection: a slot of the cancel table, its connection on the worker */
366
+ async runner() {
367
+ const slot = this.free.pop();
368
+ if (slot === undefined)
369
+ throw new StatementError({ code: "53300", message: `too many sessions are reading the copy at once (${CANCEL_SLOTS})`, hint: "close idle connections (a pool's size), then connect again" });
370
+ resetSlot(this.cancels, slot);
371
+ try {
372
+ return new RemoteRunner(this, await this.call({ op: "runner.open", slot }), slot);
373
+ }
374
+ catch (e) {
375
+ this.free.push(slot);
376
+ throw e instanceof ExecutorGone ? goneError(e, "") : e;
377
+ }
378
+ }
379
+ release(slot) { this.free.push(slot); }
380
+ /** Closes every connection and the instance on the worker, then the worker; resolves once it has exited, or once
381
+ * `graceMs` has passed (CLOSE_GRACE_MS says why), whatever the worker does. Never rejects: a failure is said in the
382
+ * log. */
383
+ close(graceMs = CLOSE_GRACE_MS) {
384
+ this.closed ??= this.closeWithin(graceMs);
385
+ return this.closed;
386
+ }
387
+ closed = null;
388
+ async closeWithin(graceMs) {
389
+ this.closing = true;
390
+ let timer;
391
+ const late = new Promise((resolve) => { timer = setTimeout(() => resolve("late"), graceMs); });
392
+ // the shutdown, then the terminate once it answered (`shutDown`), then the exit — or the worker's own exit
393
+ const closed = (async () => { await this.shutDown(); await this.exited; return "closed"; })();
394
+ try {
395
+ if (await Promise.race([closed, late]) === "late") {
396
+ this.log(`the copy's thread did not close within ${graceMs / 1000} s: given up on (its sessions are told 57P02); it ends once its shutdown finishes, and the process waits for it (a thread stuck inside DuckDB needs a SIGKILL)`);
397
+ this.abandon(`it did not close within ${graceMs / 1000} s`);
398
+ }
399
+ }
400
+ finally {
401
+ clearTimeout(timer);
402
+ }
403
+ }
404
+ }
405
+ // ------------------------------------------------------------------------------------------------- a connection
406
+ class RemoteConnection {
407
+ ex;
408
+ conn;
409
+ constructor(ex, conn) { this.ex = ex; this.conn = conn; }
410
+ async run(sql, values) {
411
+ return this.ex.call({ op: "conn.run", conn: this.conn, sql, values: plainValues(values) });
412
+ }
413
+ async runAndReadAll(sql, values) {
414
+ const r = await this.ex.call({ op: "conn.read", conn: this.conn, sql, values: plainValues(values) });
415
+ const types = r.types.map(typeFromJson);
416
+ let rows = null;
417
+ return {
418
+ columnType: (i) => types[i],
419
+ columnNames: r.names,
420
+ getRowsJS: () => r.js,
421
+ getRowsJson: () => r.json,
422
+ getRows: () => (rows ??= r.rows.map((row) => row.map((v, i) => valueFromClone(v, types[i])))),
423
+ getRowObjectsJson: () => r.json.map((row) => Object.fromEntries(r.names.map((n, i) => [n, row[i]]))),
424
+ };
425
+ }
426
+ async prepare(sql) {
427
+ const r = await this.ex.call({ op: "conn.prepare", conn: this.conn, sql });
428
+ const types = r.types.map(typeFromJson);
429
+ return { columnCount: r.names.length, columnName: (i) => r.names[i], columnType: (i) => types[i], destroySync: () => undefined };
430
+ }
431
+ interrupt() { this.ex.post({ op: "conn.interrupt", conn: this.conn }); }
432
+ closeSync() { this.ex.post({ op: "conn.close", conn: this.conn }); }
433
+ }
434
+ /** the reason an abort carries across: the statement timeout's fields, else none (the client's cancel) */
435
+ const reasonOf = (signal) => !signal.aborted ? null : signal.reason instanceof StatementError ? { fields: signal.reason.fields } : {};
436
+ /** what a session is told when the worker is gone mid-statement */
437
+ function goneError(e, sql) {
438
+ if (e.stopped)
439
+ return new StatementError(STOPPING, sql);
440
+ return new StatementError({ code: "57P02", message: "terminating connection because the copy's engine stopped", detail: e.message,
441
+ hint: "Mason starts again by itself: reconnect in a few seconds and repeat the statement (mason status says when it is back)" }, sql);
442
+ }
443
+ /**
444
+ * A wire session's connection on the worker (src/duck/store.ts SessionRunner runs there). Each statement has a number;
445
+ * its cancel is written to the cancel table AT ONCE when it calls a Mason function, then posted for the rest. Its
446
+ * failure is read against THIS thread's signal (`failureOf`), so a timeout is said as the timeout whichever reached the
447
+ * worker first, the abort or the cancelled call's error.
448
+ */
449
+ class RemoteRunner {
450
+ ex;
451
+ id;
452
+ slot;
453
+ seq = 0;
454
+ closed = false;
455
+ constructor(ex, id, slot) { this.ex = ex; this.id = id; this.slot = slot; }
456
+ /** a statement's abort, from the moment it is asked until `end`: written to the cancel table at once when the plan
457
+ * says it calls a Mason function (`PlannedSql.functions`, never read off its text), then posted for the rest */
458
+ watch(seq, sql, signal) {
459
+ const calls = sql.functions.length > 0;
460
+ const abort = () => {
461
+ if (calls)
462
+ cancelStatement(this.ex.cancels, this.slot, seq);
463
+ this.ex.post({ op: "runner.abort", runner: this.id, seq, reason: reasonOf(signal) });
464
+ };
465
+ signal.addEventListener("abort", abort);
466
+ if (signal.aborted && calls)
467
+ cancelStatement(this.ex.cancels, this.slot, seq);
468
+ return () => signal.removeEventListener("abort", abort);
469
+ }
470
+ failure(signal, e, sql) {
471
+ if (e instanceof ExecutorGone)
472
+ return goneError(e, sql.text);
473
+ // a cancelled statement's failure read under THIS signal (the same function as the worker's, src/duck/store.ts),
474
+ // since the cancelled call's error may reach the worker before the abort does; any other failure as the worker
475
+ // threw it (an EncodeError is the listener's to word)
476
+ return signal.aborted ? failureOf(signal, e, sql.text) : e;
477
+ }
478
+ async run(sql, params, session, signal, fields = null, exact = []) {
479
+ const seq = ++this.seq;
480
+ const unwatch = this.watch(seq, sql, signal);
481
+ let columns;
482
+ try {
483
+ columns = await this.ex.call({ op: "runner.run", runner: this.id, seq, sql, params, session, fields, exact, aborted: reasonOf(signal) });
484
+ }
485
+ catch (e) {
486
+ unwatch();
487
+ throw this.failure(signal, e, sql);
488
+ }
489
+ let ended = false;
490
+ const end = () => { if (!ended) {
491
+ ended = true;
492
+ unwatch();
493
+ } };
494
+ const calls = sql.functions.length > 0;
495
+ return {
496
+ columns,
497
+ fetch: async (formats, s) => {
498
+ if (ended)
499
+ return null;
500
+ let got;
501
+ try {
502
+ got = await this.ex.call({ op: "answer.fetch", runner: this.id, seq, formats, timeZone: s.timeZone });
503
+ }
504
+ catch (e) {
505
+ end();
506
+ throw this.failure(signal, e, sql);
507
+ }
508
+ // A STATEMENT WHOSE CANCEL WAS ASKED IS NEVER "ok" (a review finding): a cancel or a timeout that landed
509
+ // after its last function call, or after DuckDB computed its last chunk, found nothing left to stop, and the
510
+ // result came back whole — the client is told 57014 all the same, as THIS thread's signal says it (the timeout's
511
+ // words, or the cancel's)
512
+ if (got === null) {
513
+ end();
514
+ if (signal.aborted)
515
+ throw failureOf(signal, null, sql.text);
516
+ return null;
517
+ }
518
+ const all = Buffer.from(got.bytes);
519
+ return got.ends.map((to, i) => all.subarray(i === 0 ? 0 : got.ends[i - 1], to));
520
+ },
521
+ close: () => {
522
+ if (ended)
523
+ return;
524
+ end();
525
+ // its calls see the cancel now; the worker cancels the rest (an interrupt) and releases it once DuckDB hands it
526
+ // back — answered then, so the process never ends while DuckDB still calls into the worker for it
527
+ if (calls)
528
+ cancelStatement(this.ex.cancels, this.slot, seq);
529
+ this.ex.background({ op: "answer.close", runner: this.id, seq });
530
+ },
531
+ };
532
+ }
533
+ async explain(sql, params, session, signal, how) {
534
+ const seq = ++this.seq;
535
+ const unwatch = this.watch(seq, sql, signal);
536
+ let lines;
537
+ try {
538
+ lines = await this.ex.call({ op: "runner.explain", runner: this.id, seq, sql, params, session, how, aborted: reasonOf(signal) });
539
+ }
540
+ catch (e) {
541
+ throw this.failure(signal, e, sql);
542
+ }
543
+ finally {
544
+ unwatch();
545
+ }
546
+ // as `fetch`'s end: an EXPLAIN ANALYZE whose cancel was asked is never answered
547
+ if (signal.aborted)
548
+ throw failureOf(signal, null, sql.text);
549
+ return lines;
550
+ }
551
+ /** closes the connection once its answers are released (the worker waits for them: never under the executor) */
552
+ async close() {
553
+ if (this.closed)
554
+ return;
555
+ this.closed = true;
556
+ try {
557
+ await this.ex.call({ op: "runner.close", runner: this.id });
558
+ }
559
+ catch (e) {
560
+ if (!(e instanceof ExecutorGone))
561
+ throw e;
562
+ }
563
+ finally {
564
+ this.ex.release(this.slot);
565
+ }
566
+ }
567
+ }