@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.
- package/LICENSE +202 -0
- package/README.md +268 -2
- package/dist/src/api/error.js +18 -0
- package/dist/src/api/postgres/catalog-worker.js +92 -0
- package/dist/src/api/postgres/catalog.js +477 -0
- package/dist/src/api/postgres/encode.js +541 -0
- package/dist/src/api/postgres/explain.js +143 -0
- package/dist/src/api/postgres/listener.js +60 -0
- package/dist/src/api/postgres/parser-worker.js +58 -0
- package/dist/src/api/postgres/parser.js +121 -0
- package/dist/src/api/postgres/plain.js +33 -0
- package/dist/src/api/postgres/protocol.js +404 -0
- package/dist/src/api/postgres/schema.js +54 -0
- package/dist/src/api/postgres/server.js +1771 -0
- package/dist/src/api/postgres/translate.js +248 -0
- package/dist/src/api/request-log.js +50 -0
- package/dist/src/cdc/pg-wire.js +588 -0
- package/dist/src/cdc/pgoutput.js +107 -0
- package/dist/src/cdc/poll.js +67 -0
- package/dist/src/cdc/source.js +65 -0
- package/dist/src/cli/background.js +198 -0
- package/dist/src/cli/command.js +173 -0
- package/dist/src/cli/commands.js +1277 -0
- package/dist/src/cli/control.js +36 -0
- package/dist/src/cli/demo.js +149 -0
- package/dist/src/cli/dev.js +669 -0
- package/dist/src/cli/doctor.js +260 -0
- package/dist/src/cli/guides.js +158 -0
- package/dist/src/cli/init.js +366 -0
- package/dist/src/cli/machine.js +162 -0
- package/dist/src/cli/main.js +83 -0
- package/dist/src/cli/mcp.js +726 -0
- package/dist/src/cli/pg-tls.js +100 -0
- package/dist/src/cli/pg-url.js +52 -0
- package/dist/src/cli/pin.js +36 -0
- package/dist/src/cli/ports.js +118 -0
- package/dist/src/cli/preflight.js +347 -0
- package/dist/src/cli/project.js +129 -0
- package/dist/src/cli/readiness.js +76 -0
- package/dist/src/cli/registry.js +220 -0
- package/dist/src/cli/replication-checks.js +120 -0
- package/dist/src/cli/replication-mode.js +148 -0
- package/dist/src/cli/run-server.js +311 -0
- package/dist/src/cli/runs.js +120 -0
- package/dist/src/cli/security.js +86 -0
- package/dist/src/cli/self-signed-cert.js +64 -0
- package/dist/src/cli/serve.js +301 -0
- package/dist/src/cli/source-edit.js +116 -0
- package/dist/src/cli/sources.js +564 -0
- package/dist/src/cli/state.js +58 -0
- package/dist/src/cli/style.js +187 -0
- package/dist/src/cli/supervise.js +248 -0
- package/dist/src/cli/sync.js +94 -0
- package/dist/src/cli/ui.js +107 -0
- package/dist/src/cli/user.js +229 -0
- package/dist/src/cli/version.js +23 -0
- package/dist/src/demo/seed.js +237 -0
- package/dist/src/demo/source.js +655 -0
- package/dist/src/duck/analyze.js +113 -0
- package/dist/src/duck/cancel.js +49 -0
- package/dist/src/duck/distinct-key.js +228 -0
- package/dist/src/duck/executor-worker.js +268 -0
- package/dist/src/duck/executor.js +567 -0
- package/dist/src/duck/explain.js +238 -0
- package/dist/src/duck/functions.js +819 -0
- package/dist/src/duck/gaps.js +94 -0
- package/dist/src/duck/jsonb-order.js +175 -0
- package/dist/src/duck/jsonb.js +188 -0
- package/dist/src/duck/kept-route.js +299 -0
- package/dist/src/duck/kept.js +964 -0
- package/dist/src/duck/late.js +221 -0
- package/dist/src/duck/libc-order.js +190 -0
- package/dist/src/duck/pg-eval-worker.js +34 -0
- package/dist/src/duck/pg-eval.js +59 -0
- package/dist/src/duck/pgtree.js +378 -0
- package/dist/src/duck/plan.js +259 -0
- package/dist/src/duck/publication.js +207 -0
- package/dist/src/duck/requests.js +209 -0
- package/dist/src/duck/resolved.js +248 -0
- package/dist/src/duck/rewrite.js +5101 -0
- package/dist/src/duck/store.js +907 -0
- package/dist/src/duck/stream.js +518 -0
- package/dist/src/duck/sync.js +2929 -0
- package/dist/src/duck/to-char.js +482 -0
- package/dist/src/duck/token.js +31 -0
- package/dist/src/duck/types.js +427 -0
- package/dist/src/duck/users.js +231 -0
- package/dist/src/duck/verified.js +1001 -0
- package/dist/src/duck/views.js +150 -0
- package/dist/src/lib/database-url.js +74 -0
- package/dist/src/lib/did-you-mean.js +43 -0
- package/dist/src/lib/dotenv.js +72 -0
- package/dist/src/lib/exit-when-written.js +20 -0
- package/dist/src/lib/float-equal.js +69 -0
- package/dist/src/lib/http-json.js +22 -0
- package/dist/src/lib/log.js +161 -0
- package/dist/src/lib/pg-numeric.js +489 -0
- package/dist/src/lib/port.js +22 -0
- package/dist/src/lib/project-file.js +49 -0
- package/dist/src/lib/project-files.js +43 -0
- package/dist/src/lib/project-version.js +76 -0
- package/dist/src/lib/runnable.js +14 -0
- package/dist/src/lib/shell-word.js +4 -0
- package/dist/src/lib/timezone.js +237 -0
- package/dist/src/lib/zone-names.js +114 -0
- package/dist/src/web/app.css +76 -0
- package/dist/src/web/app.js +491 -0
- package/dist/src/web/index.html +44 -0
- package/package.json +22 -4
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POSTGRES DECIDES, DUCKDB COMPUTES (docs/DESIGN.md). Before DuckDB answers a statement, the catalog Postgres
|
|
3
|
+
* (src/api/postgres/catalog.ts: Postgres 18 itself, holding every copied table under its source types, no rows) parses
|
|
4
|
+
* and analyses it, and that analysis is the answer's contract:
|
|
5
|
+
* - a statement Postgres REFUSES at analysis (an unknown column, a GROUP BY it would not take, `'99.99'` compared with
|
|
6
|
+
* an integer, ORDER BY outside a DISTINCT's list) is refused with Postgres's own error, word for word;
|
|
7
|
+
* - the result's column NAMES, type OIDs and typmods are Postgres's RowDescription, and the parameters' types its
|
|
8
|
+
* ParameterDescription;
|
|
9
|
+
* - the analysed tree (`debug_print_parse`, src/duck/pgtree.ts) gives the rewrite every expression's resolved type.
|
|
10
|
+
* The statement is analysed under the SESSION's search_path (an unqualified table is the one the session would read).
|
|
11
|
+
*
|
|
12
|
+
* ONE BATCH, SERIALISED (the catalog's queue): the logging is switched on, the statement is parsed, described and
|
|
13
|
+
* closed under a name of its own, and everything is set back, before the next session's call runs. Nothing of it is
|
|
14
|
+
* left in the sidecar between calls — another session's Sync can lose nothing (tests/postgres-api/server.test.ts "a
|
|
15
|
+
* catalog statement's portal, beside another session's").
|
|
16
|
+
*/
|
|
17
|
+
import { cstr, decodeBackend, frame, int16, int32 } from "../api/postgres/protocol.js";
|
|
18
|
+
import { parseTree, typesByLocation } from "./pgtree.js";
|
|
19
|
+
const NAME = "__mason_analysis";
|
|
20
|
+
/** the catalog's TimeZone, set back after a statement set the session's (src/api/postgres/catalog.ts sets it at start) */
|
|
21
|
+
const CATALOG_ZONE = "SET TimeZone = 'UTC'";
|
|
22
|
+
const query = (sql) => frame("Q", cstr(sql));
|
|
23
|
+
/** Asks the catalog Postgres to analyse `sql` (with these declared parameter OIDs, 0 = infer). */
|
|
24
|
+
export async function analyze(run, sql, paramOids, searchPath = "public", timeZone = "UTC") {
|
|
25
|
+
const bytes = Buffer.concat([
|
|
26
|
+
// the session's search_path, pg_catalog after it as the catalog's own setup has it (catalog.ts)
|
|
27
|
+
// …and its TimeZone: Postgres parses a literal under it ('2026-01-01 12:00'::timestamptz is that zone's noon), and
|
|
28
|
+
// the rewrite writes the value it parsed (src/duck/rewrite.ts `aConst`)
|
|
29
|
+
query(`SET search_path = ${searchPath}, pg_catalog; SET TimeZone = '${timeZone.replace(/'/g, "''")}'; SET client_min_messages = log; SET debug_print_parse = on; SET debug_pretty_print = off`),
|
|
30
|
+
frame("P", cstr(NAME), cstr(sql), int16(paramOids.length), ...paramOids.map(int32)),
|
|
31
|
+
frame("D", Buffer.from("S"), cstr(NAME)),
|
|
32
|
+
frame("C", Buffer.from("S"), cstr(NAME)),
|
|
33
|
+
frame("S"),
|
|
34
|
+
// the catalog's own TimeZone back (catalog.ts sets UTC at its start): RESET would be the server's boot default, the
|
|
35
|
+
// machine's zone, under which the catalog then answered what reads no table
|
|
36
|
+
query(`RESET debug_print_parse; RESET debug_pretty_print; RESET client_min_messages; ${CATALOG_ZONE}; SET search_path = public, pg_catalog`),
|
|
37
|
+
]);
|
|
38
|
+
const messages = decodeBackend(Buffer.from(await run(bytes)));
|
|
39
|
+
// the replies to the first Query end at its ReadyForQuery; the statement's run to the next one
|
|
40
|
+
const z = messages.findIndex((m) => m.type === "ReadyForQuery");
|
|
41
|
+
const end = messages.findIndex((m, i) => i > z && m.type === "ReadyForQuery");
|
|
42
|
+
if (z < 0 || end < 0)
|
|
43
|
+
throw new Error(`the catalog Postgres's analysis came back out of step (${messages.map((m) => m.type).join(", ")}): report it`);
|
|
44
|
+
const mine = messages.slice(z + 1, end);
|
|
45
|
+
const refused = mine.find((m) => m.type === "ErrorResponse");
|
|
46
|
+
if (refused?.type === "ErrorResponse")
|
|
47
|
+
return { refused: refused.fields };
|
|
48
|
+
const firstError = messages.find((m) => m.type === "ErrorResponse");
|
|
49
|
+
if (firstError?.type === "ErrorResponse")
|
|
50
|
+
throw new Error(`the catalog Postgres could not switch its analysis log on or off: ${firstError.fields.M}`);
|
|
51
|
+
const log = mine.find((m) => m.type === "NoticeResponse" && /^\{QUERY\b/.test(m.fields.D ?? ""));
|
|
52
|
+
if (log?.type !== "NoticeResponse")
|
|
53
|
+
throw new Error("the catalog Postgres logged no parse tree for the statement: report it");
|
|
54
|
+
const tree = parseTree(log.fields.D);
|
|
55
|
+
const pd = mine.find((m) => m.type === "ParameterDescription");
|
|
56
|
+
const rd = mine.find((m) => m.type === "RowDescription");
|
|
57
|
+
return {
|
|
58
|
+
params: pd?.type === "ParameterDescription" ? pd.oids : [],
|
|
59
|
+
fields: rd?.type === "RowDescription" ? rd.fields.map((f) => ({ name: f.name, oid: f.typeOid, typmod: f.typeModifier })) : null,
|
|
60
|
+
types: typesByLocation(tree),
|
|
61
|
+
tree,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** the types whose input DuckDB reads otherwise than Postgres, as the catalog Postgres writes their value back */
|
|
65
|
+
const PARAM_TYPES = {
|
|
66
|
+
16: "boolean", 21: "smallint", 23: "integer", 20: "bigint", 700: "real", 701: "double precision", 1700: "numeric",
|
|
67
|
+
2950: "uuid", 114: "json", 3802: "jsonb", 1083: "time", 1082: "date", 1114: "timestamp", 1184: "timestamptz", 1186: "interval", 869: "inet",
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* A statement's parameters as Postgres reads them: each value through its type's input function (the type Postgres
|
|
71
|
+
* inferred, or the one declared), under the session's TimeZone — so `'1.5'` for a smallint is Postgres's 22P02, and
|
|
72
|
+
* `'01/02/2026'` for a date is 2026-01-02. A value comes back as text DuckDB reads to the same value: a number, a
|
|
73
|
+
* boolean or a uuid canonical; a date as days and a timestamp as microseconds since 2000-01-01 (src/duck/rewrite.ts
|
|
74
|
+
* `datumText` writes them), an interval as its months, days and microseconds; `infinity` as itself.
|
|
75
|
+
*/
|
|
76
|
+
export async function readParameters(run, oids, values, timeZone) {
|
|
77
|
+
const quote = (s) => `'${s.replace(/'/g, "''")}'`;
|
|
78
|
+
const items = values.map((v, i) => {
|
|
79
|
+
const oid = oids[i] ?? 0, type = PARAM_TYPES[oid];
|
|
80
|
+
if (v === null || type === undefined)
|
|
81
|
+
return null;
|
|
82
|
+
const x = `${quote(v)}::${type}`;
|
|
83
|
+
if (oid === 1082)
|
|
84
|
+
return `CASE WHEN isfinite(${x}) THEN (${x} - date '2000-01-01')::text ELSE ${x}::text END`;
|
|
85
|
+
if (oid === 1114 || oid === 1184)
|
|
86
|
+
return `CASE WHEN isfinite(${x}) THEN (extract(epoch FROM ${x}) * 1000000 - 946684800000000)::int8::text ELSE ${x}::text END`;
|
|
87
|
+
if (oid === 1186)
|
|
88
|
+
return `(extract(year FROM ${x}) * 12 + extract(month FROM ${x}))::text || ' ' || extract(day FROM ${x})::text || ' ' || ((extract(hour FROM ${x}) * 60 + extract(minute FROM ${x})) * 60000000 + extract(microseconds FROM ${x}))::int8::text`;
|
|
89
|
+
return `${x}::text`;
|
|
90
|
+
});
|
|
91
|
+
if (items.every((x) => x === null))
|
|
92
|
+
return { values: values.map(() => null) };
|
|
93
|
+
const sql = `SET TimeZone = ${quote(timeZone)}; SELECT ${items.map((x) => x ?? "NULL").join(", ")}; ${CATALOG_ZONE}`;
|
|
94
|
+
const messages = decodeBackend(Buffer.from(await run(query(sql))));
|
|
95
|
+
const refused = messages.find((m) => m.type === "ErrorResponse");
|
|
96
|
+
if (refused?.type === "ErrorResponse") {
|
|
97
|
+
// the value's own error (22P02, 22003, 22008 …) — and the zone set back, which the failed statement skipped
|
|
98
|
+
await run(query(CATALOG_ZONE));
|
|
99
|
+
return { refused: refused.fields };
|
|
100
|
+
}
|
|
101
|
+
const row = messages.find((m) => m.type === "DataRow");
|
|
102
|
+
if (row?.type !== "DataRow")
|
|
103
|
+
throw new Error("the catalog Postgres read the parameters and answered no row: report it");
|
|
104
|
+
return { values: items.map((x, i) => x === null ? null : { oid: oids[i], text: row.values[i].toString("utf8") }) };
|
|
105
|
+
}
|
|
106
|
+
/** One read of the catalog Postgres's own catalog (a signature from pg_proc, a type's name): its rows as text. */
|
|
107
|
+
export async function catalogRows(run, sql) {
|
|
108
|
+
const messages = decodeBackend(Buffer.from(await run(query(sql))));
|
|
109
|
+
const refused = messages.find((m) => m.type === "ErrorResponse");
|
|
110
|
+
if (refused?.type === "ErrorResponse")
|
|
111
|
+
throw new Error(`the catalog Postgres refused its own catalog read: ${refused.fields.M} (in: ${sql.slice(0, 200)})`);
|
|
112
|
+
return messages.flatMap((m) => m.type === "DataRow" ? [m.values.map((v) => v === null ? null : v.toString("utf8"))] : []);
|
|
113
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE CANCEL TABLE (the executor worker: src/duck/executor.ts). A statement that calls a Mason function is
|
|
3
|
+
* cancelled through its SESSION as well as interrupted (src/duck/functions.ts "every call carries its session"): its
|
|
4
|
+
* next call reads this table and raises, and its result is closed only once its calls have stopped (store.ts `cleanUp`,
|
|
5
|
+
* the suite's hang). The calls run on the executor worker, which such a statement keeps busy between
|
|
6
|
+
* DuckDB's turns for seconds at a time, so a cancel cannot be a message to it: the listener (the main thread) writes
|
|
7
|
+
* the table directly, in memory both threads share, and the next call sees it at once.
|
|
8
|
+
*
|
|
9
|
+
* Two Int32 cells per session SLOT (a wire session's DuckDB connection holds one while it is open): `cancelled`, the
|
|
10
|
+
* highest statement number of the session cancelled so far, and `running`, the number of the statement its connection
|
|
11
|
+
* runs now (set by the worker once the one before it has been released). A call is cancelled when `cancelled >=
|
|
12
|
+
* running`: a cancel of a statement already past never reaches the next one, a cancel that lands before its statement
|
|
13
|
+
* starts still does, and a statement being drained after its cancel stays cancelled whatever the next one's number.
|
|
14
|
+
* Dependency-free: the listener's thread loads it without DuckDB.
|
|
15
|
+
*/
|
|
16
|
+
/** how many wire sessions may hold a DuckDB connection at once (a slot each) */
|
|
17
|
+
export const CANCEL_SLOTS = 4096;
|
|
18
|
+
/** a fresh table (the executor makes one and hands it to its worker) */
|
|
19
|
+
export function cancelTable() { return new Int32Array(new SharedArrayBuffer(CANCEL_SLOTS * 2 * 4)); }
|
|
20
|
+
const cancelledAt = (slot) => slot * 2;
|
|
21
|
+
const runningAt = (slot) => slot * 2 + 1;
|
|
22
|
+
/** a slot handed to a new session: nothing cancelled, nothing running */
|
|
23
|
+
export function resetSlot(t, slot) { Atomics.store(t, cancelledAt(slot), 0); Atomics.store(t, runningAt(slot), 0); }
|
|
24
|
+
/** statement `seq` of the session in `slot` is cancelled (and every one before it): monotonic, from either thread */
|
|
25
|
+
export function cancelStatement(t, slot, seq) {
|
|
26
|
+
for (let was = Atomics.load(t, cancelledAt(slot)); was < seq; was = Atomics.load(t, cancelledAt(slot))) {
|
|
27
|
+
if (Atomics.compareExchange(t, cancelledAt(slot), was, seq) === was)
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** statement `seq` is the one the session's connection runs now */
|
|
32
|
+
export function beginStatement(t, slot, seq) { Atomics.store(t, runningAt(slot), seq); }
|
|
33
|
+
/** whether the statement the session's connection runs now is cancelled */
|
|
34
|
+
export function isCancelled(t, slot) {
|
|
35
|
+
const running = Atomics.load(t, runningAt(slot));
|
|
36
|
+
return running !== 0 && Atomics.load(t, cancelledAt(slot)) >= running;
|
|
37
|
+
}
|
|
38
|
+
/** whether statement `seq` of the session in `slot` was cancelled (its cancel asked, whether or not a call saw it) */
|
|
39
|
+
export function statementCancelled(t, slot, seq) {
|
|
40
|
+
return seq !== 0 && Atomics.load(t, cancelledAt(slot)) >= seq;
|
|
41
|
+
}
|
|
42
|
+
/** the session variable's value for a slot, and back (`s<slot>`; null for what is no session's) */
|
|
43
|
+
export const slotName = (slot) => `s${slot}`;
|
|
44
|
+
export function slotOf(name) {
|
|
45
|
+
if (name === null || !/^s\d+$/.test(name))
|
|
46
|
+
return null;
|
|
47
|
+
const n = Number(name.slice(1));
|
|
48
|
+
return n < CANCEL_SLOTS ? n : null;
|
|
49
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/** pg_proc's count(any) */
|
|
2
|
+
const COUNT_ANY = 2147;
|
|
3
|
+
const RTE_RELATION = 0;
|
|
4
|
+
/** JoinType: INNER, LEFT, FULL, RIGHT (nodes.h); the planner's SEMI / ANTI never reach a parse tree */
|
|
5
|
+
const JOINS = new Set([0, 1, 2, 3]);
|
|
6
|
+
export const columnKey = (relid, attno) => `${relid}.${attno}`;
|
|
7
|
+
const isNode = (v) => v !== null && v !== undefined && typeof v === "object" && !Array.isArray(v);
|
|
8
|
+
const num = (v) => typeof v === "string" && /^-?\d+$/.test(v) ? Number(v) : null;
|
|
9
|
+
const list = (v) => Array.isArray(v) ? v : [];
|
|
10
|
+
/** whether an AGGREF is a count(DISTINCT …) of one argument: the only aggregate this rewrite touches */
|
|
11
|
+
const isDistinctCount = (a) => a._node === "AGGREF" && num(a.aggfnoid) === COUNT_ANY && list(a.aggdistinct).length === 1;
|
|
12
|
+
/**
|
|
13
|
+
* The tables a statement's count(DISTINCT …) may be decided over: every base table of its queries, by the catalog's
|
|
14
|
+
* OID, when it holds such a count at all (none: the plan asks the catalog nothing). Read before the decision, so the
|
|
15
|
+
* catalog is asked once per statement planned.
|
|
16
|
+
*/
|
|
17
|
+
export function distinctCountTables(tree) {
|
|
18
|
+
let counts = false;
|
|
19
|
+
const relids = new Set();
|
|
20
|
+
const visit = (v) => {
|
|
21
|
+
if (Array.isArray(v)) {
|
|
22
|
+
v.forEach(visit);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (!isNode(v))
|
|
26
|
+
return;
|
|
27
|
+
if (isDistinctCount(v))
|
|
28
|
+
counts = true;
|
|
29
|
+
if (v._node === "RANGETBLENTRY" && num(v.rtekind) === RTE_RELATION) {
|
|
30
|
+
const r = num(v.relid);
|
|
31
|
+
if (r !== null && r > 0)
|
|
32
|
+
relids.add(r);
|
|
33
|
+
}
|
|
34
|
+
for (const [k, x] of Object.entries(v))
|
|
35
|
+
if (k !== "_node")
|
|
36
|
+
visit(x);
|
|
37
|
+
};
|
|
38
|
+
visit(tree);
|
|
39
|
+
return counts ? [...relids].sort((a, b) => a - b) : [];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The catalog Postgres's single-column PRIMARY KEYS of `relids` (never another unique index: the module's doc), with
|
|
43
|
+
* each one's collation and the equality operators of its btree family. A primary key is valid, unique and over a plain
|
|
44
|
+
* column; the conditions are written out all the same, so the rule is this query's and not a reader's memory.
|
|
45
|
+
*/
|
|
46
|
+
export function uniqueColumnsSql(relids) {
|
|
47
|
+
return "SELECT i.indrelid::text, i.indkey[0]::text, i.indcollation[0]::text, coalesce(string_agg(o.amopopr::text, ' ' ORDER BY o.amopopr), '') " +
|
|
48
|
+
"FROM pg_index i JOIN pg_opclass c ON c.oid = i.indclass[0] JOIN pg_am m ON m.oid = c.opcmethod AND m.amname = 'btree' " +
|
|
49
|
+
"LEFT JOIN pg_amop o ON o.amopfamily = c.opcfamily AND o.amopmethod = c.opcmethod AND o.amopstrategy = 3 " +
|
|
50
|
+
`WHERE i.indrelid IN (${relids.map((r) => String(Math.trunc(r))).join(", ")}) AND i.indisprimary AND i.indisunique AND i.indisvalid AND i.indnatts = 1 AND i.indnkeyatts = 1 ` +
|
|
51
|
+
"AND i.indkey[0] > 0 AND i.indpred IS NULL AND i.indexprs IS NULL GROUP BY 1, 2, 3";
|
|
52
|
+
}
|
|
53
|
+
/** `uniqueColumnsSql`'s rows, as the decision reads them (a table has one primary key; the merge is for a second row
|
|
54
|
+
* of one column, which no catalog writes) */
|
|
55
|
+
export function uniqueColumnsOf(rows) {
|
|
56
|
+
const out = new Map();
|
|
57
|
+
for (const [relid, attno, collation, ops] of rows) {
|
|
58
|
+
const key = columnKey(Number(relid), Number(attno));
|
|
59
|
+
const equalities = new Set((ops ?? "").split(" ").filter((x) => x !== "").map(Number));
|
|
60
|
+
const had = out.get(key);
|
|
61
|
+
// two rows for one column under two collations (none today): either's equality, under its own collation, is the key's;
|
|
62
|
+
// kept as the first (the decision then asks that one's), which is conservative
|
|
63
|
+
if (had === undefined)
|
|
64
|
+
out.set(key, { collation: Number(collation ?? 0), equalities });
|
|
65
|
+
else if (had.collation === Number(collation ?? 0))
|
|
66
|
+
out.set(key, { collation: had.collation, equalities: new Set([...had.equalities, ...equalities]) });
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
/** a VAR of this query's own FROM (varlevelsup 0), bare or under a binary-compatible relabel (a varchar read as text) */
|
|
71
|
+
function ownVar(v) {
|
|
72
|
+
if (!isNode(v))
|
|
73
|
+
return null;
|
|
74
|
+
if (v._node === "RELABELTYPE")
|
|
75
|
+
return ownVar(v.arg);
|
|
76
|
+
return v._node === "VAR" && (num(v.varlevelsup) ?? 0) === 0 && (num(v.varattno) ?? 0) >= 1 ? v : null;
|
|
77
|
+
}
|
|
78
|
+
/** the rtindexes a join tree's node holds (its tables', and its joins' own: a USING join's column is the join's) */
|
|
79
|
+
function indexesOf(n) {
|
|
80
|
+
const out = new Set();
|
|
81
|
+
const visit = (v) => {
|
|
82
|
+
if (!isNode(v))
|
|
83
|
+
return;
|
|
84
|
+
if (v._node === "RANGETBLREF") {
|
|
85
|
+
const i = num(v.rtindex);
|
|
86
|
+
if (i !== null)
|
|
87
|
+
out.add(i);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (v._node === "JOINEXPR") {
|
|
91
|
+
const i = num(v.rtindex);
|
|
92
|
+
if (i !== null)
|
|
93
|
+
out.add(i);
|
|
94
|
+
visit(v.larg);
|
|
95
|
+
visit(v.rarg);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
visit(n);
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
/** a conjunction's conjuncts (outfuncs.c writes BOOLEXPR's boolop as `and`, `or`, `not`), or the one condition */
|
|
102
|
+
function conjuncts(q) {
|
|
103
|
+
if (!isNode(q))
|
|
104
|
+
return [];
|
|
105
|
+
if (q._node === "BOOLEXPR" && q.boolop === "and")
|
|
106
|
+
return list(q.args).flatMap((a) => conjuncts(a));
|
|
107
|
+
return [q];
|
|
108
|
+
}
|
|
109
|
+
/** the base table a FROM's RANGETBLREF names: its RANGETBLENTRY, when it is one (a relation, not a subquery, a CTE, a
|
|
110
|
+
* function, VALUES or a join) */
|
|
111
|
+
function baseTable(rtable, ref) {
|
|
112
|
+
if (!isNode(ref) || ref._node !== "RANGETBLREF")
|
|
113
|
+
return null;
|
|
114
|
+
const rtindex = num(ref.rtindex);
|
|
115
|
+
const rte = rtindex === null ? undefined : rtable[rtindex - 1];
|
|
116
|
+
if (!isNode(rte) || num(rte.rtekind) !== RTE_RELATION || rte.relkind !== "r")
|
|
117
|
+
return null;
|
|
118
|
+
const relid = num(rte.relid);
|
|
119
|
+
return rtindex !== null && relid !== null && relid > 0 ? { rtindex, relid } : null;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Whether a join's condition matches each row of its `side` with at most ONE row of `other`, a base table: a conjunct
|
|
123
|
+
* that is an equality, by the operator of `other`'s single-column primary key and under its collation, of that
|
|
124
|
+
* column (bare) with a column of `side` (bare).
|
|
125
|
+
*/
|
|
126
|
+
function atMostOne(quals, side, other, unique) {
|
|
127
|
+
return conjuncts(quals).some((c) => {
|
|
128
|
+
if (c._node !== "OPEXPR" || list(c.args).length !== 2)
|
|
129
|
+
return false;
|
|
130
|
+
const [a, b] = list(c.args).map(ownVar);
|
|
131
|
+
if (a == null || b == null)
|
|
132
|
+
return false;
|
|
133
|
+
for (const [mine, theirs] of [[a, b], [b, a]]) {
|
|
134
|
+
if (num(theirs.varno) !== other.rtindex || !side.has(num(mine.varno) ?? -1))
|
|
135
|
+
continue;
|
|
136
|
+
const u = unique.get(columnKey(other.relid, num(theirs.varattno)));
|
|
137
|
+
const collation = num(c.inputcollid) ?? 0;
|
|
138
|
+
if (u !== undefined && u.equalities.has(num(c.opno) ?? -1) && (collation === 0 || collation === u.collation))
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
return false;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Whether the FROM of `query` holds each row of the table at `rtindex` at most once: its one item, that table or a
|
|
146
|
+
* join tree whose every join on the way down to it joins one base table on that table's primary key (`atMostOne`).
|
|
147
|
+
*/
|
|
148
|
+
export function notMultiplied(query, rtindex, unique) {
|
|
149
|
+
const jt = query.jointree;
|
|
150
|
+
if (!isNode(jt))
|
|
151
|
+
return false;
|
|
152
|
+
const from = list(jt.fromlist);
|
|
153
|
+
if (from.length !== 1)
|
|
154
|
+
return false;
|
|
155
|
+
const rtable = list(query.rtable);
|
|
156
|
+
const down = (n) => {
|
|
157
|
+
if (!isNode(n))
|
|
158
|
+
return false;
|
|
159
|
+
if (n._node === "RANGETBLREF")
|
|
160
|
+
return num(n.rtindex) === rtindex;
|
|
161
|
+
if (n._node !== "JOINEXPR" || !JOINS.has(num(n.jointype) ?? -1) || !isNode(n.larg) || !isNode(n.rarg))
|
|
162
|
+
return false;
|
|
163
|
+
const left = indexesOf(n.larg);
|
|
164
|
+
const [side, otherRef] = left.has(rtindex) ? [n.larg, n.rarg] : indexesOf(n.rarg).has(rtindex) ? [n.rarg, n.larg] : [null, null];
|
|
165
|
+
if (side === null)
|
|
166
|
+
return false;
|
|
167
|
+
const other = baseTable(rtable, otherRef);
|
|
168
|
+
return other !== null && atMostOne(n.quals, indexesOf(side), other, unique) && down(side);
|
|
169
|
+
};
|
|
170
|
+
return down(from[0]);
|
|
171
|
+
}
|
|
172
|
+
/** Whether this AGGREF of `query` is a count(DISTINCT x) whose DISTINCT changes nothing (the module's doc). */
|
|
173
|
+
function distinctOfKey(query, a, unique) {
|
|
174
|
+
if (!isDistinctCount(a) || (num(a.agglevelsup) ?? 0) !== 0 || a.aggkind !== "n" || a.aggstar === "true" || list(a.aggorder).length > 0)
|
|
175
|
+
return false;
|
|
176
|
+
const args = list(a.args);
|
|
177
|
+
const arg = args.length === 1 && isNode(args[0]) && args[0]._node === "TARGETENTRY" ? args[0].expr : undefined;
|
|
178
|
+
// the column itself: a cast or an expression of it need not be unique (`id / 2`, `id::text` under another equality)
|
|
179
|
+
if (!isNode(arg) || arg._node !== "VAR" || (num(arg.varlevelsup) ?? 0) !== 0)
|
|
180
|
+
return false;
|
|
181
|
+
const varno = num(arg.varno), attno = num(arg.varattno);
|
|
182
|
+
if (varno === null || attno === null || attno < 1)
|
|
183
|
+
return false;
|
|
184
|
+
const table = baseTable(list(query.rtable), { _node: "RANGETBLREF", rtindex: String(varno) });
|
|
185
|
+
if (table === null)
|
|
186
|
+
return false;
|
|
187
|
+
const u = unique.get(columnKey(table.relid, attno));
|
|
188
|
+
const distinct = list(a.aggdistinct)[0];
|
|
189
|
+
const collation = num(a.inputcollid) ?? 0;
|
|
190
|
+
if (u === undefined || !isNode(distinct) || !u.equalities.has(num(distinct.eqop) ?? -1) || (collation !== 0 && collation !== u.collation))
|
|
191
|
+
return false;
|
|
192
|
+
return notMultiplied(query, varno, unique);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* THE DECISION: the offsets (into the analysed text) of every count(DISTINCT x) of the statement whose DISTINCT may be
|
|
196
|
+
* dropped — each AGGREF decided in the query it belongs to (an outer query's aggregate inside a subquery,
|
|
197
|
+
* agglevelsup > 0, is left as it is).
|
|
198
|
+
*/
|
|
199
|
+
export function distinctKeyCounts(tree, unique) {
|
|
200
|
+
const out = new Set();
|
|
201
|
+
if (unique.size === 0)
|
|
202
|
+
return out;
|
|
203
|
+
const visit = (v, query) => {
|
|
204
|
+
if (Array.isArray(v)) {
|
|
205
|
+
for (const x of v)
|
|
206
|
+
visit(x, query);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (!isNode(v))
|
|
210
|
+
return;
|
|
211
|
+
if (v._node === "QUERY") {
|
|
212
|
+
for (const [k, x] of Object.entries(v))
|
|
213
|
+
if (k !== "_node")
|
|
214
|
+
visit(x, v);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (query !== null && v._node === "AGGREF" && distinctOfKey(query, v, unique)) {
|
|
218
|
+
const at = num(v.location);
|
|
219
|
+
if (at !== null && at >= 0)
|
|
220
|
+
out.add(at);
|
|
221
|
+
}
|
|
222
|
+
for (const [k, x] of Object.entries(v))
|
|
223
|
+
if (k !== "_node")
|
|
224
|
+
visit(x, query);
|
|
225
|
+
};
|
|
226
|
+
visit(tree, null);
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE EXECUTOR WORKER'S SIDE (src/duck/executor.ts says why it exists): the one DuckDB instance of the copy's file,
|
|
3
|
+
* every connection of it, and the Mason functions, registered from THIS thread so that every call DuckDB's executor
|
|
4
|
+
* makes lands here and never on the listener's thread. It answers the requests the main thread posts — a connection's
|
|
5
|
+
* statements (`conn.*`), a wire session's (`runner.*`, `answer.*`, run by src/duck/store.ts SessionRunner as before)
|
|
6
|
+
* — each with its id, and says its log lines as messages.
|
|
7
|
+
*
|
|
8
|
+
* THIS thread never waits for the executor (src/duck/store.ts "HOW A STATEMENT IS CANCELLED"): a result or a
|
|
9
|
+
* connection is closed only once DuckDB has handed it back. A request is answered asynchronously, so the turns between
|
|
10
|
+
* a statement's function calls serve the others; a statement's cancel does not need one (the cancel table,
|
|
11
|
+
* src/duck/cancel.ts).
|
|
12
|
+
*/
|
|
13
|
+
import { parentPort, workerData } from "node:worker_threads";
|
|
14
|
+
import { openDuck, SessionRunner } from "./store.js";
|
|
15
|
+
import { FUNCTION_CALLS, FUNCTION_ROWS, registerLibcOrder, useCancelTable } from "./functions.js";
|
|
16
|
+
import { stopPgEval } from "./pg-eval.js";
|
|
17
|
+
import { StatementError } from "../api/postgres/server.js";
|
|
18
|
+
import { STOPPING, wireError } from "./executor.js";
|
|
19
|
+
const { cancels } = workerData;
|
|
20
|
+
useCancelTable(cancels);
|
|
21
|
+
const port = parentPort;
|
|
22
|
+
let instance = null;
|
|
23
|
+
/** the connections by number: 0 is the writer (the store's), the rest `connect`'s */
|
|
24
|
+
const conns = new Map();
|
|
25
|
+
let nextConn = 1;
|
|
26
|
+
const runners = new Map();
|
|
27
|
+
let nextRunner = 1;
|
|
28
|
+
/** each session's close, from its start until it ends (`runner.close`, or the shutdown's): the shutdown waits for every
|
|
29
|
+
* one before it closes the instance — one it did not wait for closed the connections and the instance under a drain's
|
|
30
|
+
* pending fetch, and the process aborted (a review: exit 134, an uncaught Napi::Error) */
|
|
31
|
+
const closing = new Map();
|
|
32
|
+
/** every connection call still running, by connection: the shutdown closes a connection only once its calls settled */
|
|
33
|
+
const connCalls = new Map();
|
|
34
|
+
/** set by `shutdown`: a session's new statement is refused 57P01, a connection's new call refused */
|
|
35
|
+
let stopping = false;
|
|
36
|
+
const statements = new Map();
|
|
37
|
+
const key = (runner, seq) => `${runner}:${seq}`;
|
|
38
|
+
const log = (line) => { port.postMessage({ log: line }); };
|
|
39
|
+
/** an abort's reason as the main thread's signal has it: the statement timeout's error, else none (the client's cancel) */
|
|
40
|
+
const abortWith = (c, reason) => {
|
|
41
|
+
if (!c.signal.aborted) {
|
|
42
|
+
if (reason.fields !== undefined)
|
|
43
|
+
c.abort(new StatementError(reason.fields, ""));
|
|
44
|
+
else
|
|
45
|
+
c.abort();
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
/** a connection's call, held in `connCalls` until it settles */
|
|
49
|
+
function onConn(n, f) {
|
|
50
|
+
if (stopping)
|
|
51
|
+
return Promise.reject(new Error("the copy is closing: no statement starts on it any more"));
|
|
52
|
+
const p = f(need(conns, n, "connection"));
|
|
53
|
+
const calls = connCalls.get(n) ?? new Set();
|
|
54
|
+
connCalls.set(n, calls);
|
|
55
|
+
calls.add(p);
|
|
56
|
+
void p.then(() => calls.delete(p), () => calls.delete(p));
|
|
57
|
+
return p;
|
|
58
|
+
}
|
|
59
|
+
/** a session's close, once: started by its `runner.close` or by the shutdown, whichever comes first */
|
|
60
|
+
function closeRunner(n) {
|
|
61
|
+
const already = closing.get(n);
|
|
62
|
+
if (already !== undefined)
|
|
63
|
+
return already;
|
|
64
|
+
const runner = need(runners, n, "session");
|
|
65
|
+
runners.delete(n);
|
|
66
|
+
const p = runner.close();
|
|
67
|
+
closing.set(n, p);
|
|
68
|
+
void p.then(() => closing.delete(n), () => closing.delete(n));
|
|
69
|
+
return p;
|
|
70
|
+
}
|
|
71
|
+
const need = (m, n, what) => {
|
|
72
|
+
const v = m.get(n);
|
|
73
|
+
if (v === undefined)
|
|
74
|
+
throw new Error(`no ${what} ${n} on the copy's thread (Mason bug: report it)`);
|
|
75
|
+
return v;
|
|
76
|
+
};
|
|
77
|
+
async function answer(m) {
|
|
78
|
+
// a session's statement asked once the stop began: told so, never started under the closing instance
|
|
79
|
+
if (stopping && ["runner.open", "runner.run", "runner.explain", "answer.fetch"].includes(m.op))
|
|
80
|
+
throw new StatementError(STOPPING, "");
|
|
81
|
+
switch (m.op) {
|
|
82
|
+
case "open": {
|
|
83
|
+
const opened = await openDuck(String(m.file), m.settings);
|
|
84
|
+
instance = opened.instance;
|
|
85
|
+
conns.set(0, opened.writer);
|
|
86
|
+
return { value: null };
|
|
87
|
+
}
|
|
88
|
+
case "connect": {
|
|
89
|
+
if (instance === null)
|
|
90
|
+
throw new Error("the copy is not open (Mason bug: report it)");
|
|
91
|
+
const n = nextConn++;
|
|
92
|
+
conns.set(n, await instance.connect());
|
|
93
|
+
return { value: n };
|
|
94
|
+
}
|
|
95
|
+
case "conn.run": {
|
|
96
|
+
const r = await onConn(Number(m.conn), (c) => c.run(String(m.sql), m.values));
|
|
97
|
+
return { value: { rowsChanged: r.rowsChanged } };
|
|
98
|
+
}
|
|
99
|
+
case "conn.read": {
|
|
100
|
+
const r = await onConn(Number(m.conn), (c) => c.runAndReadAll(String(m.sql), m.values));
|
|
101
|
+
return { value: { js: r.getRowsJS(), json: r.getRowsJson(), rows: r.getRows(), names: r.columnNames(),
|
|
102
|
+
types: Array.from({ length: r.columnCount }, (_, i) => r.columnType(i).toJson()) } };
|
|
103
|
+
}
|
|
104
|
+
case "conn.prepare": {
|
|
105
|
+
const p = await onConn(Number(m.conn), (c) => c.prepare(String(m.sql)));
|
|
106
|
+
try {
|
|
107
|
+
return { value: { names: Array.from({ length: p.columnCount }, (_, i) => p.columnName(i)), types: Array.from({ length: p.columnCount }, (_, i) => p.columnType(i).toJson()) } };
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
p.destroySync();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
case "conn.interrupt":
|
|
114
|
+
conns.get(Number(m.conn))?.interrupt();
|
|
115
|
+
return undefined;
|
|
116
|
+
case "conn.close": {
|
|
117
|
+
const c = conns.get(Number(m.conn));
|
|
118
|
+
conns.delete(Number(m.conn));
|
|
119
|
+
try {
|
|
120
|
+
c?.closeSync();
|
|
121
|
+
}
|
|
122
|
+
catch (e) {
|
|
123
|
+
log(`closing a connection of the copy failed: ${e.message}`);
|
|
124
|
+
}
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
case "libc":
|
|
128
|
+
registerLibcOrder(need(conns, 0, "connection"), m.order);
|
|
129
|
+
return { value: null };
|
|
130
|
+
case "calls": return { value: Object.fromEntries(FUNCTION_CALLS) };
|
|
131
|
+
case "function rows": return { value: Object.fromEntries(FUNCTION_ROWS) };
|
|
132
|
+
case "runner.open": {
|
|
133
|
+
if (instance === null)
|
|
134
|
+
throw new Error("the copy is not open (Mason bug: report it)");
|
|
135
|
+
const n = nextRunner++;
|
|
136
|
+
runners.set(n, await SessionRunner.open(instance, log, Number(m.slot), cancels));
|
|
137
|
+
return { value: n };
|
|
138
|
+
}
|
|
139
|
+
case "runner.run": {
|
|
140
|
+
const runner = need(runners, Number(m.runner), "session");
|
|
141
|
+
const k = key(Number(m.runner), Number(m.seq));
|
|
142
|
+
const s = { controller: new AbortController(), answer: null };
|
|
143
|
+
if (m.aborted !== null)
|
|
144
|
+
abortWith(s.controller, m.aborted);
|
|
145
|
+
statements.set(k, s);
|
|
146
|
+
runner.next = Number(m.seq);
|
|
147
|
+
try {
|
|
148
|
+
const a = await runner.run(m.sql, m.params, m.session, s.controller.signal, m.fields, m.exact);
|
|
149
|
+
s.answer = a;
|
|
150
|
+
return { value: a.columns.map((c) => ({ name: c.name, oid: c.oid, size: c.size })) };
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
statements.delete(k);
|
|
154
|
+
throw e;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
case "runner.abort": {
|
|
158
|
+
const s = statements.get(key(Number(m.runner), Number(m.seq)));
|
|
159
|
+
if (s !== undefined && m.reason !== null)
|
|
160
|
+
abortWith(s.controller, m.reason);
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
case "answer.fetch": {
|
|
164
|
+
const k = key(Number(m.runner), Number(m.seq));
|
|
165
|
+
const s = statements.get(k);
|
|
166
|
+
if (s?.answer == null)
|
|
167
|
+
return { value: null };
|
|
168
|
+
let rows;
|
|
169
|
+
try {
|
|
170
|
+
rows = await s.answer.fetch(m.formats, { timeZone: String(m.timeZone) });
|
|
171
|
+
}
|
|
172
|
+
catch (e) {
|
|
173
|
+
statements.delete(k);
|
|
174
|
+
throw e;
|
|
175
|
+
}
|
|
176
|
+
if (rows === null) {
|
|
177
|
+
statements.delete(k);
|
|
178
|
+
return { value: null };
|
|
179
|
+
}
|
|
180
|
+
// one buffer a chunk, moved (never copied) to the listener's thread, and where each row ends in it
|
|
181
|
+
let size = 0;
|
|
182
|
+
for (const r of rows)
|
|
183
|
+
size += r.length;
|
|
184
|
+
const bytes = new ArrayBuffer(size);
|
|
185
|
+
const into = new Uint8Array(bytes);
|
|
186
|
+
const ends = new Array(rows.length);
|
|
187
|
+
let at = 0;
|
|
188
|
+
rows.forEach((r, i) => { into.set(r, at); at += r.length; ends[i] = at; });
|
|
189
|
+
return { value: { bytes, ends }, transfer: [bytes] };
|
|
190
|
+
}
|
|
191
|
+
case "answer.close": {
|
|
192
|
+
// answered once DuckDB handed the answer back (its drain, src/duck/store.ts): until then the main thread holds the
|
|
193
|
+
// process open, which must never end while DuckDB calls into this thread (src/duck/executor.ts "NOTHING ENDS")
|
|
194
|
+
const k = key(Number(m.runner), Number(m.seq));
|
|
195
|
+
statements.get(k)?.answer?.close();
|
|
196
|
+
statements.delete(k);
|
|
197
|
+
await (runners.get(Number(m.runner)) ?? null)?.idle();
|
|
198
|
+
await closing.get(Number(m.runner));
|
|
199
|
+
return { value: null };
|
|
200
|
+
}
|
|
201
|
+
case "runner.explain": {
|
|
202
|
+
const runner = need(runners, Number(m.runner), "session");
|
|
203
|
+
const k = key(Number(m.runner), Number(m.seq));
|
|
204
|
+
const s = { controller: new AbortController(), answer: null };
|
|
205
|
+
if (m.aborted !== null)
|
|
206
|
+
abortWith(s.controller, m.aborted);
|
|
207
|
+
statements.set(k, s);
|
|
208
|
+
runner.next = Number(m.seq);
|
|
209
|
+
try {
|
|
210
|
+
return { value: await runner.explain(m.sql, m.params, m.session, s.controller.signal, m.how) };
|
|
211
|
+
}
|
|
212
|
+
finally {
|
|
213
|
+
statements.delete(k);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
case "runner.close": {
|
|
217
|
+
await closeRunner(Number(m.runner));
|
|
218
|
+
return { value: null };
|
|
219
|
+
}
|
|
220
|
+
case "shutdown": {
|
|
221
|
+
stopping = true;
|
|
222
|
+
// THE STOP (mason stop, a shutdown): every statement still in flight is stopped as a cancel stops it — through its
|
|
223
|
+
// session when it calls a Mason function, interrupted when it calls none — with the stop as its reason, so its
|
|
224
|
+
// session is told 57P01 (STOPPING), never "ok" and never a hang; then every session's connection is closed once
|
|
225
|
+
// nothing runs on it (the listener's own closes, still in flight, included), and only then the connections and
|
|
226
|
+
// the instance. Nothing is closed under a pending call of DuckDB's: that aborted the process
|
|
227
|
+
for (const s of statements.values())
|
|
228
|
+
abortWith(s.controller, { fields: STOPPING });
|
|
229
|
+
for (const n of [...runners.keys()])
|
|
230
|
+
void closeRunner(n);
|
|
231
|
+
await Promise.allSettled([...closing.values()]);
|
|
232
|
+
// a connection's call left running (the sync and the request log stop theirs first): interrupted, and waited
|
|
233
|
+
// for — never closed under (a task of it inside a call waits for this thread)
|
|
234
|
+
for (const [n, calls] of connCalls)
|
|
235
|
+
if (calls.size > 0)
|
|
236
|
+
conns.get(n)?.interrupt();
|
|
237
|
+
for (const calls of connCalls.values())
|
|
238
|
+
await Promise.allSettled([...calls]);
|
|
239
|
+
for (const [n, c] of [...conns].reverse()) {
|
|
240
|
+
conns.delete(n);
|
|
241
|
+
c.closeSync();
|
|
242
|
+
}
|
|
243
|
+
instance?.closeSync();
|
|
244
|
+
instance = null;
|
|
245
|
+
await stopPgEval();
|
|
246
|
+
return { value: null };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
throw new Error(`the copy's thread was asked ${JSON.stringify(m.op)}, which it does not know (Mason bug: report it)`);
|
|
250
|
+
}
|
|
251
|
+
port.on("message", (m) => {
|
|
252
|
+
// the executor's watch (src/duck/executor.ts SILENT_MS): answered at once, by a thread whose loop turns
|
|
253
|
+
if (m.op === "ping") {
|
|
254
|
+
port.postMessage({ pong: true });
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
answer(m).then((r) => {
|
|
258
|
+
if (m.id === undefined)
|
|
259
|
+
return;
|
|
260
|
+
port.postMessage({ id: m.id, ok: true, value: r?.value ?? null }, r?.transfer ?? []);
|
|
261
|
+
}, (e) => {
|
|
262
|
+
if (m.id === undefined) {
|
|
263
|
+
log(`${m.op} failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
port.postMessage({ id: m.id, ok: false, error: wireError(e) });
|
|
267
|
+
});
|
|
268
|
+
});
|