@mason-data/cli 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -73
- package/dist/src/api/error.js +3 -0
- package/dist/src/api/postgres/catalog-worker.js +49 -2
- package/dist/src/api/postgres/catalog.js +368 -25
- package/dist/src/api/postgres/encode.js +3 -1
- package/dist/src/api/postgres/explain.js +3 -2
- package/dist/src/api/postgres/listener.js +11 -3
- package/dist/src/api/postgres/protocol.js +39 -23
- package/dist/src/api/postgres/proxy.js +283 -0
- package/dist/src/api/postgres/schema.js +6 -0
- package/dist/src/api/postgres/server.js +1617 -245
- package/dist/src/api/postgres/translate.js +127 -10
- package/dist/src/api/request-log.js +18 -5
- package/dist/src/cdc/poll.js +5 -5
- package/dist/src/cli/background.js +47 -25
- package/dist/src/cli/bridge-stdin.js +101 -0
- package/dist/src/cli/cancel.js +75 -0
- package/dist/src/cli/command.js +170 -11
- package/dist/src/cli/commands.js +804 -209
- package/dist/src/cli/control.js +59 -0
- package/dist/src/cli/credentials.js +182 -0
- package/dist/src/cli/demo.js +20 -11
- package/dist/src/cli/deploy.js +574 -0
- package/dist/src/cli/dev.js +491 -130
- package/dist/src/cli/doctor.js +131 -38
- package/dist/src/cli/generated-files.js +143 -0
- package/dist/src/cli/guides.js +175 -36
- package/dist/src/cli/hosted.js +850 -0
- package/dist/src/cli/init.js +162 -65
- package/dist/src/cli/login.js +289 -0
- package/dist/src/cli/machine.js +13 -12
- package/dist/src/cli/main.js +11 -5
- package/dist/src/cli/mason.cjs +17 -0
- package/dist/src/cli/mcp.js +31 -22
- package/dist/src/cli/metrics.js +79 -0
- package/dist/src/cli/output-gate.js +176 -0
- package/dist/src/cli/page-share.js +96 -0
- package/dist/src/cli/pg-tls.js +14 -4
- package/dist/src/cli/pg-url.js +4 -0
- package/dist/src/cli/pin.js +19 -14
- package/dist/src/cli/pool-size.cjs +85 -0
- package/dist/src/cli/ports.js +2 -0
- package/dist/src/cli/preflight.js +138 -46
- package/dist/src/cli/project.js +142 -4
- package/dist/src/cli/readiness.js +76 -6
- package/dist/src/cli/registry.js +65 -38
- package/dist/src/cli/replica-hosted.js +665 -0
- package/dist/src/cli/replica.js +76 -0
- package/dist/src/cli/replication-checks.js +7 -5
- package/dist/src/cli/replication-mode.js +105 -26
- package/dist/src/cli/request-filter.js +134 -0
- package/dist/src/cli/roles.js +585 -0
- package/dist/src/cli/run-server.js +637 -48
- package/dist/src/cli/runs.js +1 -1
- package/dist/src/cli/security.js +67 -18
- package/dist/src/cli/serve.js +204 -46
- package/dist/src/cli/share-run.js +329 -0
- package/dist/src/cli/share.js +286 -0
- package/dist/src/cli/source-edit.js +75 -17
- package/dist/src/cli/source-role.js +344 -0
- package/dist/src/cli/sources.js +306 -47
- package/dist/src/cli/style.js +2 -1
- package/dist/src/cli/supervise.js +27 -24
- package/dist/src/cli/sync.js +55 -25
- package/dist/src/cli/token-hosted.js +337 -0
- package/dist/src/cli/tunnel.js +200 -0
- package/dist/src/cli/ui-ssh.js +460 -0
- package/dist/src/cli/ui.js +124 -17
- package/dist/src/cli/usage.js +128 -0
- package/dist/src/cli/user-hosted.js +299 -0
- package/dist/src/cli/user.js +573 -65
- package/dist/src/cli/version.js +9 -7
- package/dist/src/demo/seed.js +29 -0
- package/dist/src/duck/analyze.js +36 -20
- package/dist/src/duck/cancel.js +10 -1
- package/dist/src/duck/collation.js +266 -0
- package/dist/src/duck/constant-key.js +94 -0
- package/dist/src/duck/distinct-key.js +283 -26
- package/dist/src/duck/events.js +165 -0
- package/dist/src/duck/executor-worker.js +20 -11
- package/dist/src/duck/executor.js +47 -33
- package/dist/src/duck/explain.js +60 -46
- package/dist/src/duck/extensions.js +160 -0
- package/dist/src/duck/functions.js +461 -63
- package/dist/src/duck/gaps.js +48 -21
- package/dist/src/duck/generated-plan.js +1070 -0
- package/dist/src/duck/generated.js +906 -0
- package/dist/src/duck/grants.js +176 -0
- package/dist/src/duck/held-types.js +64 -0
- package/dist/src/duck/history.js +127 -0
- package/dist/src/duck/jsonb-order.js +9 -12
- package/dist/src/duck/kept-route.js +6 -5
- package/dist/src/duck/kept.js +15 -13
- package/dist/src/duck/late.js +1469 -40
- package/dist/src/duck/libc-bound.js +341 -0
- package/dist/src/duck/libc-order.js +892 -126
- package/dist/src/duck/pg-eval-worker.js +7 -1
- package/dist/src/duck/pg-eval.js +6 -4
- package/dist/src/duck/pgtree.js +774 -23
- package/dist/src/duck/plan.js +369 -47
- package/dist/src/duck/publication.js +20 -6
- package/dist/src/duck/requests.js +269 -93
- package/dist/src/duck/resolved.js +117 -2
- package/dist/src/duck/rewrite.js +4584 -580
- package/dist/src/duck/row-filter.js +655 -0
- package/dist/src/duck/statement.js +19 -0
- package/dist/src/duck/store.js +439 -177
- package/dist/src/duck/stream.js +19 -10
- package/dist/src/duck/sync.js +1411 -368
- package/dist/src/duck/token.js +4 -1
- package/dist/src/duck/types.js +69 -12
- package/dist/src/duck/usage.js +326 -0
- package/dist/src/duck/users.js +69 -15
- package/dist/src/duck/verified.js +161 -1
- package/dist/src/duck/views.js +5 -5
- package/dist/src/lib/database-name.js +62 -0
- package/dist/src/lib/database-url.js +55 -10
- package/dist/src/lib/dotenv.js +17 -4
- package/dist/src/lib/instant.js +50 -0
- package/dist/src/lib/log-buffer.js +77 -0
- package/dist/src/lib/log-read.js +196 -0
- package/dist/src/lib/pg-numeric.js +390 -49
- package/dist/src/lib/project-version.js +23 -23
- package/dist/src/lib/secret-mask.js +142 -0
- package/dist/src/lib/table-sync.js +282 -0
- package/dist/src/web/app.css +706 -66
- package/dist/src/web/app.js +331 -409
- package/dist/src/web/charts.js +229 -0
- package/dist/src/web/index.html +82 -18
- package/dist/src/web/pages/queries.js +192 -0
- package/dist/src/web/pages/status.js +611 -0
- package/dist/src/web/pages/syncs.js +155 -0
- package/dist/src/web/pages/tables.js +439 -0
- package/dist/src/web/pages/usage.js +397 -0
- package/dist/src/web/pages/users.js +219 -0
- package/dist/src/web/shared.js +584 -0
- package/dist/src/web/vendor/README.md +25 -0
- package/dist/src/web/vendor/geist-1.7.2/Geist-Variable.woff2 +0 -0
- package/dist/src/web/vendor/geist-1.7.2/GeistMono-Variable.woff2 +0 -0
- package/dist/src/web/vendor/geist-1.7.2/LICENSE.txt +92 -0
- package/dist/src/web/vendor/lucide-1.56.0/LICENSE.txt +43 -0
- package/dist/src/web/vendor/lucide-1.56.0/chart-column.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/layout-dashboard.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/logs.mjs +34 -0
- package/dist/src/web/vendor/lucide-1.56.0/menu.mjs +28 -0
- package/dist/src/web/vendor/lucide-1.56.0/square-terminal.mjs +28 -0
- package/dist/src/web/vendor/lucide-1.56.0/table.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/users.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/x.mjs +27 -0
- package/dist/src/web/vendor/uplot-1.6.32/LICENSE.txt +21 -0
- package/dist/src/web/vendor/uplot-1.6.32/uPlot.esm.js +6140 -0
- package/dist/src/web/vendor/uplot-1.6.32/uPlot.min.css +1 -0
- package/dist/src/web/vendor/zilla-slab-5.3.0/LICENSE.txt +93 -0
- package/dist/src/web/vendor/zilla-slab-5.3.0/zilla-slab-latin-700-normal.woff2 +0 -0
- package/package.json +12 -4
|
@@ -35,10 +35,10 @@ import { Worker } from "node:worker_threads";
|
|
|
35
35
|
import { fileURLToPath } from "node:url";
|
|
36
36
|
import * as duck from "@duckdb/node-api";
|
|
37
37
|
import { runnable } from "../lib/runnable.js";
|
|
38
|
-
import { StatementError } from "
|
|
38
|
+
import { StatementError } from "./statement.js";
|
|
39
39
|
import { EncodeError } from "../api/postgres/encode.js";
|
|
40
40
|
import { NumericError } from "../lib/pg-numeric.js";
|
|
41
|
-
import { CANCEL_SLOTS, cancelStatement, cancelTable, resetSlot } from "./cancel.js";
|
|
41
|
+
import { beats, CANCEL_SLOTS, cancelStatement, cancelTable, resetSlot } from "./cancel.js";
|
|
42
42
|
// a cycle (store.ts opens the executor): read at run time only
|
|
43
43
|
import { failureOf } from "./store.js";
|
|
44
44
|
/** A DuckDB type from its `toJson()` (the worker's), as `@duckdb/node-api`'s own classes. */
|
|
@@ -59,7 +59,7 @@ export function typeFromJson(j) {
|
|
|
59
59
|
}
|
|
60
60
|
const simple = SIMPLE_TYPES.get(id);
|
|
61
61
|
if (simple === undefined)
|
|
62
|
-
throw new Error(`a
|
|
62
|
+
throw new Error(`a type of the engine's that Mason cannot carry between threads: ${JSON.stringify(j)} (Mason bug: report it)`);
|
|
63
63
|
return simple(alias);
|
|
64
64
|
}
|
|
65
65
|
/** every type of no parameters, by its id: each class's `create(alias)` */
|
|
@@ -112,11 +112,11 @@ export function valueFromClone(v, t) {
|
|
|
112
112
|
}
|
|
113
113
|
export function wireError(e) {
|
|
114
114
|
if (e instanceof StatementError)
|
|
115
|
-
return { kind: "statement", fields: e.fields, sql: e.sql };
|
|
115
|
+
return { kind: "statement", fields: e.fields, sql: e.sql, gap: e.gap };
|
|
116
116
|
if (e instanceof EncodeError)
|
|
117
|
-
return { kind: "encode", message: e.message, code: e.code };
|
|
117
|
+
return { kind: "encode", message: e.message, code: e.code, gap: e.gap };
|
|
118
118
|
if (e instanceof NumericError)
|
|
119
|
-
return { kind: "numeric", message: e.message, sqlstate: e.sqlstate };
|
|
119
|
+
return { kind: "numeric", message: e.message, sqlstate: e.sqlstate, gap: e.gap, detail: e.detail };
|
|
120
120
|
return { kind: "error", message: e instanceof Error ? e.message : String(e), name: e instanceof Error ? e.name : "Error" };
|
|
121
121
|
}
|
|
122
122
|
/** the worker is gone: what every request in flight, and every later one, is answered. `stopped`: by the run's own stop
|
|
@@ -131,11 +131,11 @@ export const STOPPING = { code: "57P01", message: "terminating connection due to
|
|
|
131
131
|
detail: "Mason is stopping (mason stop, or a shutdown)", hint: "connect again once Mason runs again (mason status says whether it does)" };
|
|
132
132
|
const errorOf = (e) => {
|
|
133
133
|
if (e.kind === "statement")
|
|
134
|
-
return new StatementError(e.fields, e.sql);
|
|
134
|
+
return new StatementError(e.fields, e.sql, e.gap);
|
|
135
135
|
if (e.kind === "encode")
|
|
136
|
-
return new EncodeError(e.message, e.code);
|
|
136
|
+
return new EncodeError(e.message, e.code, e.gap);
|
|
137
137
|
if (e.kind === "numeric")
|
|
138
|
-
return new NumericError(e.message, e.sqlstate);
|
|
138
|
+
return new NumericError(e.message, e.sqlstate, e.gap, e.detail);
|
|
139
139
|
const err = new Error(e.message);
|
|
140
140
|
err.name = e.name;
|
|
141
141
|
return err;
|
|
@@ -148,7 +148,7 @@ function plainValues(values) {
|
|
|
148
148
|
if (v === null || typeof v !== "object" || Array.isArray(v) || ArrayBuffer.isView(v) || v instanceof Date)
|
|
149
149
|
continue;
|
|
150
150
|
if (Object.getPrototypeOf(v) !== Object.prototype)
|
|
151
|
-
throw new Error(`a ${v.constructor.name} cannot be sent to
|
|
151
|
+
throw new Error(`a ${v.constructor.name} cannot be sent to the engine's thread: pass its text or a plain value (Mason bug: report it)`);
|
|
152
152
|
}
|
|
153
153
|
return [...values];
|
|
154
154
|
}
|
|
@@ -156,12 +156,17 @@ function plainValues(values) {
|
|
|
156
156
|
* EVERY CALL TO THE WORKER SETTLES (under load the worker deadlocked inside DuckDB, src/duck/store.ts
|
|
157
157
|
* `cleanUp`, and every request to it — the session's statements, the store's close, the test's after-hook — waited for
|
|
158
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
|
|
160
|
-
*
|
|
161
|
-
* later is told 57P02, and the run is told (`onCrash`), as for a crash.
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
159
|
+
* - THE WORKER MUST BE HEARD: while a request is pending the executor pings it every `PING_MS`, and a worker of which
|
|
160
|
+
* nothing was heard (no answer, no log line, no pong, no beat) for `SILENT_MS` is declared stopped — every request in
|
|
161
|
+
* flight and later is told 57P02, and the run is told (`onCrash`), as for a crash. THE TIME IS THIS THREAD'S OWN, its
|
|
162
|
+
* ticks counted and never the clock's: a main thread blocked or a process frozen past the bound (a test's spawnSync, a
|
|
163
|
+
* SIGSTOP, a paused container) counted against a healthy worker, whose answer sat unread in the port when the tick
|
|
164
|
+
* fired (docs/DESIGN.md "The watch"). THE BEAT (src/duck/cancel.ts) is the worker's JavaScript running with no turn of
|
|
165
|
+
* its loop: a statement calling a Mason function holds that loop for a thousand calls at a time, 999 × 322 ms over
|
|
166
|
+
* 20 kB documents, and such a statement ran 1,151 s without a pong and answered. What is left is a thread that neither
|
|
167
|
+
* turned its loop nor ran a row of a Mason function for 60 s: blocked inside the engine, since nothing on it waits for
|
|
168
|
+
* the engine's tasks any more (src/duck/store.ts `cleanUp`), or spinning. A deadlock is for ever, so a longer bound
|
|
169
|
+
* only says it later.
|
|
165
170
|
* - THE CLOSE IS BOUNDED: `close` gives the worker's shutdown `CLOSE_GRACE_MS` (a calling statement stops at its next
|
|
166
171
|
* call, milliseconds; a call-free one at DuckDB's interrupt; and the whole stop must fit inside a platform's 10 s
|
|
167
172
|
* before SIGKILL and `mason stop`'s 15 s), then declares it stopped the same way and resolves; the thread is ended
|
|
@@ -198,8 +203,10 @@ export class Executor {
|
|
|
198
203
|
/** the cancel table, shared with the worker (src/duck/cancel.ts), and its free slots */
|
|
199
204
|
cancels = cancelTable();
|
|
200
205
|
free = Array.from({ length: CANCEL_SLOTS }, (_, i) => CANCEL_SLOTS - 1 - i);
|
|
201
|
-
/**
|
|
202
|
-
|
|
206
|
+
/** pings sent since the worker last said anything or beat: the watch's count, of this thread's own ticks */
|
|
207
|
+
unanswered = 0;
|
|
208
|
+
/** the worker's beat at the watch's last look */
|
|
209
|
+
beat = 0;
|
|
203
210
|
watch;
|
|
204
211
|
silentMs;
|
|
205
212
|
/** why the worker was given up on without exiting (`close`'s bound, the silence), once it was: its instance may
|
|
@@ -218,7 +225,7 @@ export class Executor {
|
|
|
218
225
|
// a request is in flight (`call`)
|
|
219
226
|
this.worker.unref();
|
|
220
227
|
this.worker.on("message", (m) => {
|
|
221
|
-
this.
|
|
228
|
+
this.unanswered = 0;
|
|
222
229
|
if ("pong" in m)
|
|
223
230
|
return;
|
|
224
231
|
if ("log" in m) {
|
|
@@ -227,7 +234,7 @@ export class Executor {
|
|
|
227
234
|
}
|
|
228
235
|
if (m.id === this.shutdownId) {
|
|
229
236
|
if (!m.ok)
|
|
230
|
-
this.log(`closing the
|
|
237
|
+
this.log(`closing the engine failed: ${errorOf(m.error).message}`);
|
|
231
238
|
this.shutdownAnswered();
|
|
232
239
|
return;
|
|
233
240
|
}
|
|
@@ -250,18 +257,24 @@ export class Executor {
|
|
|
250
257
|
this.shutdownAnswered();
|
|
251
258
|
resolve();
|
|
252
259
|
}));
|
|
253
|
-
// the watch (see SILENT_MS): only while a request is pending — an idle worker has nothing to answer
|
|
260
|
+
// the watch (see SILENT_MS): only while a request is pending — an idle worker has nothing to answer — and never
|
|
261
|
+
// sooner than the bound after its first ping, whatever the moment the request came: the count is checked, then raised
|
|
262
|
+
const tick = Math.min(PING_MS, this.silentMs / 4);
|
|
263
|
+
const bound = Math.ceil(this.silentMs / tick);
|
|
254
264
|
this.watch = setInterval(() => {
|
|
255
|
-
|
|
256
|
-
|
|
265
|
+
const beat = beats(this.cancels);
|
|
266
|
+
if (this.gone !== null || this.pending.size === 0 || beat !== this.beat) {
|
|
267
|
+
this.beat = beat;
|
|
268
|
+
this.unanswered = 0;
|
|
257
269
|
return;
|
|
258
270
|
}
|
|
259
|
-
if (
|
|
260
|
-
this.abandon(`it answered nothing for ${Math.round(this.silentMs / 1000)} s with ${this.pending.size} request(s) waiting (stuck inside
|
|
271
|
+
if (this.unanswered >= bound) {
|
|
272
|
+
this.abandon(`it answered nothing for ${Math.round(this.silentMs / 1000)} s and ran no Mason function, with ${this.pending.size} request(s) waiting (stuck inside the engine: Mason bug, report it with mason logs)`);
|
|
261
273
|
return;
|
|
262
274
|
}
|
|
275
|
+
this.unanswered++;
|
|
263
276
|
this.worker.postMessage({ op: "ping" });
|
|
264
|
-
},
|
|
277
|
+
}, tick);
|
|
265
278
|
this.watch.unref();
|
|
266
279
|
}
|
|
267
280
|
/** the worker given up on without its exit: every request told so (57P02), the run told, and its shutdown asked —
|
|
@@ -290,7 +303,7 @@ export class Executor {
|
|
|
290
303
|
}
|
|
291
304
|
void this.shutdown.then(async () => { if (!this.hasExited)
|
|
292
305
|
await this.worker.terminate(); })
|
|
293
|
-
.catch((e) => this.log(`ending the
|
|
306
|
+
.catch((e) => this.log(`ending the engine's thread failed: ${e.message}`));
|
|
294
307
|
return this.shutdown;
|
|
295
308
|
}
|
|
296
309
|
static async start(o) {
|
|
@@ -317,7 +330,7 @@ export class Executor {
|
|
|
317
330
|
p.reject(err);
|
|
318
331
|
}
|
|
319
332
|
if (!stopped) {
|
|
320
|
-
this.log(`the
|
|
333
|
+
this.log(`the engine stopped: ${reason}`);
|
|
321
334
|
if (!this.closing)
|
|
322
335
|
for (const f of this.crashed)
|
|
323
336
|
f(reason);
|
|
@@ -355,8 +368,9 @@ export class Executor {
|
|
|
355
368
|
/** a connection of the instance */
|
|
356
369
|
async connect() { return new RemoteConnection(this, await this.call({ op: "connect" })); }
|
|
357
370
|
writer() { return new RemoteConnection(this, 0); }
|
|
358
|
-
/**
|
|
359
|
-
|
|
371
|
+
/** a libc order's functions (src/duck/functions.ts registerLibcOrder), registered on the worker: the key under
|
|
372
|
+
* `collate`, and jsonb's order under the database's (`database`) */
|
|
373
|
+
async registerLibcOrder(collate, order, database) { await this.call({ op: "libc", collate, order, database }); }
|
|
360
374
|
/** how many vectors each Mason function was called with so far (src/duck/functions.ts FUNCTION_CALLS: the worker's
|
|
361
375
|
* count, the functions being its) — what a test or a measurement reads to know whether a statement reached one */
|
|
362
376
|
async functionCalls() { return new Map(Object.entries(await this.call({ op: "calls" }))); }
|
|
@@ -366,7 +380,7 @@ export class Executor {
|
|
|
366
380
|
async runner() {
|
|
367
381
|
const slot = this.free.pop();
|
|
368
382
|
if (slot === undefined)
|
|
369
|
-
throw new StatementError({ code: "53300", message: `too many sessions are reading the
|
|
383
|
+
throw new StatementError({ code: "53300", message: `too many sessions are reading the synced data at once (${CANCEL_SLOTS})`, hint: "close idle connections (a pool's size), then connect again" });
|
|
370
384
|
resetSlot(this.cancels, slot);
|
|
371
385
|
try {
|
|
372
386
|
return new RemoteRunner(this, await this.call({ op: "runner.open", slot }), slot);
|
|
@@ -393,7 +407,7 @@ export class Executor {
|
|
|
393
407
|
const closed = (async () => { await this.shutDown(); await this.exited; return "closed"; })();
|
|
394
408
|
try {
|
|
395
409
|
if (await Promise.race([closed, late]) === "late") {
|
|
396
|
-
this.log(`the
|
|
410
|
+
this.log(`the engine'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 the engine needs a SIGKILL)`);
|
|
397
411
|
this.abandon(`it did not close within ${graceMs / 1000} s`);
|
|
398
412
|
}
|
|
399
413
|
}
|
|
@@ -437,7 +451,7 @@ const reasonOf = (signal) => !signal.aborted ? null : signal.reason instanceof S
|
|
|
437
451
|
function goneError(e, sql) {
|
|
438
452
|
if (e.stopped)
|
|
439
453
|
return new StatementError(STOPPING, sql);
|
|
440
|
-
return new StatementError({ code: "57P02", message: "terminating connection because the
|
|
454
|
+
return new StatementError({ code: "57P02", message: "terminating connection because the engine stopped", detail: e.message,
|
|
441
455
|
hint: "Mason starts again by itself: reconnect in a few seconds and repeat the statement (mason status says when it is back)" }, sql);
|
|
442
456
|
}
|
|
443
457
|
/**
|
package/dist/src/duck/explain.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { agoText, syncText } from "../lib/table-sync.js";
|
|
1
2
|
/** a node of DuckDB's JSON, either form: `name` (EXPLAIN) or `type` with `intermediate_rows` and `timing` (the profile).
|
|
2
3
|
* The profile's RESULT_COLLECTOR and EXECUTE are the result's plumbing, not the plan's: their child stands for them */
|
|
3
4
|
export function planNodes(json) {
|
|
@@ -27,7 +28,7 @@ export function profiled(doc) {
|
|
|
27
28
|
return { sql: typeof d.query?.sql === "string" ? d.query.sql : null, plan: planNodes(d.operator ?? []) };
|
|
28
29
|
}
|
|
29
30
|
/** what EXPLAIN ANALYZE says of a statement DuckDB's profiler timed no operator of */
|
|
30
|
-
export const UNPROFILED = "
|
|
31
|
+
export const UNPROFILED = "the engine's profiler recorded no operator of this statement: the engine answered it while planning it (as it answers a count(*) of a whole table from the table's row count), so the plan is its EXPLAIN, with the rows it answered";
|
|
31
32
|
/** DuckDB's operator as DuckDB's own plan prints it (`HASH_GROUP_BY` is `Hash Group By`), a scan `Table Scan` as its
|
|
32
33
|
* profiler names it: DuckDB's EXPLAIN says SEQ_SCAN for the same operator, and Postgres's `Seq Scan` says otherwise */
|
|
33
34
|
export const operatorName = (type) => type.toLowerCase().split("_").map((w) => w === "n" ? "N" : w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
@@ -35,7 +36,7 @@ export const operatorName = (type) => type.toLowerCase().split("_").map((w) => w
|
|
|
35
36
|
const quotedName = (s) => /^[a-z_][a-z0-9_$]*$/.test(s) ? s : `"${s.replaceAll('"', '""')}"`;
|
|
36
37
|
/**
|
|
37
38
|
* A TABLE AS POSTGRES WRITES IT, each part quoted where it must be (`public.orders`, `public."Clients"`,
|
|
38
|
-
* `public."x; y"`): the ONE way EXPLAIN names a copy (its line, FORMAT JSON's `Mason.
|
|
39
|
+
* `public."x; y"`): the ONE way EXPLAIN names a copy (its line, FORMAT JSON's `Mason.Tables[].Table`), which a statement
|
|
39
40
|
* can read back as it is (`mason mcp`'s tables and describe name tables by it too).
|
|
40
41
|
*/
|
|
41
42
|
export const qualifiedName = (t) => `${quotedName(t.schema)}.${quotedName(t.name)}`;
|
|
@@ -93,35 +94,41 @@ const utc = (iso) => `${iso.slice(0, 19).replace("T", " ")} UTC`;
|
|
|
93
94
|
const UNKNOWN = "how it is kept current is unknown: the sync has no record of it";
|
|
94
95
|
/**
|
|
95
96
|
* HOW A COPY IS KEPT CURRENT, in EXPLAIN's words, from the sync's one derivation (src/duck/sync.ts `KeptCurrent`, which
|
|
96
|
-
* `mason status` renders too): a copy not yet copied or whose copy failed says so first; then
|
|
97
|
-
*
|
|
98
|
-
* copied
|
|
99
|
-
*
|
|
97
|
+
* `mason status` renders too): a copy not yet copied or whose copy failed says so first; then its sync and its age —
|
|
98
|
+
* `off: last synced 3 days ago (…)` with the command that syncs it, `polled every 30s, last 4 s ago (…)` — or served as
|
|
99
|
+
* copied and skipped by the passes (to be copied again, caught up first), copied again whole, or, in replication mode,
|
|
100
|
+
* its part in the stream, how far the stream has applied and why it is not running. A copy's age is never left out:
|
|
101
|
+
* a BI tool or an agent reading a stale count cannot tell it is one. "kept current" is said only of a copy that is
|
|
102
|
+
* (`keptCurrentBy`). `rows` false says no count of its rows: a filtered table's are every row's, not the role's.
|
|
100
103
|
*/
|
|
101
|
-
export function keptText(f) {
|
|
104
|
+
export function keptText(f, now = Date.now(), rows = true) {
|
|
102
105
|
if (f === null)
|
|
103
106
|
return UNKNOWN;
|
|
104
107
|
if (f.phase === "waiting")
|
|
105
|
-
return "waiting for its
|
|
108
|
+
return "waiting for its first sync to start";
|
|
106
109
|
if (f.phase === "copying")
|
|
107
|
-
return `being
|
|
110
|
+
return `being read whole${rows ? ` (${count(f.rowsSoFar ?? 0)} rows so far)` : ""}: kept current once synced`;
|
|
108
111
|
if (f.phase === "failed")
|
|
109
|
-
return `its
|
|
112
|
+
return `its first sync failed: ${(f.error ?? "mason logs says why").split("\n")[0]}`;
|
|
110
113
|
const k = f.kept;
|
|
111
114
|
if (k === undefined)
|
|
112
115
|
return UNKNOWN;
|
|
116
|
+
if (k.by === "generated")
|
|
117
|
+
return k.text;
|
|
113
118
|
if (k.by === "poll") {
|
|
114
|
-
const
|
|
119
|
+
const synced = k.syncedAt === null ? "never synced" : `last synced ${agoText(k.syncedAt, now)} (${utc(k.syncedAt)})`;
|
|
115
120
|
if (k.pending !== undefined)
|
|
116
|
-
return `NOT kept current (${
|
|
117
|
-
if (k.
|
|
118
|
-
return `
|
|
119
|
-
return `
|
|
121
|
+
return `NOT kept current (${synced}): ${k.pending}`;
|
|
122
|
+
if (k.sync.mode === "off")
|
|
123
|
+
return `off: ${synced}; mason sync ${f.source} syncs it now`;
|
|
124
|
+
return `polled every ${k.sync.every}, ${k.syncedAt === null ? "not polled yet" : `last ${agoText(k.syncedAt, now)} (${utc(k.syncedAt)})`}`;
|
|
120
125
|
}
|
|
121
126
|
if (k.by === "recopy")
|
|
122
|
-
return `kept current by
|
|
127
|
+
return `kept current by reading it again whole, ${k.copiedAt === null ? "no sync recorded yet" : `last synced ${utc(k.copiedAt)}`}`;
|
|
123
128
|
const stream = `logical replication (slot ${k.slot})`;
|
|
124
|
-
|
|
129
|
+
// the copy's age as status says it (replicated, at <lsn>, <age>), then the newest change it applied
|
|
130
|
+
const synced = k.syncedAt === null ? "not synced yet" : `last synced ${agoText(k.syncedAt, now)} (${utc(k.syncedAt)})`;
|
|
131
|
+
const where = `replicated, at ${k.position}, ${synced}, ${k.lastCommit === null ? "no change applied this run" : `last change committed ${utc(k.lastCommit)}`}`;
|
|
125
132
|
if (k.state === "streamed")
|
|
126
133
|
return k.notRunning === null ? `kept current by ${stream}, ${where}`
|
|
127
134
|
: `streamed by ${stream}, ${where}; NOT kept current now, the stream is not running: ${k.notRunning}`;
|
|
@@ -129,54 +136,61 @@ export function keptText(f) {
|
|
|
129
136
|
: `${stream}, not streamed yet: ${k.why ?? ""}`;
|
|
130
137
|
}
|
|
131
138
|
/** what keeps a copy current NOW, as FORMAT JSON's `Kept Current By` says it: null for a copy nothing does (not copied
|
|
132
|
-
* yet, skipped by the passes,
|
|
139
|
+
* yet, skipped by the passes, its sync off, not streamed, the stream down), so a reader keying on it is
|
|
133
140
|
* never told of a mode that is not at work on it */
|
|
134
141
|
export function keptCurrentBy(f) {
|
|
135
142
|
const k = f?.phase === "copied" ? f.kept : undefined;
|
|
136
143
|
if (k === undefined)
|
|
137
144
|
return null;
|
|
138
145
|
if (k.by === "poll")
|
|
139
|
-
return k.pending === undefined && k.
|
|
146
|
+
return k.pending === undefined && k.sync.mode === "poll" ? "polling" : null;
|
|
140
147
|
if (k.by === "recopy")
|
|
141
148
|
return "copying again";
|
|
149
|
+
if (k.by === "generated")
|
|
150
|
+
return null;
|
|
142
151
|
return k.state === "streamed" && k.notRunning === null ? "logical replication" : null;
|
|
143
152
|
}
|
|
144
153
|
/** how a copy is kept current, as FORMAT JSON's keys say it (`keptText`'s facts) */
|
|
145
|
-
function keptJson(f) {
|
|
154
|
+
function keptJson(f, rows) {
|
|
146
155
|
const k = f?.phase === "copied" ? f.kept : undefined;
|
|
147
156
|
return {
|
|
148
|
-
"Kept Current By": keptCurrentBy(f), "Kept Current": keptText(f),
|
|
149
|
-
...(k?.by === "poll" ? { "Last Poll": k.
|
|
150
|
-
...(k?.by === "replication" ? { Slot: k.slot, "Stream State": k.state, "Applied Through": k.position, "Last Change Committed": k.lastCommit, "Stream Not Running": k.notRunning } : {}),
|
|
157
|
+
"Kept Current By": keptCurrentBy(f), "Kept Current": keptText(f, undefined, rows),
|
|
158
|
+
...(k?.by === "poll" ? { Sync: syncText(k.sync), "Last Synced": k.syncedAt, "Next Poll": k.nextAt } : {}),
|
|
159
|
+
...(k?.by === "replication" ? { Sync: "replication", Slot: k.slot, "Stream State": k.state, "Applied Through": k.position, "Last Synced": k.syncedAt, "Last Change Committed": k.lastCommit, "Stream Not Running": k.notRunning } : {}),
|
|
151
160
|
};
|
|
152
161
|
}
|
|
162
|
+
/** what EXPLAIN says of a statement that reads a filtered table, in the place of the engine's plan */
|
|
163
|
+
const FILTERED = "the engine's plan is not shown for a statement that reads a table the role filters: it would say what the engine knows of every row; a user with no row filter reads the full plan";
|
|
153
164
|
const ms = (x) => x.toFixed(3);
|
|
154
165
|
const count = (n) => n.toLocaleString("en-US");
|
|
155
166
|
const detail = (v) => Array.isArray(v) ? v.join(", ") : v;
|
|
156
167
|
/** the details a plan line carries without VERBOSE: everything but the projections and filters, which VERBOSE adds */
|
|
157
168
|
const VERBOSE_ONLY = new Set(["Projections", "Filters", "Table", "Type"]);
|
|
169
|
+
/** a filtered table says no row count: the estimate is every row's, which the role does not read */
|
|
158
170
|
function copyLine(c) {
|
|
159
|
-
const rows = c.rows === null ? "rows unknown" : `about ${count(c.rows)} rows`;
|
|
160
|
-
return `${c.table} (${rows}
|
|
171
|
+
const rows = c.filter !== undefined ? "" : c.rows === null ? "rows unknown; " : `about ${count(c.rows)} rows; `;
|
|
172
|
+
return `${c.table} (${rows}${keptText(c.facts, undefined, c.filter === undefined)}${c.filter !== undefined ? `; ${c.filter}` : ""})`;
|
|
161
173
|
}
|
|
162
174
|
/** EXPLAIN as Postgres's text format: one line a row */
|
|
163
175
|
export function explainText(e, o) {
|
|
164
176
|
const lines = [];
|
|
165
|
-
lines.push(e.copies.length === 0 ? "Mason: answered
|
|
166
|
-
: e.copies.length === 1 ? `Mason: answered
|
|
177
|
+
lines.push(e.copies.length === 0 ? "Mason: answered reading no synced table"
|
|
178
|
+
: e.copies.length === 1 ? `Mason: answered from the synced table ${copyLine(e.copies[0])}` : "Mason: answered from the synced tables");
|
|
167
179
|
if (e.copies.length > 1)
|
|
168
180
|
for (const c of e.copies)
|
|
169
181
|
lines.push(` ${copyLine(c)}`);
|
|
170
|
-
lines.push(` Sent to
|
|
171
|
-
lines.push(` Computed by Mason after
|
|
182
|
+
lines.push(` Sent to the engine: ${e.sql}`);
|
|
183
|
+
lines.push(` Computed by Mason after the engine: ${e.computed.length === 0 ? "nothing" : e.computed.join("; ")}`);
|
|
172
184
|
if (e.functions.length > 0)
|
|
173
|
-
lines.push(` Mason's functions
|
|
185
|
+
lines.push(` Mason's functions the engine calls: ${e.functions.join(", ")}`);
|
|
174
186
|
for (const name of o.ignored)
|
|
175
|
-
lines.push(` Note: ${name} has no
|
|
187
|
+
lines.push(` Note: ${name} has no meaning to the engine: ignored`);
|
|
188
|
+
if (e.filtered === true)
|
|
189
|
+
lines.push(` Note: ${FILTERED}`);
|
|
176
190
|
if (e.analyzed?.profiled === false)
|
|
177
191
|
lines.push(` Note: ${UNPROFILED}`);
|
|
178
192
|
else if (e.analyzed !== undefined && o.timing)
|
|
179
|
-
lines.push(" Note: an operator's thread time is
|
|
193
|
+
lines.push(" Note: an operator's thread time is the engine's, summed over the threads that ran it and the operators under it: a parallel one's exceeds the Execution Time");
|
|
180
194
|
const walk = (n, depth) => {
|
|
181
195
|
const lead = depth === 0 ? "" : `${" ".repeat(6 * depth - 4)}-> `;
|
|
182
196
|
const on = scannedTable(n);
|
|
@@ -193,8 +207,8 @@ export function explainText(e, o) {
|
|
|
193
207
|
e.plan.forEach((n) => walk(n, 0));
|
|
194
208
|
if (e.analyzed !== undefined && o.summary) {
|
|
195
209
|
const p = e.analyzed.planning, x = e.analyzed.execution;
|
|
196
|
-
lines.push(`Planning Time: ${ms(p.analysisMs + p.rewriteMs + p.duckdbMs)} ms (Postgres's analysis ${ms(p.analysisMs)}, Mason's rewrite ${ms(p.rewriteMs)},
|
|
197
|
-
lines.push(`Execution Time: ${ms(x.duckdbMs + x.masonMs)} ms (
|
|
210
|
+
lines.push(`Planning Time: ${ms(p.analysisMs + p.rewriteMs + p.duckdbMs)} ms (Postgres's analysis ${ms(p.analysisMs)}, Mason's rewrite ${ms(p.rewriteMs)}, the engine's planning ${ms(p.duckdbMs)})`);
|
|
211
|
+
lines.push(`Execution Time: ${ms(x.duckdbMs + x.masonMs)} ms (the engine ${ms(x.duckdbMs)}, Mason's conversion and exact numerics ${ms(x.masonMs)})`);
|
|
198
212
|
}
|
|
199
213
|
return lines;
|
|
200
214
|
}
|
|
@@ -208,31 +222,31 @@ export function explainJson(e, o) {
|
|
|
208
222
|
"Node Type": operatorName(n.type),
|
|
209
223
|
...(on === null ? {} : { "Relation Name": on.name, Schema: on.schema }),
|
|
210
224
|
...(o.costs && n.estimate !== null ? { "Plan Rows": n.estimate } : {}),
|
|
211
|
-
...(n.actualRows === undefined ? {} : { "Actual Rows": n.actualRows, "Actual Loops": 1, ...(o.timing && n.threadMs !== undefined ? { "
|
|
212
|
-
...Object.fromEntries(Object.entries(n.extra).filter(([k]) => o.verbose ? k !== "Table" : !VERBOSE_ONLY.has(k)).map(([k, v]) => [`
|
|
225
|
+
...(n.actualRows === undefined ? {} : { "Actual Rows": n.actualRows, "Actual Loops": 1, ...(o.timing && n.threadMs !== undefined ? { "Engine Thread Time": Number(ms(n.threadMs)) } : {}) }),
|
|
226
|
+
...Object.fromEntries(Object.entries(n.extra).filter(([k]) => o.verbose ? k !== "Table" : !VERBOSE_ONLY.has(k)).map(([k, v]) => [`Engine ${k}`, v])),
|
|
213
227
|
...(n.children.length === 0 ? {} : { Plans: n.children.map(node) }),
|
|
214
228
|
};
|
|
215
229
|
};
|
|
216
230
|
// Postgres's plan has one root; DuckDB's has one too (a statement's), kept as a list should it ever have more
|
|
217
231
|
const roots = e.plan.map(node);
|
|
218
232
|
const top = {
|
|
219
|
-
Plan: roots.length === 1 ? roots[0] : { "Node Type": "
|
|
233
|
+
Plan: e.filtered === true ? { "Node Type": "Plan Not Shown" } : roots.length === 1 ? roots[0] : { "Node Type": "Engine Plans", Plans: roots },
|
|
220
234
|
Mason: {
|
|
221
|
-
"Answered By": "
|
|
222
|
-
|
|
223
|
-
"Sent To
|
|
224
|
-
"Computed After
|
|
225
|
-
"Mason Functions
|
|
226
|
-
...(o.ignored.length === 0 ? {} : { Ignored: o.ignored.map((n) => `${n} has no
|
|
227
|
-
...(e.analyzed?.profiled === false ? { Note: UNPROFILED } : {}),
|
|
235
|
+
"Answered By": "the engine",
|
|
236
|
+
Tables: e.copies.map((c) => ({ Table: c.table, ...(c.filter === undefined ? { "Estimated Rows": c.rows } : {}), ...keptJson(c.facts, c.filter === undefined), ...(c.filter !== undefined ? { "Row Filter": c.filter } : {}) })),
|
|
237
|
+
"Sent To Engine": e.sql,
|
|
238
|
+
"Computed After Engine": e.computed,
|
|
239
|
+
"Mason Functions Engine Calls": e.functions,
|
|
240
|
+
...(o.ignored.length === 0 ? {} : { Ignored: o.ignored.map((n) => `${n} has no meaning to the engine`) }),
|
|
241
|
+
...(e.filtered === true ? { Note: FILTERED } : e.analyzed?.profiled === false ? { Note: UNPROFILED } : {}),
|
|
228
242
|
},
|
|
229
243
|
};
|
|
230
244
|
if (e.analyzed !== undefined && o.summary) {
|
|
231
245
|
const p = e.analyzed.planning, x = e.analyzed.execution;
|
|
232
246
|
top["Planning Time"] = Number(ms(p.analysisMs + p.rewriteMs + p.duckdbMs));
|
|
233
247
|
top["Execution Time"] = Number(ms(x.duckdbMs + x.masonMs));
|
|
234
|
-
top.Mason["Planning"] = { "Postgres Analysis": Number(ms(p.analysisMs)), "Mason Rewrite": Number(ms(p.rewriteMs)), "
|
|
235
|
-
top.Mason["Execution"] = {
|
|
248
|
+
top.Mason["Planning"] = { "Postgres Analysis": Number(ms(p.analysisMs)), "Mason Rewrite": Number(ms(p.rewriteMs)), "Engine Planning": Number(ms(p.duckdbMs)) };
|
|
249
|
+
top.Mason["Execution"] = { Engine: Number(ms(x.duckdbMs)), "Mason Conversion": Number(ms(x.masonMs)) };
|
|
236
250
|
}
|
|
237
251
|
return JSON.stringify([top], null, 2);
|
|
238
252
|
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE COPY'S EXTENSIONS SHIP WITH MASON. The copy reads the source through DuckDB's `postgres` extension
|
|
3
|
+
* (src/duck/sync.ts attach), and a statement that casts a value to inet needs DuckDB's `inet`; DuckDB bundles
|
|
4
|
+
* neither, and by default downloads one from its extension repository at the first statement that needs it. Mason
|
|
5
|
+
* ships both instead, ONE npm PACKAGE PER PLATFORM (`@mason-data/extensions-<platform>`), as DuckDB's node bindings ship
|
|
6
|
+
* their binary: an optional dependency of @mason-data/cli whose `os` / `cpu` / `libc` make npm install the one this
|
|
7
|
+
* machine runs, built by cli/extensions.ts from the files' checksums pinned in cli/extensions.json. The image installs
|
|
8
|
+
* the same package (docker/mason-data.Dockerfile).
|
|
9
|
+
*
|
|
10
|
+
* - A package found (`findExtensions`): the instance's `extension_directory` IS the package's folder and nothing is
|
|
11
|
+
* ever installed (`extensionSettings`): `LOAD postgres` reads it, an inet cast autoloads it from there, and
|
|
12
|
+
* autoinstall is off, so no statement downloads anything. DuckDB checks each file's signature at LOAD
|
|
13
|
+
* (`allow_unsigned_extensions` stays off: the official extensions are signed), and LOAD writes nothing, so the
|
|
14
|
+
* folder may be read-only (a global npm install, the image's /app).
|
|
15
|
+
* - None (`npm install --omit=optional`, a platform no package is built for — musl, Windows on arm64 — or a checkout):
|
|
16
|
+
* DuckDB's own way, said with the reason: the first copy installs the extension from the internet into
|
|
17
|
+
* ~/.duckdb/extensions (src/duck/sync.ts attach). `mason doctor` says which (`probeExtensions`).
|
|
18
|
+
*
|
|
19
|
+
* tests/duck-extensions.test.ts holds the resolver per platform over a fake package; the integration test
|
|
20
|
+
* (tests/integration/extensions.itest.ts) holds a copy and an inet cast to the package with nothing downloaded.
|
|
21
|
+
*/
|
|
22
|
+
import * as fs from "node:fs";
|
|
23
|
+
import * as path from "node:path";
|
|
24
|
+
import { createRequire } from "node:module";
|
|
25
|
+
import { DuckDBInstance, version } from "@duckdb/node-api";
|
|
26
|
+
/** Each platform a package is built for: Node's `<platform>-<arch>` (the package's name) and DuckDB's name for it (the
|
|
27
|
+
* folder of the extension repository, and of `extension_directory`). Linux is glibc's: musl has no package. */
|
|
28
|
+
export const EXTENSION_PLATFORMS = {
|
|
29
|
+
"darwin-arm64": "osx_arm64",
|
|
30
|
+
"darwin-x64": "osx_amd64",
|
|
31
|
+
"linux-arm64": "linux_arm64",
|
|
32
|
+
"linux-x64": "linux_amd64",
|
|
33
|
+
"win32-x64": "windows_amd64",
|
|
34
|
+
};
|
|
35
|
+
/** The extensions each package holds, by the name DuckDB installs them under (`INSTALL postgres` is `postgres_scanner`). */
|
|
36
|
+
export const EXTENSION_NAMES = ["postgres_scanner", "inet"];
|
|
37
|
+
export const packageName = (p) => `@mason-data/extensions-${p}`;
|
|
38
|
+
/** Where a package keeps the files: `<package>/extensions/<engine>/<DuckDB's platform>/<name>.duckdb_extension`, the
|
|
39
|
+
* layout DuckDB reads `extension_directory` in. */
|
|
40
|
+
export const extensionFile = (root, engine, platform, name) => path.join(root, "extensions", engine, EXTENSION_PLATFORMS[platform], `${name}.duckdb_extension`);
|
|
41
|
+
export function thisHost() {
|
|
42
|
+
// Node's own report names glibc's version where glibc runs; musl (Alpine) has none (detect-libc's first check). Its
|
|
43
|
+
// network part is left out, as detect-libc leaves it: enumerating the interfaces has taken seconds in a container
|
|
44
|
+
let header;
|
|
45
|
+
if (process.platform === "linux") {
|
|
46
|
+
// `excludeNetwork` is Node's (20+), not yet in @types/node
|
|
47
|
+
const report = process.report;
|
|
48
|
+
const network = report.excludeNetwork;
|
|
49
|
+
report.excludeNetwork = true;
|
|
50
|
+
try {
|
|
51
|
+
header = report.getReport().header;
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
report.excludeNetwork = network;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { platform: process.platform, arch: process.arch, musl: process.platform === "linux" && header?.glibcVersionRuntime === undefined };
|
|
58
|
+
}
|
|
59
|
+
/** The platform's key as npm and DuckDB's bindings name it: `linux-x64`, `linux-x64-musl`, `darwin-arm64`. */
|
|
60
|
+
export const hostKey = (h) => `${h.platform}-${h.arch}${h.musl ? "-musl" : ""}`;
|
|
61
|
+
const requireHere = createRequire(import.meta.url);
|
|
62
|
+
/**
|
|
63
|
+
* THE PACKAGE FOR THIS MACHINE, found where Node would find it from this module (the CLI's node_modules, nested or
|
|
64
|
+
* hoisted; the image's /app/node_modules), holding both files for THIS engine. Reads two files' existence: no engine
|
|
65
|
+
* is opened. `resolve` and `host` are a test's.
|
|
66
|
+
*/
|
|
67
|
+
export function findExtensions(o = {}) {
|
|
68
|
+
const host = o.host ?? thisHost();
|
|
69
|
+
const engine = o.engine ?? `v${version().replace(/^v/, "")}`;
|
|
70
|
+
const key = hostKey(host);
|
|
71
|
+
if (!(key in EXTENSION_PLATFORMS)) {
|
|
72
|
+
return { shipped: false, package: null, why: `no package of Mason's extensions is built for ${key} (only ${Object.keys(EXTENSION_PLATFORMS).join(", ")})` };
|
|
73
|
+
}
|
|
74
|
+
const platform = key;
|
|
75
|
+
const name = packageName(platform);
|
|
76
|
+
let manifest;
|
|
77
|
+
try {
|
|
78
|
+
manifest = (o.resolve ?? requireHere.resolve)(`${name}/package.json`);
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
// not installed is the one answer expected here; anything else is said as it is
|
|
82
|
+
if (e.code !== "MODULE_NOT_FOUND")
|
|
83
|
+
throw e;
|
|
84
|
+
return { shipped: false, package: name, why: `${name} is not installed beside Mason (npm installs it with @mason-data/cli unless optional dependencies are left out: --omit=optional)` };
|
|
85
|
+
}
|
|
86
|
+
const root = path.dirname(manifest);
|
|
87
|
+
let pkgVersion;
|
|
88
|
+
try {
|
|
89
|
+
pkgVersion = String(JSON.parse(fs.readFileSync(manifest, "utf8")).version ?? "");
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
// a broken install: refused in Mason's words with the way out, never the parser's error
|
|
93
|
+
return { shipped: false, package: name, why: `${name}'s package.json at ${manifest} cannot be read (${e.message}): install @mason-data/cli again` };
|
|
94
|
+
}
|
|
95
|
+
const missing = EXTENSION_NAMES.filter((n) => !fs.existsSync(extensionFile(root, engine, platform, n)));
|
|
96
|
+
if (missing.length > 0) {
|
|
97
|
+
const has = fs.existsSync(path.join(root, "extensions")) ? fs.readdirSync(path.join(root, "extensions")) : [];
|
|
98
|
+
return { shipped: false, package: name, why: `${name} ${pkgVersion} holds no ${missing.join(" or ")} for the engine Mason runs (${engine}; it holds ${has.length ? has.join(", ") : "none"}): ` +
|
|
99
|
+
"install @mason-data/cli again, whose packages are released together" };
|
|
100
|
+
}
|
|
101
|
+
return { shipped: true, package: name, version: pkgVersion, directory: path.join(root, "extensions"), platform };
|
|
102
|
+
}
|
|
103
|
+
/** The instance's settings for what was found: with the package, its folder and no download, ever; without, DuckDB's
|
|
104
|
+
* defaults (the first copy installs into ~/.duckdb/extensions). */
|
|
105
|
+
export function extensionSettings(x) {
|
|
106
|
+
return x.shipped ? { extension_directory: x.directory, autoinstall_known_extensions: "false", autoload_known_extensions: "true" } : {};
|
|
107
|
+
}
|
|
108
|
+
/** The statement the copy loads its postgres extension with: from the package, a LOAD; without one, DuckDB's INSTALL
|
|
109
|
+
* first (a download, once a machine). */
|
|
110
|
+
export const loadPostgresSql = (x) => x.shipped ? "LOAD postgres" : "INSTALL postgres; LOAD postgres;";
|
|
111
|
+
/** What a copy that could not load the postgres extension says (src/duck/sync.ts attach), with the way out. */
|
|
112
|
+
export function extensionFailure(x, error) {
|
|
113
|
+
return x.shipped
|
|
114
|
+
? `Mason could not load its postgres extension from ${x.package} ${x.version} (${x.directory}): ${error}. Install @mason-data/cli again; mason doctor checks it`
|
|
115
|
+
: `Mason could not load its postgres extension: ${error}. ${x.why}, so the first sync on a machine downloads it ` +
|
|
116
|
+
"(then ~/.duckdb/extensions): connect to the internet once, and the sync is tried again at the next pass";
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* THE PROBE `mason doctor` SAYS (the contract answers a probe, never a file's existence): with the package, both
|
|
120
|
+
* extensions LOADED by an instance of their own with the run's settings — the signature and the platform checked as
|
|
121
|
+
* the run's will be; without one, whether DuckDB has them installed in ~/.duckdb/extensions already, and `ok` null when
|
|
122
|
+
* it has not (the first copy will download them: fine online, which no probe can tell). Downloads nothing.
|
|
123
|
+
*/
|
|
124
|
+
export async function probeExtensions(x = findExtensions()) {
|
|
125
|
+
let instance;
|
|
126
|
+
try {
|
|
127
|
+
instance = await DuckDBInstance.create(":memory:", { autoinstall_known_extensions: "false", ...extensionSettings(x) });
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
return { ok: null, detail: `could not open an engine to check them: ${e.message}` };
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const c = await instance.connect();
|
|
134
|
+
try {
|
|
135
|
+
if (x.shipped) {
|
|
136
|
+
for (const n of EXTENSION_NAMES) {
|
|
137
|
+
try {
|
|
138
|
+
await c.run(`LOAD ${n}`);
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
return { ok: false, detail: `${x.package} ${x.version} is installed, and its ${n.replace("_scanner", "")} extension does not load: ${e.message.split("\n")[0]}. Install @mason-data/cli again` };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return { ok: true, detail: `postgres and inet ship with Mason (${x.package} ${x.version}): nothing is downloaded` };
|
|
145
|
+
}
|
|
146
|
+
const rows = (await c.runAndReadAll(`SELECT extension_name, installed FROM duckdb_extensions() WHERE extension_name IN (${EXTENSION_NAMES.map((n) => `'${n}'`).join(", ")})`)).getRowObjects();
|
|
147
|
+
const absent = EXTENSION_NAMES.filter((n) => !rows.some((r) => r.extension_name === n && r.installed === true)).map((n) => n.replace("_scanner", ""));
|
|
148
|
+
if (absent.length === 0)
|
|
149
|
+
return { ok: true, detail: `postgres and inet were downloaded on this machine before (~/.duckdb/extensions), not shipped: ${x.why}` };
|
|
150
|
+
// not a failure: online, the first copy downloads them; whether this machine will be online then, no probe can tell
|
|
151
|
+
return { ok: null, detail: `${absent.join(" and ")} not on this machine yet, so the first sync downloads them and needs the internet once: ${x.why}` };
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
c.closeSync();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
instance.closeSync();
|
|
159
|
+
}
|
|
160
|
+
}
|