@mason-data/cli 0.8.0 → 0.10.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 +19 -8
- package/dist/src/api/error.js +4 -0
- package/dist/src/api/postgres/catalog-worker.js +49 -2
- package/dist/src/api/postgres/catalog.js +166 -32
- package/dist/src/api/postgres/listener.js +11 -3
- package/dist/src/api/postgres/proxy.js +129 -0
- package/dist/src/api/postgres/schema.js +6 -0
- package/dist/src/api/postgres/server.js +527 -103
- package/dist/src/api/postgres/translate.js +47 -2
- package/dist/src/cli/background.js +18 -19
- package/dist/src/cli/bridge-stdin.js +101 -0
- package/dist/src/cli/command.js +337 -15
- package/dist/src/cli/commands.js +458 -169
- package/dist/src/cli/credentials.js +182 -0
- package/dist/src/cli/demo.js +2 -2
- package/dist/src/cli/deploy.js +680 -0
- package/dist/src/cli/dev.js +148 -27
- package/dist/src/cli/doctor.js +122 -36
- package/dist/src/cli/generated-files.js +143 -0
- package/dist/src/cli/guides.js +116 -16
- package/dist/src/cli/hosted.js +850 -0
- package/dist/src/cli/init.js +154 -33
- package/dist/src/cli/login.js +292 -0
- package/dist/src/cli/machine.js +125 -40
- package/dist/src/cli/main.js +59 -10
- package/dist/src/cli/mason.cjs +17 -0
- package/dist/src/cli/mcp.js +17 -14
- 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 +1 -1
- package/dist/src/cli/pin.js +6 -2
- package/dist/src/cli/pool-size.cjs +85 -0
- package/dist/src/cli/ports.js +2 -0
- package/dist/src/cli/preflight.js +130 -39
- package/dist/src/cli/project.js +100 -1
- package/dist/src/cli/readiness.js +42 -6
- package/dist/src/cli/registry.js +89 -36
- package/dist/src/cli/replica-hosted.js +1324 -0
- package/dist/src/cli/replica.js +114 -0
- package/dist/src/cli/replication-mode.js +5 -0
- package/dist/src/cli/request-filter.js +16 -6
- package/dist/src/cli/roles.js +2 -1
- package/dist/src/cli/run-server.js +179 -9
- package/dist/src/cli/secret-input.js +193 -0
- package/dist/src/cli/security.js +57 -10
- package/dist/src/cli/serve.js +65 -9
- package/dist/src/cli/share-run.js +51 -8
- package/dist/src/cli/share.js +23 -18
- package/dist/src/cli/source-edit.js +10 -4
- package/dist/src/cli/source-role.js +343 -0
- package/dist/src/cli/sources.js +50 -23
- package/dist/src/cli/style.js +16 -1
- package/dist/src/cli/supervise.js +15 -23
- package/dist/src/cli/sync.js +17 -8
- package/dist/src/cli/token-hosted.js +343 -0
- package/dist/src/cli/tunnel.js +1 -1
- package/dist/src/cli/ui-hosted.js +163 -0
- package/dist/src/cli/ui-ssh.js +460 -0
- package/dist/src/cli/ui.js +162 -17
- package/dist/src/cli/usage.js +19 -15
- package/dist/src/cli/user-hosted.js +364 -0
- package/dist/src/cli/user.js +275 -105
- package/dist/src/demo/seed.js +26 -0
- package/dist/src/duck/analyze.js +36 -20
- 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 +39 -8
- package/dist/src/duck/executor-worker.js +2 -2
- package/dist/src/duck/executor.js +6 -5
- package/dist/src/duck/explain.js +4 -0
- package/dist/src/duck/functions.js +448 -49
- package/dist/src/duck/gaps.js +34 -16
- package/dist/src/duck/generated-plan.js +1070 -0
- package/dist/src/duck/generated.js +906 -0
- package/dist/src/duck/grants.js +1 -1
- package/dist/src/duck/held-types.js +64 -0
- package/dist/src/duck/history.js +4 -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 +9 -7
- 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 +275 -30
- package/dist/src/duck/publication.js +14 -0
- package/dist/src/duck/requests.js +9 -5
- package/dist/src/duck/resolved.js +117 -2
- package/dist/src/duck/rewrite.js +4567 -583
- package/dist/src/duck/row-filter.js +12 -6
- package/dist/src/duck/statement.js +19 -0
- package/dist/src/duck/store.js +294 -68
- package/dist/src/duck/stream.js +9 -5
- package/dist/src/duck/sync.js +602 -126
- package/dist/src/duck/token.js +4 -1
- package/dist/src/duck/types.js +63 -7
- package/dist/src/duck/usage.js +6 -4
- package/dist/src/duck/users.js +110 -9
- package/dist/src/duck/verified.js +159 -2
- package/dist/src/duck/views.js +4 -3
- package/dist/src/lib/cidr.js +177 -0
- package/dist/src/lib/database-url.js +55 -10
- package/dist/src/lib/instant.js +50 -0
- package/dist/src/lib/log-buffer.js +13 -4
- package/dist/src/lib/log-read.js +196 -0
- package/dist/src/lib/pg-numeric.js +387 -49
- package/dist/src/lib/secret-mask.js +142 -0
- package/dist/src/lib/table-sync.js +84 -19
- package/dist/src/web/app.js +9 -4
- package/dist/src/web/pages/status.js +178 -13
- package/dist/src/web/pages/syncs.js +1 -1
- package/dist/src/web/pages/tables.js +8 -3
- package/dist/src/web/pages/users.js +4 -3
- package/dist/src/web/shared.js +19 -3
- package/package.json +8 -7
package/dist/src/duck/store.js
CHANGED
|
@@ -19,16 +19,16 @@ import * as os from "node:os";
|
|
|
19
19
|
import { DuckDBInstance, version } from "@duckdb/node-api";
|
|
20
20
|
import { Executor } from "./executor.js";
|
|
21
21
|
import { beginStatement, cancelStatement, slotName, statementCancelled } from "./cancel.js";
|
|
22
|
-
import { StatementError } from "
|
|
23
|
-
import { duckParameters, missingRelationHint, planDuck, quoteDuckKeywords } from "./plan.js";
|
|
22
|
+
import { StatementError } from "./statement.js";
|
|
23
|
+
import { duckParameters, missingRelationHint, ordersText, planDuck, quoteDuckKeywords } from "./plan.js";
|
|
24
24
|
import { VERIFIED } from "./verified.js";
|
|
25
25
|
import { registerFunctions, SESSION_VARIABLE, SETTINGS_VARIABLE } from "./functions.js";
|
|
26
|
-
import { duckCollation } from "./
|
|
27
|
-
import { decodeLibcOrder, encodeLibcOrder } from "./libc-order.js";
|
|
26
|
+
import { caseMappingOf, CollationResolver, duckCollation, isLibcCollation } from "./collation.js";
|
|
27
|
+
import { decodeLibcOrder, encodeLibcOrder, isCurrentLibcOrder } from "./libc-order.js";
|
|
28
28
|
import { dataRow } from "../api/postgres/protocol.js";
|
|
29
29
|
import { cellText, exactColumn, pgTypedColumn, wireColumn } from "./types.js";
|
|
30
30
|
import { gapIn, gapMark } from "./gaps.js";
|
|
31
|
-
import { cellsOf, evaluateText, NumericError } from "../lib/pg-numeric.js";
|
|
31
|
+
import { cellsOf, evaluateText, fieldOverflow, NumericError } from "../lib/pg-numeric.js";
|
|
32
32
|
import { explainJson, explainText, planNodes, profiled, qualifiedName, scannedTables } from "./explain.js";
|
|
33
33
|
import * as fs from "node:fs";
|
|
34
34
|
import * as path from "node:path";
|
|
@@ -65,20 +65,94 @@ const INSTANCE_SETTINGS = { late_materialization_max_rows: "10000", force_compre
|
|
|
65
65
|
/**
|
|
66
66
|
* DUCKDB'S MEMORY, BOUNDED FOR THIS PROCESS: DuckDB's default is 80% of the machine's RAM, in a process
|
|
67
67
|
* that also holds V8's heap and two PGlites, which ends with the system killing every session and the sync together.
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
68
|
+
* Unless `MASON_DUCKDB_MEMORY` says (`8GB`, `512MiB`), half the memory, or what leaves `OUTSIDE_RESERVE_BYTES` beside
|
|
69
|
+
* it when that is less (`defaultMemoryLimit`); past it a sort, a join or an aggregate spills to `<file>.tmp` (DuckDB's
|
|
70
|
+
* own default, named here so it is the copy's folder), and what cannot spill fails ITS statement with 53200 and the way
|
|
71
|
+
* out (pgErrorOf), the run going on.
|
|
72
|
+
*
|
|
73
|
+
* THE ROOM OUTSIDE THE LIMIT (issue 301, docs/DESIGN.md "Memory: the room outside the engine's limit"): the limit counts
|
|
74
|
+
* DuckDB's buffers, never Node's heap, the catalog's PGlite, the pg-eval PGlite nor what DuckDB allocates outside its
|
|
75
|
+
* buffer manager, and with half of a 1, 1.5, 2 or 3 GiB container that room was too small: the kernel killed the run in
|
|
76
|
+
* each. Measured with the limit at 256MiB, the process reached 2.3 GiB beside it (the pg-eval PGlite alone ~0.5 GiB of
|
|
77
|
+
* WebAssembly memory once a statement handed it 1.5 M values, the catalog's ~0.26 GiB). `OUTSIDE_RESERVE_BYTES` is that
|
|
78
|
+
* peak with a margin; `MEMORY_FLOOR_BYTES` the smallest container the whole path (a first sync, a generated table's
|
|
79
|
+
* build, writes on the stream, statements through the listener) ran in under this default. Under the floor, with
|
|
80
|
+
* `MASON_DUCKDB_MEMORY` unset, a start goes on and WARNS (`memoryWarning`: the run's start, mason status, mason doctor),
|
|
81
|
+
* its statements given MIN_SHARE_BYTES, the least room for them and the most for the rest (Tom's decision on PR 382: a
|
|
82
|
+
* warning, never a refusal); set, the variable is the operator's word at any size.
|
|
83
|
+
*/
|
|
84
|
+
export const OUTSIDE_RESERVE_BYTES = 2560 * 2 ** 20;
|
|
85
|
+
export const MEMORY_FLOOR_BYTES = 3072 * 2 ** 20;
|
|
86
|
+
/** the statements' share where the reserve leaves less (under the floor, where a start warns): what let the run last
|
|
87
|
+
* longest, measured (docs/DESIGN.md): at 2 GiB the whole path ran with it, as with 256MiB, and at 1.5 GiB it answered
|
|
88
|
+
* every statement before the kill where 256MiB was killed in the writes; the same statements answered at both, one
|
|
89
|
+
* wide aggregate failing alone (53200), and the old quarter, 512MiB, was killed at 2 GiB */
|
|
90
|
+
export const MIN_SHARE_BYTES = 128 * 2 ** 20;
|
|
91
|
+
/**
|
|
92
|
+
* The default memory limit for `totalBytes` of memory, in whole MiB (mason doctor says it for any machine): half of it,
|
|
93
|
+
* the page cache keeping the other half, or what leaves OUTSIDE_RESERVE_BYTES beside it when that is less (from 5 GiB
|
|
94
|
+
* up it is half), never under MIN_SHARE_BYTES (from 2.625 GiB down: under the floor, where a start warns), nor past half.
|
|
95
|
+
*/
|
|
96
|
+
export function defaultMemoryLimit(totalBytes) {
|
|
97
|
+
const mib = 2 ** 20;
|
|
98
|
+
const half = Math.floor(totalBytes / 2 / mib), room = Math.floor((totalBytes - OUTSIDE_RESERVE_BYTES) / mib);
|
|
99
|
+
return `${Math.min(half, Math.max(MIN_SHARE_BYTES / mib, room))}MiB`;
|
|
100
|
+
}
|
|
101
|
+
/** A size as a reader recognises it: GiB to one decimal from 1 GiB up, else MiB. */
|
|
102
|
+
export function memorySize(bytes) {
|
|
103
|
+
return bytes >= 2 ** 30 ? `${(bytes / 2 ** 30).toFixed(1)} GiB` : `${Math.round(bytes / 2 ** 20)} MiB`;
|
|
104
|
+
}
|
|
105
|
+
/** `MASON_DUCKDB_MEMORY` as it is set: null unset (or set empty, as the run reads it) */
|
|
106
|
+
function memorySet(env) {
|
|
107
|
+
const v = env.MASON_DUCKDB_MEMORY;
|
|
108
|
+
return v === undefined || v === "" ? null : v;
|
|
109
|
+
}
|
|
110
|
+
/** `MASON_DUCKDB_MEMORY` that is not an amount: the one text, for the store's open, mason serve, mason start and doctor */
|
|
111
|
+
export function memorySettingWrong(text) {
|
|
112
|
+
return { message: `MASON_DUCKDB_MEMORY=${JSON.stringify(text)} is not an amount of memory`,
|
|
113
|
+
why: "it is the memory the synced data's statements may use before they spill to disk",
|
|
114
|
+
fix: `write it as 8GB or 512MiB, or unset it for the default (${DEFAULT_MEMORY_LIMIT} here: ${DEFAULT_MEMORY_RULE})` };
|
|
115
|
+
}
|
|
116
|
+
/** THE DEFAULT'S RULE IN WORDS, the one text every message, help and guide that states the default says (a fact derived
|
|
117
|
+
* in several places: PR 380's first-sync refusal said "half the machine's RAM" after the default had changed);
|
|
118
|
+
* `defaultMemoryLimit` is the rule, and `tests/conventions.test.ts` holds src/ to saying it only through this */
|
|
119
|
+
export const DEFAULT_MEMORY_RULE = `half the container's memory limit, else of the machine's RAM, or what leaves ${memorySize(OUTSIDE_RESERVE_BYTES)} ` +
|
|
120
|
+
`beside it when that is less, never under ${memorySize(MIN_SHARE_BYTES)}`;
|
|
121
|
+
/**
|
|
122
|
+
* WHAT A START REFUSES OF THE MEMORY (mason serve's one report; mason start and mason doctor take the first):
|
|
123
|
+
* `MASON_DUCKDB_MEMORY` that is not an amount. A size under the floor is never refused: it is `memoryWarning`'s.
|
|
71
124
|
*/
|
|
72
|
-
|
|
73
|
-
|
|
125
|
+
export function memoryProblems(env) {
|
|
126
|
+
const set = memorySet(env);
|
|
127
|
+
return set !== null && !MEMORY_LIMIT_RE.test(set) ? [memorySettingWrong(set)] : [];
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* THE ONE WARNING OF THE MEMORY: `memory` under MEMORY_FLOOR_BYTES while `MASON_DUCKDB_MEMORY` is unset (or not an
|
|
131
|
+
* amount, which is refused beside it). The start goes on; the run says it at its start and in what mason status reads
|
|
132
|
+
* (its warnings), and mason doctor's memory line says it, each from this text alone. `memory` is `machineMemory()`.
|
|
133
|
+
*/
|
|
134
|
+
export function memoryWarning(memory, env) {
|
|
135
|
+
const set = memorySet(env);
|
|
136
|
+
if ((set !== null && MEMORY_LIMIT_RE.test(set)) || memory.bytes >= MEMORY_FLOOR_BYTES)
|
|
137
|
+
return null;
|
|
138
|
+
const size = memorySize(memory.bytes);
|
|
139
|
+
return { message: `${size} of memory is under the ${memorySize(MEMORY_FLOOR_BYTES)} Mason needs`,
|
|
140
|
+
why: `${size} is ${memory.from === "container" ? "the container's memory limit" : "the machine's RAM"}; ` +
|
|
141
|
+
`beside what the synced data's statements may use, Mason itself needs up to ${memorySize(OUTSIDE_RESERVE_BYTES)} (measured, with a margin), ` +
|
|
142
|
+
"and with less the system may stop the whole run, every session with it, when a first sync, a burst of writes or a statement needs the memory",
|
|
143
|
+
fix: `give it ${memorySize(MEMORY_FLOOR_BYTES)} or more (the container's memory limit), or set MASON_DUCKDB_MEMORY=<n> ` +
|
|
144
|
+
"to choose the statements' share yourself, the rest left to Mason (mason doctor says what it leaves)" };
|
|
145
|
+
}
|
|
146
|
+
/** a problem or the warning as one line: mason serve's settings report, the run's warning, mason doctor's detail */
|
|
147
|
+
export function memoryProblemText(p) { return `${p.message}: ${p.why} — ${p.fix}`; }
|
|
74
148
|
/**
|
|
75
149
|
* THE MEMORY THIS PROCESS MAY USE: the machine's RAM, or the container's limit where one is set — a cgroup v2
|
|
76
150
|
* `memory.max` or a cgroup v1 `memory.limit_in_bytes` below it. `os.totalmem()` is the HOST's RAM inside a container,
|
|
77
151
|
* so half of it was more than a limited container had, and the kernel killed the run instead of DuckDB spilling
|
|
78
152
|
* (the review of the cut). `read` is the test's.
|
|
79
153
|
*/
|
|
80
|
-
export function
|
|
81
|
-
let limit = total;
|
|
154
|
+
export function machineMemory(read = (f) => fs.readFileSync(f, "utf8"), total = os.totalmem()) {
|
|
155
|
+
let limit = { bytes: total, from: "machine" };
|
|
82
156
|
for (const file of ["/sys/fs/cgroup/memory.max", "/sys/fs/cgroup/memory/memory.limit_in_bytes"]) {
|
|
83
157
|
let text;
|
|
84
158
|
try {
|
|
@@ -89,12 +163,35 @@ export function machineMemoryBytes(read = (f) => fs.readFileSync(f, "utf8"), tot
|
|
|
89
163
|
} // not a cgroup of that version, or not a container: the machine's RAM
|
|
90
164
|
// v2 says `max` for no limit; v1 says a number near 2^63; either way a number below the RAM is the limit
|
|
91
165
|
const n = Number(text);
|
|
92
|
-
if (text !== "max" && Number.isFinite(n) && n > 0 && n < limit)
|
|
93
|
-
limit = n;
|
|
166
|
+
if (text !== "max" && Number.isFinite(n) && n > 0 && n < limit.bytes)
|
|
167
|
+
limit = { bytes: n, from: "container" };
|
|
94
168
|
}
|
|
95
169
|
return limit;
|
|
96
170
|
}
|
|
171
|
+
/** `machineMemory`'s bytes: the one size the default limit and the floor are read against */
|
|
172
|
+
export function machineMemoryBytes(read, total) { return machineMemory(read, total).bytes; }
|
|
97
173
|
export const DEFAULT_MEMORY_LIMIT = defaultMemoryLimit(machineMemoryBytes());
|
|
174
|
+
/**
|
|
175
|
+
* WHAT STATEMENTS MAY SPILL (issue 105: a 100-row preview's sort took every free byte of a volume, and the sync's room
|
|
176
|
+
* with it). DuckDB's default ceiling is "90% of available disk space", and what it answered when the disk ran out first
|
|
177
|
+
* was `XX000` with a path. Here the ceiling is HALF of the room statements have, what is free where the copy is and what
|
|
178
|
+
* they have spilled there (the disk's free bytes exclude the spill, so a ceiling of the free bytes alone would grow with
|
|
179
|
+
* each read as the spill grows, and end near the whole disk). The sync's writes keep the other half whatever statements
|
|
180
|
+
* do, and it follows the disk (`DuckStore.keepSpillCeiling`, every `SPILL_REFRESH_MS`): a copy that grows narrows it.
|
|
181
|
+
* `MASON_SPILL_LIMIT` sets the amount instead, never past 90% of that room. Spilled files are shared by the statements
|
|
182
|
+
* running, so two at once share the ceiling. Past it the statement that needed the room fails alone, 53400 as past
|
|
183
|
+
* Postgres's temp_file_limit, with the way out (pgErrorOf), and its files are gone with it; a disk that is really full
|
|
184
|
+
* is 53100. A sizing decision reads `store.spillLimitBytes`, as it reads `store.memoryLimitBytes`.
|
|
185
|
+
*/
|
|
186
|
+
const SPILL_FREE_SHARE = 0.5, SPILL_FREE_MOST = 0.9;
|
|
187
|
+
const SPILL_REFRESH_MS = 15_000;
|
|
188
|
+
export function spillCeiling(freeBytes, spilledBytes, setBytes) {
|
|
189
|
+
const room = freeBytes + spilledBytes;
|
|
190
|
+
return Math.max(2 ** 20, Math.floor(setBytes === null ? room * SPILL_FREE_SHARE : Math.min(setBytes, room * SPILL_FREE_MOST)));
|
|
191
|
+
}
|
|
192
|
+
/** the bytes free where `dir` is: the one reading of it (the sync's disk check, the spill's ceiling) */
|
|
193
|
+
export function freeBytesAt(dir) { const st = fs.statfsSync(dir); return st.bavail * st.bsize; }
|
|
194
|
+
const spillSetting = (bytes) => `${Math.floor(bytes / 2 ** 20)}MiB`;
|
|
98
195
|
export const MEMORY_LIMIT_RE = /^\s*(\d+(?:\.\d+)?)\s*(KB|MB|GB|TB|KiB|MiB|GiB|TiB)\s*$/i;
|
|
99
196
|
/** DuckDB's memory limit in bytes, as DuckDB reads the text: KB / MB / GB / TB are powers of 1000, KiB / MiB / GiB / TiB of 1024 */
|
|
100
197
|
export function memoryLimitBytes(text) {
|
|
@@ -139,17 +236,31 @@ export class DuckStore {
|
|
|
139
236
|
file;
|
|
140
237
|
/** the memory limit every connection shares, in bytes */
|
|
141
238
|
memoryLimitBytes;
|
|
239
|
+
/** what statements may spill now, in bytes (`spillCeiling`): it follows the disk */
|
|
240
|
+
spillLimitBytes;
|
|
241
|
+
spillSet;
|
|
242
|
+
diskFree;
|
|
243
|
+
spillTimer;
|
|
244
|
+
spillLane = null;
|
|
245
|
+
spillKept = null;
|
|
246
|
+
log;
|
|
142
247
|
executor;
|
|
143
248
|
closed = null;
|
|
144
249
|
closeGraceMs;
|
|
145
250
|
/** where the copy's extensions come from: Mason's package for this platform, or DuckDB's install (src/duck/extensions.ts) */
|
|
146
251
|
extensions;
|
|
147
|
-
constructor(file, executor, memoryLimit, extensions, closeGraceMs) {
|
|
252
|
+
constructor(file, executor, memoryLimit, extensions, spill, log, closeGraceMs) {
|
|
148
253
|
this.file = file;
|
|
149
254
|
this.executor = executor;
|
|
150
255
|
this.memoryLimitBytes = memoryLimit;
|
|
151
256
|
this.extensions = extensions;
|
|
152
257
|
this.closeGraceMs = closeGraceMs;
|
|
258
|
+
this.spillLimitBytes = spill.limit;
|
|
259
|
+
this.spillSet = spill.set;
|
|
260
|
+
this.diskFree = spill.diskFree;
|
|
261
|
+
this.log = log;
|
|
262
|
+
this.spillTimer = setInterval(() => { void this.keepSpillCeiling(); }, SPILL_REFRESH_MS);
|
|
263
|
+
this.spillTimer.unref();
|
|
153
264
|
this.writer = executor.writer();
|
|
154
265
|
this.instance = { connect: () => executor.connect() };
|
|
155
266
|
}
|
|
@@ -160,8 +271,13 @@ export class DuckStore {
|
|
|
160
271
|
const extensions = options.extensions ?? findExtensions();
|
|
161
272
|
const memoryLimit = options.memoryLimit ?? DEFAULT_MEMORY_LIMIT;
|
|
162
273
|
if (!MEMORY_LIMIT_RE.test(memoryLimit))
|
|
163
|
-
throw new Error(
|
|
274
|
+
throw new Error(memoryProblemText(memorySettingWrong(memoryLimit)));
|
|
275
|
+
if (options.spillLimit !== undefined && !MEMORY_LIMIT_RE.test(options.spillLimit))
|
|
276
|
+
throw new Error(`MASON_SPILL_LIMIT=${JSON.stringify(options.spillLimit)} is not an amount of disk: write it as 40GB or 512MiB (unset, half of what is free on the disk)`);
|
|
277
|
+
const spillSet = options.spillLimit === undefined ? null : memoryLimitBytes(options.spillLimit);
|
|
278
|
+
const diskFree = options.diskFree ?? freeBytesAt;
|
|
164
279
|
const where = path.resolve(file);
|
|
280
|
+
const spillLimit = spillCeiling(diskFree(path.dirname(where)), 0, spillSet);
|
|
165
281
|
const before = opened.get(where);
|
|
166
282
|
if (before !== undefined && before.closed === null)
|
|
167
283
|
throw new Error(`Mason could not open the synced data ${file}: this process has it open already (Mason bug: report it)`);
|
|
@@ -175,7 +291,7 @@ export class DuckStore {
|
|
|
175
291
|
if (before !== undefined)
|
|
176
292
|
await before.closed;
|
|
177
293
|
// every connection's, the sync's and each session's
|
|
178
|
-
executor = await Executor.start({ file, settings: { ...INSTANCE_SETTINGS, ...extensionSettings(extensions), memory_limit: memoryLimit, temp_directory: `${file}.tmp
|
|
294
|
+
executor = await Executor.start({ file, settings: { ...INSTANCE_SETTINGS, ...extensionSettings(extensions), memory_limit: memoryLimit, temp_directory: `${file}.tmp`, max_temp_directory_size: spillSetting(spillLimit) }, log,
|
|
179
295
|
...(options.silentMs !== undefined ? { silentMs: options.silentMs } : {}), ...(options.workerFile !== undefined ? { workerFile: options.workerFile } : {}) });
|
|
180
296
|
}
|
|
181
297
|
catch (e) {
|
|
@@ -183,7 +299,7 @@ export class DuckStore {
|
|
|
183
299
|
opened.delete(where);
|
|
184
300
|
throw e;
|
|
185
301
|
}
|
|
186
|
-
const store = new DuckStore(file, executor, memoryLimitBytes(memoryLimit), extensions, options.closeGraceMs);
|
|
302
|
+
const store = new DuckStore(file, executor, memoryLimitBytes(memoryLimit), extensions, { limit: spillLimit, set: spillSet, diskFree }, log, options.closeGraceMs);
|
|
187
303
|
// closed: the file may be opened again — unless its thread was given up on (`abandoned`), which keeps it here
|
|
188
304
|
store.closing = (p) => { mine.closed = p; void p.then(() => { if (executor.abandoned !== null)
|
|
189
305
|
mine.abandoned = executor.abandoned;
|
|
@@ -192,6 +308,45 @@ export class DuckStore {
|
|
|
192
308
|
return store;
|
|
193
309
|
}
|
|
194
310
|
closing = () => undefined;
|
|
311
|
+
/**
|
|
312
|
+
* THE SPILL'S CEILING MOVED WITH THE DISK (`spillCeiling`): the bytes free and spilled now, set on the instance on a
|
|
313
|
+
* connection of its own, never the writer's. One at a time; a move under 1 % is none. DuckDB refuses a ceiling under
|
|
314
|
+
* what is spilled at that moment (more may have spilled since the read): it stays where it was until the next turn.
|
|
315
|
+
* Any other failure is said; the ceiling stands, which is the last one set.
|
|
316
|
+
*/
|
|
317
|
+
keepSpillCeiling() {
|
|
318
|
+
if (this.closed !== null)
|
|
319
|
+
return Promise.resolve();
|
|
320
|
+
this.spillKept ??= this.spillTurn().catch((e) => {
|
|
321
|
+
if (this.closed === null)
|
|
322
|
+
this.log(`the spill's ceiling was not moved: ${e instanceof Error ? e.message : String(e)}`);
|
|
323
|
+
}).finally(() => { this.spillKept = null; });
|
|
324
|
+
return this.spillKept;
|
|
325
|
+
}
|
|
326
|
+
async spillTurn() {
|
|
327
|
+
let c;
|
|
328
|
+
// a connection that failed is asked for again next turn, never kept as the answer
|
|
329
|
+
try {
|
|
330
|
+
c = await (this.spillLane ??= this.executor.connect());
|
|
331
|
+
}
|
|
332
|
+
catch (e) {
|
|
333
|
+
this.spillLane = null;
|
|
334
|
+
throw e;
|
|
335
|
+
}
|
|
336
|
+
const spilled = Number((await c.runAndReadAll("SELECT coalesce(sum(size), 0) FROM duckdb_temporary_files()")).getRowsJS()[0][0]);
|
|
337
|
+
const next = spillCeiling(this.diskFree(path.dirname(path.resolve(this.file))), spilled, this.spillSet);
|
|
338
|
+
if (Math.abs(next - this.spillLimitBytes) < this.spillLimitBytes * 0.01)
|
|
339
|
+
return;
|
|
340
|
+
try {
|
|
341
|
+
await c.run(`SET GLOBAL max_temp_directory_size = '${spillSetting(next)}'`);
|
|
342
|
+
}
|
|
343
|
+
catch (e) {
|
|
344
|
+
if (e instanceof Error && e.message.includes("failed to adjust"))
|
|
345
|
+
return;
|
|
346
|
+
throw e;
|
|
347
|
+
}
|
|
348
|
+
this.spillLimitBytes = next;
|
|
349
|
+
}
|
|
195
350
|
/** called once if the engine's thread dies while the run uses it (src/duck/executor.ts): the run exits with the reason */
|
|
196
351
|
onCrash(f) { this.executor.onCrash(f); }
|
|
197
352
|
/** how many vectors each Mason function was called with so far, on the engine's thread (a test's, a measurement's) */
|
|
@@ -202,13 +357,14 @@ export class DuckStore {
|
|
|
202
357
|
runner() { return this.executor.runner(); }
|
|
203
358
|
/**
|
|
204
359
|
* The source's order of text under a libc collation (src/duck/libc-order.ts), kept in the copy: the one recorded
|
|
205
|
-
* for this collation and version, else `read` from the source (a few seconds) and recorded.
|
|
360
|
+
* for this collation and version, in this release's form, else `read` from the source (a few seconds) and recorded.
|
|
361
|
+
* `read: true` when it was.
|
|
206
362
|
*/
|
|
207
363
|
async libcOrder(collate, version, read) {
|
|
208
364
|
await this.writer.run(`CREATE SCHEMA IF NOT EXISTS ${MASON_SCHEMA}`);
|
|
209
365
|
await this.writer.run(`CREATE TABLE IF NOT EXISTS ${MASON_SCHEMA}.libc_order (collation_name VARCHAR, collation_version VARCHAR, data VARCHAR)`);
|
|
210
366
|
const kept = (await this.writer.runAndReadAll(`SELECT data FROM ${MASON_SCHEMA}.libc_order WHERE collation_name = $1 AND collation_version IS NOT DISTINCT FROM $2`, [collate, version])).getRowsJS();
|
|
211
|
-
if (kept.length > 0)
|
|
367
|
+
if (kept.length > 0 && isCurrentLibcOrder(String(kept[0][0])))
|
|
212
368
|
return { order: decodeLibcOrder(String(kept[0][0])), read: false };
|
|
213
369
|
const order = await read();
|
|
214
370
|
// one order per collation: a version that moved replaces the last one's
|
|
@@ -220,22 +376,50 @@ export class DuckStore {
|
|
|
220
376
|
* its text sorted as the SOURCE sorts it (`collation`, pg_database's; absent, by bytes; a libc one by `libc`, its
|
|
221
377
|
* order as `libcOrder` read it), refusing what is not `verified` (null: nothing is refused for it, the oracle's
|
|
222
378
|
* recording run alone). */
|
|
223
|
-
async listener(collation, verified = VERIFIED, libc, copies = () => [], kept) {
|
|
379
|
+
async listener(collation, verified = VERIFIED, libc, copies = () => [], kept, more = {}) {
|
|
224
380
|
const known = new Set((await this.writer.runAndReadAll("SELECT collname FROM pragma_collations()")).getRowsJson().map((r) => String(r[0])));
|
|
225
381
|
quoteDuckKeywords((await this.writer.runAndReadAll("SELECT keyword_name FROM duckdb_keywords() WHERE keyword_category <> 'unreserved'")).getRowsJson().map((r) => String(r[0])));
|
|
226
|
-
const
|
|
382
|
+
const named = collation === undefined ? { kind: "bytes" } : duckCollation(collation.provider, collation.collate, collation.locale, known);
|
|
383
|
+
// (a libc locale whose strcoll is byte order, musl's or macOS's: src/duck/libc-order.ts)
|
|
384
|
+
const c = named.kind === "libc" && libc?.bytes === true ? { kind: "bytes" } : named;
|
|
227
385
|
if (c.kind === "libc") {
|
|
228
386
|
if (libc === undefined)
|
|
229
387
|
throw new Error(`the database orders text by the libc collation ${collation.collate}, and its order was not read (Mason bug: report it)`);
|
|
230
|
-
await this.executor.registerLibcOrder(libc);
|
|
388
|
+
await this.executor.registerLibcOrder(c.collate, libc, true);
|
|
231
389
|
}
|
|
390
|
+
// each other libc collation read at the start (a synced column's): its key's order, by its collcollate
|
|
391
|
+
for (const [collate, order] of more.libcOrders ?? [])
|
|
392
|
+
if (!order.bytes)
|
|
393
|
+
await this.executor.registerLibcOrder(collate, order, false);
|
|
394
|
+
// a libc collation named first by a statement: read from the source on its first use, kept in the copy, then the
|
|
395
|
+
// engine told its order before the plan that needs it is made (src/duck/collation.ts)
|
|
396
|
+
const readLibc = more.readLibc;
|
|
397
|
+
// the database's case mapping is its CTYPE's (datctype; ICU's or builtin's locale), never its order's
|
|
398
|
+
const ctype = collation === undefined ? null : collation.provider === "c" ? collation.ctype ?? collation.collate : collation.locale ?? collation.collate;
|
|
399
|
+
const resolver = new CollationResolver({ database: c, databaseCollate: collation !== undefined && isLibcCollation(collation.provider, collation.collate) ? collation.collate : null,
|
|
400
|
+
...(collation === undefined ? {} : { databaseCase: { mapping: caseMappingOf(collation.provider, ctype, collation.locale), ctype: ctype ?? "" } }),
|
|
401
|
+
source: more.collations ?? null, known, libc: more.libcOrders ?? new Map(),
|
|
402
|
+
readLibc: readLibc === undefined ? null : async (sc) => {
|
|
403
|
+
const r = await this.libcOrder(sc.collate ?? "C", sc.version ?? null, () => readLibc(sc));
|
|
404
|
+
if (!r.order.bytes)
|
|
405
|
+
await this.executor.registerLibcOrder(sc.collate ?? "C", r.order, false);
|
|
406
|
+
return r.order;
|
|
407
|
+
} });
|
|
408
|
+
// the database's own order, held where a plan reads each order by its collcollate (src/duck/libc-bound.ts)
|
|
409
|
+
if (c.kind === "libc" && libc !== undefined)
|
|
410
|
+
resolver.hold(c.collate, libc);
|
|
232
411
|
// a jsonb's stored key is this collation's (src/duck/kept.ts `orderBy`)
|
|
233
412
|
await kept?.orderBy(c, c.kind === "libc" ? libc : undefined);
|
|
234
413
|
return {
|
|
235
414
|
collation: c,
|
|
415
|
+
collations: resolver,
|
|
236
416
|
open: () => this.runner(),
|
|
237
|
-
plan: (catalog, text, stmt, base, paramOids,
|
|
417
|
+
plan: (catalog, text, stmt, base, paramOids, under, userTypes, wireBound, grants, relations, outputs) => planDuck({ catalog, text, stmt, base, paramOids, searchPath: under.searchPath, timeZone: under.timeZone, dateStyle: under.dateStyle, collations: resolver, verified, userTypes, copies, kept: kept ?? null, wireBound: wireBound ?? false, grants, ...(relations === undefined ? {} : { relations }), outputs: outputs ?? false }),
|
|
238
418
|
parameters: duckParameters,
|
|
419
|
+
// a SELECT of no table that orders text under a collation that is not byte order: the synced data's (issue 122);
|
|
420
|
+
// one that maps case (upper, lower, initcap: issue 406) is the synced data's where its run answers it (the
|
|
421
|
+
// listener's `answersCase`), else the catalog's, as before
|
|
422
|
+
ordersText: (catalog, text, paramOids, under, bindable) => ordersText({ catalog, text, paramOids, under, collations: resolver, bindable }),
|
|
239
423
|
// the sync's state, which EXPLAIN says how each copy is kept current from (src/duck/sync.ts KeptCurrent)
|
|
240
424
|
copies,
|
|
241
425
|
// a stored JSON path ready or gone (src/duck/kept.ts): the plans kept are made again
|
|
@@ -246,6 +430,7 @@ export class DuckStore {
|
|
|
246
430
|
* (a second open of the file waits for it), or once the executor's grace has passed whatever the thread does
|
|
247
431
|
* (src/duck/executor.ts CLOSE_GRACE_MS); never rejects — a failure is said in the log. */
|
|
248
432
|
close() {
|
|
433
|
+
clearInterval(this.spillTimer);
|
|
249
434
|
if (this.closed === null) {
|
|
250
435
|
this.closed = this.executor.close(this.closeGraceMs);
|
|
251
436
|
this.closing(this.closed);
|
|
@@ -281,11 +466,22 @@ export function pgErrorOf(err, sql) {
|
|
|
281
466
|
const full = err instanceof Error ? err.message : String(err);
|
|
282
467
|
const kind = /^([A-Za-z ]+) Error: /.exec(full)?.[1] ?? "";
|
|
283
468
|
const message = full.replace(/^[A-Za-z ]+ Error: /, "").replace(/\n\n?LINE \d+:[\s\S]*$/, "").trim();
|
|
469
|
+
// NO ROOM TO SPILL (issue 105): the ceiling, or the disk itself before it. Postgres's class for a full disk, in
|
|
470
|
+
// Mason's words: the engine's text names a path and a setting no client of Mason has, so it stays out of the answer
|
|
471
|
+
// Read by the engine's own kind and sentence: a statement's literal may hold the same words in any other error
|
|
472
|
+
const ceiling = kind === "Out of Memory" && /This limit was set by the 'max_temp_directory_size' setting/.test(full);
|
|
473
|
+
if (ceiling || (kind === "IO" && /No space left on device/.test(full))) {
|
|
474
|
+
return new StatementError({ code: ceiling ? "53400" : "53100", message: ceiling ? "could not write to temporary file: this statement needs more room on disk than statements may take"
|
|
475
|
+
: "could not write to temporary file: No space left on device",
|
|
476
|
+
hint: "ask for less at once (a filter, a LIMIT, fewer or narrower columns, a coarser GROUP BY), free disk where the synced data is, or raise MASON_SPILL_LIMIT (40GB; by default half of what is free) and start mason again; nothing else stopped" }, sql, "spill-limit");
|
|
477
|
+
}
|
|
284
478
|
const pg = postgresWording(kind, message);
|
|
285
479
|
// a refusal raised inside DuckDB (a Mason function's, the rewrite's error() call) crossed it as its text alone: the
|
|
286
480
|
// entry its text names (gapIn), read here, the one place that text becomes a statement's error
|
|
481
|
+
// (Mason's own refusal keeps the engine's text out of the DETAIL: it may quote the row, tests/conventions.test.ts)
|
|
482
|
+
// (a Mason function's error that carries Postgres's own DETAIL sends it as Postgres does, apart from the message)
|
|
287
483
|
if (pg !== null)
|
|
288
|
-
return new StatementError({ code: pg.code, message: pg.message, detail: `the engine: ${message}` }, sql, pg.gap ?? (pg.code === "0A000" ? gapIn(pg.message) : null));
|
|
484
|
+
return new StatementError({ code: pg.code, message: pg.message, ...(pg.code === "0A000" ? {} : { detail: pg.detail ?? `the engine: ${message}` }) }, sql, pg.gap ?? (pg.code === "0A000" ? gapIn(pg.message) : null));
|
|
289
485
|
const fn = /(?:Scalar|Aggregate|Table) Function with name "?([A-Za-z_][\w]*)"? does not exist/.exec(message)?.[1];
|
|
290
486
|
if (fn !== undefined) {
|
|
291
487
|
// what reaches DuckDB was verified against Postgres (src/duck/verified.ts), so a function DuckDB lacks is a gap in
|
|
@@ -314,9 +510,12 @@ export function pgErrorOf(err, sql) {
|
|
|
314
510
|
: kind === "Out of Range" ? "22003"
|
|
315
511
|
: kind === "Interrupt" || /INTERRUPT/i.test(full) ? "57014"
|
|
316
512
|
: kind === "Out of Memory" ? "53200"
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
513
|
+
// a commit that could not pin or allocate a block is the same limit reached, under the transaction's own kind
|
|
514
|
+
// (measured on DuckDB 2.0.0: "Failed to commit: failed to pin block of size 1.0 MiB (365.7 MiB/366.2 MiB used)")
|
|
515
|
+
: kind === "TransactionContext" && /^Failed to commit: failed to (?:pin block|allocate data) of size .* used\)/.test(message) ? "53200"
|
|
516
|
+
: kind === "Not implemented" ? "0A000"
|
|
517
|
+
: kind === "Invalid Input" ? "22023"
|
|
518
|
+
: "XX000";
|
|
320
519
|
return new StatementError({ code, message: code === "57014" ? "canceling statement due to user request"
|
|
321
520
|
: code === "53200" ? "out of memory: this statement needs more than the memory Mason's engine may use, in a part that cannot spill to disk" : message,
|
|
322
521
|
// DuckDB's own advice is SET, which a client of Mason cannot do: Mason's way out instead
|
|
@@ -345,16 +544,23 @@ function postgresWording(kind, message) {
|
|
|
345
544
|
const field = /^Date out of range: (.*)$/.exec(message);
|
|
346
545
|
if (field)
|
|
347
546
|
return { code: "22008", message: `date field value out of range: ${field[1]}` };
|
|
348
|
-
// a cast to numeric(p, s) of a value with more than p - s integer digits
|
|
349
|
-
if (/Casting value "[^"]*" to type DECIMAL\(\d+,\s*\d+\) failed: value is out of range/.test(message))
|
|
350
|
-
return { code: "22003", message: "numeric field overflow" };
|
|
351
547
|
// an exact sum past DuckDB's 38 digits (a text read as numeric is summed as a DECIMAL(38,18), src/duck/rewrite.ts summed)
|
|
352
548
|
if (/^Overflow in HUGEINT addition\b/.test(message))
|
|
353
549
|
return { code: "0A000", message: `a sum past the engine's 38 digits is not supported by Mason (${gapMark("numeric-exact-sum")})`, gap: "numeric-exact-sum" };
|
|
354
550
|
// a numeric(p, s) read from a text holding NaN or Infinity (src/duck/rewrite.ts pgInput): DuckDB's decimal has none
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
551
|
+
if (/Could not convert string ['"](NaN|-?Infinity)['"] to DECIMAL/.test(message))
|
|
552
|
+
return { code: "0A000", message: `a numeric NaN or Infinity read from a text is not supported by Mason (${gapMark("numeric-special")})`, gap: "numeric-special" };
|
|
553
|
+
// A NUMERIC PAST ITS DIGITS, in every spelling the engine gives (a generated table's run stores one, src/duck/generated.ts
|
|
554
|
+
// failureWords): a cast to numeric(p, s), of a value or a text, with more than p - s digits before the point, said
|
|
555
|
+
// with Postgres's DETAIL (and a number constant that does not fit: `12345::numeric(3)`, DuckDB's "Could not cast value
|
|
556
|
+
// 12345 to DECIMAL(3,0)"); past the 38 digits the engine computes a numeric with, by an operation, a ROUND or a cast
|
|
557
|
+
const typmod = /(?:Casting value "[^"]*" to type|Could not convert string ['"].*['"] to|^Could not cast value \S+ to) DECIMAL\((\d+),\s*(\d+)\)/.exec(message);
|
|
558
|
+
if (typmod !== null && Number(typmod[1]) < 38) {
|
|
559
|
+
return { code: "22003", ...fieldOverflow(Number(typmod[1]), Number(typmod[2])) };
|
|
560
|
+
}
|
|
561
|
+
if (typmod !== null || /Overflow in (?:\w+ )?(?:addition|subtraction|multiplication|negation|ROUND|cast)\b.*DECIMAL|Overflow in HUGEINT \w+|out of range for (?:the desired )?DECIMAL|Could not cast value .* to DECIMAL/i.test(message)) {
|
|
562
|
+
return { code: "22003", message: "numeric field overflow", detail: "The value has more than the 38 digits Mason's engine computes a numeric with." };
|
|
563
|
+
}
|
|
358
564
|
const conv = /Could not convert string '((?:[^']|'')*)' to (INT16|INT32|INT64|DOUBLE|FLOAT|DATE|TIMESTAMP|UUID|BOOL)/.exec(message);
|
|
359
565
|
if (conv) {
|
|
360
566
|
const t = { INT16: "smallint", INT32: "integer", INT64: "bigint", DOUBLE: "double precision", FLOAT: "real", DATE: "date", TIMESTAMP: "timestamp", UUID: "uuid", BOOL: "boolean" }[conv[2]];
|
|
@@ -362,9 +568,10 @@ function postgresWording(kind, message) {
|
|
|
362
568
|
}
|
|
363
569
|
// a Mason function's own error (src/duck/functions.ts): Postgres's words after the prefix
|
|
364
570
|
// Postgres's own error, from its own function (src/duck/pg-eval.ts): its SQLSTATE, said
|
|
365
|
-
|
|
571
|
+
// (and its DETAIL on a line of its own, src/duck/functions.ts `raised`)
|
|
572
|
+
const pg = /^mason\[([0-9A-Z]{5})\]: (.*?)(?:\nDETAIL: (.*))?$/s.exec(message);
|
|
366
573
|
if (pg)
|
|
367
|
-
return { code: pg[1], message: pg[2] };
|
|
574
|
+
return { code: pg[1], message: pg[2], ...(pg[3] === undefined ? {} : { detail: pg[3] }) };
|
|
368
575
|
const own = /^mason: (.*)$/s.exec(message);
|
|
369
576
|
if (own)
|
|
370
577
|
return { code: /^cannot |argument list|key must not be null/.test(own[1]) ? "22023" : /^invalid input syntax/.test(own[1]) ? "22P02" : / is not supported| not supported for type /.test(own[1]) ? "0A000" : "XX000", message: own[1] };
|
|
@@ -373,45 +580,27 @@ function postgresWording(kind, message) {
|
|
|
373
580
|
return { code: "22P02", message };
|
|
374
581
|
return null;
|
|
375
582
|
}
|
|
376
|
-
|
|
377
|
-
const ENDING_SLOW_MS = 5_000;
|
|
378
|
-
/** One wire session's DuckDB connection, with the session's zone and search_path applied before each statement. Runs
|
|
379
|
-
* on the executor worker (src/duck/executor-worker.ts), its statements asked by the listener's RemoteRunner. */
|
|
380
|
-
export class SessionRunner {
|
|
381
|
-
/** the session's slot of the cancel table (src/duck/cancel.ts), which its connection's `__mason_session` names */
|
|
382
|
-
slot;
|
|
383
|
-
cancels;
|
|
384
|
-
/** the number of the statement `run` or `explain` is asked next (the listener's, carried by the request) */
|
|
385
|
-
next = 0;
|
|
386
|
-
/** the answers being drained (`streamed`): `close` and the next statement wait for them first */
|
|
387
|
-
releasing = new Set();
|
|
388
|
-
/** what runs on the connection now: a statement being started, a chunk being read, an EXPLAIN. `close` waits for it
|
|
389
|
-
* too — a connection closed under DuckDB's pending call aborted the process (a stop during a drain) */
|
|
390
|
-
busy = new Set();
|
|
391
|
-
/** the answer left open on the connection, if any: what `close` drains before it closes the connection */
|
|
392
|
-
openAnswer = null;
|
|
583
|
+
export class SessionSetup {
|
|
393
584
|
conn;
|
|
394
|
-
log;
|
|
395
585
|
zone = null;
|
|
396
586
|
path = null;
|
|
397
587
|
settings = null;
|
|
398
|
-
constructor(conn
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
}
|
|
404
|
-
/** a connection for one Postgres session, in the cancel table's `slot` */
|
|
405
|
-
static async open(instance, log, slot, cancels) {
|
|
406
|
-
const conn = await instance.connect();
|
|
588
|
+
constructor(conn) { this.conn = conn; }
|
|
589
|
+
/** `name`: what the Mason functions are passed as the session: a slot of the cancel table (`s<n>`, src/duck/cancel.ts)
|
|
590
|
+
* for a wire session; any other name is never cancelled through the table (a run is interrupted instead). Set up as a
|
|
591
|
+
* session with no role is: UTC, under `public`, no settings, until `settle` says otherwise. */
|
|
592
|
+
static async open(conn, name) {
|
|
407
593
|
// NULLs sort as Postgres sorts them: last ascending, FIRST descending (DuckDB's default is last both ways). An
|
|
408
594
|
// integer division is the rewrite's `//` where Postgres's operator is integer division (src/duck/rewrite.ts), so
|
|
409
595
|
// DuckDB's `/` stays the float division every other `/` is
|
|
410
596
|
await conn.run("SET default_null_order = 'nulls_last_on_asc_first_on_desc'");
|
|
411
597
|
// every Mason function's first argument (src/duck/functions.ts "every call carries its session")
|
|
412
|
-
await conn.run(`SET VARIABLE ${SESSION_VARIABLE} = ${quoteLiteral(
|
|
413
|
-
|
|
598
|
+
await conn.run(`SET VARIABLE ${SESSION_VARIABLE} = ${quoteLiteral(name)}`);
|
|
599
|
+
const setup = new SessionSetup(conn);
|
|
600
|
+
await setup.settle(NO_ROLE_SESSION);
|
|
601
|
+
return setup;
|
|
414
602
|
}
|
|
603
|
+
/** the session's zone, search_path and settings, each set when it changed: before each statement */
|
|
415
604
|
async settle(session) {
|
|
416
605
|
if (session.timeZone !== this.zone) {
|
|
417
606
|
await this.conn.run(`SET TimeZone = ${quoteLiteral(session.timeZone)}`);
|
|
@@ -434,6 +623,43 @@ export class SessionRunner {
|
|
|
434
623
|
this.settings = settings;
|
|
435
624
|
}
|
|
436
625
|
}
|
|
626
|
+
}
|
|
627
|
+
/** a session with no role, as a generated table is planned and run (src/duck/generated-plan.ts): UTC, under `public`, no
|
|
628
|
+
* settings (what reads one is refused there: `current_setting` is "it reads a session") */
|
|
629
|
+
export const NO_ROLE_SESSION = { timeZone: "UTC", searchPath: "public", dateStyle: "ISO, MDY", settings: {} };
|
|
630
|
+
/** when the wait for a statement to end is said in the log */
|
|
631
|
+
const ENDING_SLOW_MS = 5_000;
|
|
632
|
+
/** One wire session's DuckDB connection, with the session's zone and search_path applied before each statement. Runs
|
|
633
|
+
* on the executor worker (src/duck/executor-worker.ts), its statements asked by the listener's RemoteRunner. */
|
|
634
|
+
export class SessionRunner {
|
|
635
|
+
/** the session's slot of the cancel table (src/duck/cancel.ts), which its connection's `__mason_session` names */
|
|
636
|
+
slot;
|
|
637
|
+
cancels;
|
|
638
|
+
/** the number of the statement `run` or `explain` is asked next (the listener's, carried by the request) */
|
|
639
|
+
next = 0;
|
|
640
|
+
/** the answers being drained (`streamed`): `close` and the next statement wait for them first */
|
|
641
|
+
releasing = new Set();
|
|
642
|
+
/** what runs on the connection now: a statement being started, a chunk being read, an EXPLAIN. `close` waits for it
|
|
643
|
+
* too — a connection closed under DuckDB's pending call aborted the process (a stop during a drain) */
|
|
644
|
+
busy = new Set();
|
|
645
|
+
/** the answer left open on the connection, if any: what `close` drains before it closes the connection */
|
|
646
|
+
openAnswer = null;
|
|
647
|
+
conn;
|
|
648
|
+
log;
|
|
649
|
+
setup;
|
|
650
|
+
constructor(conn, log, slot, cancels, setup) {
|
|
651
|
+
this.conn = conn;
|
|
652
|
+
this.log = log;
|
|
653
|
+
this.slot = slot;
|
|
654
|
+
this.cancels = cancels;
|
|
655
|
+
this.setup = setup;
|
|
656
|
+
}
|
|
657
|
+
/** a connection for one Postgres session, in the cancel table's `slot` */
|
|
658
|
+
static async open(instance, log, slot, cancels) {
|
|
659
|
+
const conn = await instance.connect();
|
|
660
|
+
return new SessionRunner(conn, log, slot, cancels, await SessionSetup.open(conn, slotName(slot)));
|
|
661
|
+
}
|
|
662
|
+
async settle(session) { await this.setup.settle(session); }
|
|
437
663
|
async prepare(sql) {
|
|
438
664
|
try {
|
|
439
665
|
return await this.conn.prepare(sql);
|
|
@@ -613,7 +839,7 @@ export class SessionRunner {
|
|
|
613
839
|
const r = result;
|
|
614
840
|
if (fields !== null && r.columnCount < fields.length)
|
|
615
841
|
throw new StatementError({ code: "XX000", message: `the engine answered ${r.columnCount} column(s) where Postgres's analysis has ${fields.length}: report it (Mason's rewrite)` }, sql);
|
|
616
|
-
// Postgres's columns where its analysis gave them (
|
|
842
|
+
// Postgres's columns where its analysis gave them (the exact numerics' cells DuckDB answers after them are not sent)
|
|
617
843
|
const columns = fields !== null
|
|
618
844
|
? fields.map((f, i) => pgTypedColumn(f.name, f.oid, r.columnType(i), f.typmod))
|
|
619
845
|
: Array.from({ length: r.columnCount }, (_, i) => wireColumn(r.columnName(i), r.columnType(i)));
|
|
@@ -918,7 +1144,7 @@ function exactRows(computed, rows, base, sql, params) {
|
|
|
918
1144
|
}
|
|
919
1145
|
catch (err) {
|
|
920
1146
|
if (err instanceof NumericError)
|
|
921
|
-
throw new StatementError({ code: err.sqlstate, message: err.message }, sql, err.gap);
|
|
1147
|
+
throw new StatementError({ code: err.sqlstate, message: err.message, ...(err.detail === null ? {} : { detail: err.detail }) }, sql, err.gap);
|
|
922
1148
|
throw err;
|
|
923
1149
|
}
|
|
924
1150
|
}
|
|
@@ -933,7 +1159,7 @@ function computedOf(exact, fields) {
|
|
|
933
1159
|
});
|
|
934
1160
|
}
|
|
935
1161
|
/** whether the column's program reads cell j */
|
|
936
|
-
const programReads = (e, j) =>
|
|
1162
|
+
const programReads = (e, j) => cellsOf(e.program).includes(String(j));
|
|
937
1163
|
/** a statement on one line: whole in the log (its ORDER BY, past 160 characters, was what told a slow preview's
|
|
938
1164
|
* cause), cut where it goes to a client in an error's detail */
|
|
939
1165
|
const oneLine = (sql, max = Infinity) => { const t = sql.replace(/\s+/g, " ").trim(); return t.length > max ? `${t.slice(0, max - 3)}…` : t; };
|
package/dist/src/duck/stream.js
CHANGED
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
* unchanged-TOAST update) whose earlier row went in an earlier piece is applied as an UPDATE of the columns it carries,
|
|
46
46
|
* as one whose row the copy holds always is.
|
|
47
47
|
*/
|
|
48
|
+
import { errorText, messageOf } from "../lib/secret-mask.js";
|
|
48
49
|
import { decode } from "../cdc/pgoutput.js";
|
|
49
50
|
import { PgConnection, formatLsn, pgIdent, pgLiteral } from "../cdc/pg-wire.js";
|
|
50
51
|
const lit = (s) => `'${s.replace(/'/g, "''")}'`;
|
|
@@ -262,6 +263,7 @@ export class ReplicationStream {
|
|
|
262
263
|
publication;
|
|
263
264
|
target;
|
|
264
265
|
log;
|
|
266
|
+
errorText;
|
|
265
267
|
flushMs;
|
|
266
268
|
flushRows;
|
|
267
269
|
constructor(o) {
|
|
@@ -270,6 +272,7 @@ export class ReplicationStream {
|
|
|
270
272
|
this.publication = o.publication;
|
|
271
273
|
this.target = o.target;
|
|
272
274
|
this.log = o.log;
|
|
275
|
+
this.errorText = o.errorText ?? ((e) => errorText(e));
|
|
273
276
|
this.flushMs = o.flushMs ?? 500;
|
|
274
277
|
this.flushRows = o.flushRows ?? PIECE_CHANGES;
|
|
275
278
|
this.acked = this.target.applied();
|
|
@@ -289,7 +292,7 @@ export class ReplicationStream {
|
|
|
289
292
|
this.repl?.sendStatus(this.acked, true);
|
|
290
293
|
}
|
|
291
294
|
catch (e) {
|
|
292
|
-
this.log(`replication: could not ask the source for its position (${e
|
|
295
|
+
this.log(`replication: could not ask the source for its position (${this.errorText(e)})`);
|
|
293
296
|
}
|
|
294
297
|
}
|
|
295
298
|
async run() {
|
|
@@ -302,7 +305,7 @@ export class ReplicationStream {
|
|
|
302
305
|
if (this.stopping)
|
|
303
306
|
break;
|
|
304
307
|
const wait = Math.min(30_000, 1_000 * 2 ** Math.min(attempts++, 5));
|
|
305
|
-
this.log(`replication: the source is away (${e
|
|
308
|
+
this.log(`replication: the source is away (${this.errorText(e)}); trying again in ${wait / 1000} s`);
|
|
306
309
|
await new Promise((r) => setTimeout(r, wait));
|
|
307
310
|
continue;
|
|
308
311
|
}
|
|
@@ -329,7 +332,7 @@ export class ReplicationStream {
|
|
|
329
332
|
this.repl = null;
|
|
330
333
|
return e;
|
|
331
334
|
}
|
|
332
|
-
this.log(`replication: the source went away (${e
|
|
335
|
+
this.log(`replication: the source went away (${this.errorText(e)}); reconnecting`);
|
|
333
336
|
}
|
|
334
337
|
finally {
|
|
335
338
|
this.repl?.close();
|
|
@@ -387,7 +390,7 @@ export class ReplicationStream {
|
|
|
387
390
|
repl.sendStatus(this.acked, true);
|
|
388
391
|
}
|
|
389
392
|
catch (e) {
|
|
390
|
-
this.log(`replication: the status update could not be sent (${e
|
|
393
|
+
this.log(`replication: the status update could not be sent (${this.errorText(e)})`);
|
|
391
394
|
}
|
|
392
395
|
}, 10_000);
|
|
393
396
|
const command = `START_REPLICATION SLOT ${pgIdent(this.slot)} LOGICAL 0/0 (proto_version '1', publication_names ${pgLiteral(`"${this.publication.replace(/"/g, '""')}"`)})`;
|
|
@@ -518,6 +521,7 @@ export function pgTime(us) {
|
|
|
518
521
|
}
|
|
519
522
|
/** the source being away — closed, refused, reset, a shutdown — which a reconnect outlasts; anything else is the stream's */
|
|
520
523
|
function away(e) {
|
|
521
|
-
|
|
524
|
+
// a decision: the raw words (src/lib/secret-mask.ts messageOf), never the ones Mason repeats
|
|
525
|
+
const m = messageOf(e);
|
|
522
526
|
return /connection closed|ECONNREFUSED|ECONNRESET|EPIPE|ETIMEDOUT|timed out|terminating connection|the database system is (shutting down|starting up)|server closed|is active for PID/i.test(m);
|
|
523
527
|
}
|