@mason-data/cli 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -73
- package/dist/src/api/error.js +3 -0
- package/dist/src/api/postgres/catalog-worker.js +49 -2
- package/dist/src/api/postgres/catalog.js +368 -25
- package/dist/src/api/postgres/encode.js +3 -1
- package/dist/src/api/postgres/explain.js +3 -2
- package/dist/src/api/postgres/listener.js +11 -3
- package/dist/src/api/postgres/protocol.js +39 -23
- package/dist/src/api/postgres/proxy.js +283 -0
- package/dist/src/api/postgres/schema.js +6 -0
- package/dist/src/api/postgres/server.js +1617 -245
- package/dist/src/api/postgres/translate.js +127 -10
- package/dist/src/api/request-log.js +18 -5
- package/dist/src/cdc/poll.js +5 -5
- package/dist/src/cli/background.js +47 -25
- package/dist/src/cli/bridge-stdin.js +101 -0
- package/dist/src/cli/cancel.js +75 -0
- package/dist/src/cli/command.js +170 -11
- package/dist/src/cli/commands.js +804 -209
- package/dist/src/cli/control.js +59 -0
- package/dist/src/cli/credentials.js +182 -0
- package/dist/src/cli/demo.js +20 -11
- package/dist/src/cli/deploy.js +574 -0
- package/dist/src/cli/dev.js +491 -130
- package/dist/src/cli/doctor.js +131 -38
- package/dist/src/cli/generated-files.js +143 -0
- package/dist/src/cli/guides.js +175 -36
- package/dist/src/cli/hosted.js +850 -0
- package/dist/src/cli/init.js +162 -65
- package/dist/src/cli/login.js +289 -0
- package/dist/src/cli/machine.js +13 -12
- package/dist/src/cli/main.js +11 -5
- package/dist/src/cli/mason.cjs +17 -0
- package/dist/src/cli/mcp.js +31 -22
- package/dist/src/cli/metrics.js +79 -0
- package/dist/src/cli/output-gate.js +176 -0
- package/dist/src/cli/page-share.js +96 -0
- package/dist/src/cli/pg-tls.js +14 -4
- package/dist/src/cli/pg-url.js +4 -0
- package/dist/src/cli/pin.js +19 -14
- package/dist/src/cli/pool-size.cjs +85 -0
- package/dist/src/cli/ports.js +2 -0
- package/dist/src/cli/preflight.js +138 -46
- package/dist/src/cli/project.js +142 -4
- package/dist/src/cli/readiness.js +76 -6
- package/dist/src/cli/registry.js +65 -38
- package/dist/src/cli/replica-hosted.js +665 -0
- package/dist/src/cli/replica.js +76 -0
- package/dist/src/cli/replication-checks.js +7 -5
- package/dist/src/cli/replication-mode.js +105 -26
- package/dist/src/cli/request-filter.js +134 -0
- package/dist/src/cli/roles.js +585 -0
- package/dist/src/cli/run-server.js +637 -48
- package/dist/src/cli/runs.js +1 -1
- package/dist/src/cli/security.js +67 -18
- package/dist/src/cli/serve.js +204 -46
- package/dist/src/cli/share-run.js +329 -0
- package/dist/src/cli/share.js +286 -0
- package/dist/src/cli/source-edit.js +75 -17
- package/dist/src/cli/source-role.js +344 -0
- package/dist/src/cli/sources.js +306 -47
- package/dist/src/cli/style.js +2 -1
- package/dist/src/cli/supervise.js +27 -24
- package/dist/src/cli/sync.js +55 -25
- package/dist/src/cli/token-hosted.js +337 -0
- package/dist/src/cli/tunnel.js +200 -0
- package/dist/src/cli/ui-ssh.js +460 -0
- package/dist/src/cli/ui.js +124 -17
- package/dist/src/cli/usage.js +128 -0
- package/dist/src/cli/user-hosted.js +299 -0
- package/dist/src/cli/user.js +573 -65
- package/dist/src/cli/version.js +9 -7
- package/dist/src/demo/seed.js +29 -0
- package/dist/src/duck/analyze.js +36 -20
- package/dist/src/duck/cancel.js +10 -1
- package/dist/src/duck/collation.js +266 -0
- package/dist/src/duck/constant-key.js +94 -0
- package/dist/src/duck/distinct-key.js +283 -26
- package/dist/src/duck/events.js +165 -0
- package/dist/src/duck/executor-worker.js +20 -11
- package/dist/src/duck/executor.js +47 -33
- package/dist/src/duck/explain.js +60 -46
- package/dist/src/duck/extensions.js +160 -0
- package/dist/src/duck/functions.js +461 -63
- package/dist/src/duck/gaps.js +48 -21
- package/dist/src/duck/generated-plan.js +1070 -0
- package/dist/src/duck/generated.js +906 -0
- package/dist/src/duck/grants.js +176 -0
- package/dist/src/duck/held-types.js +64 -0
- package/dist/src/duck/history.js +127 -0
- package/dist/src/duck/jsonb-order.js +9 -12
- package/dist/src/duck/kept-route.js +6 -5
- package/dist/src/duck/kept.js +15 -13
- package/dist/src/duck/late.js +1469 -40
- package/dist/src/duck/libc-bound.js +341 -0
- package/dist/src/duck/libc-order.js +892 -126
- package/dist/src/duck/pg-eval-worker.js +7 -1
- package/dist/src/duck/pg-eval.js +6 -4
- package/dist/src/duck/pgtree.js +774 -23
- package/dist/src/duck/plan.js +369 -47
- package/dist/src/duck/publication.js +20 -6
- package/dist/src/duck/requests.js +269 -93
- package/dist/src/duck/resolved.js +117 -2
- package/dist/src/duck/rewrite.js +4584 -580
- package/dist/src/duck/row-filter.js +655 -0
- package/dist/src/duck/statement.js +19 -0
- package/dist/src/duck/store.js +439 -177
- package/dist/src/duck/stream.js +19 -10
- package/dist/src/duck/sync.js +1411 -368
- package/dist/src/duck/token.js +4 -1
- package/dist/src/duck/types.js +69 -12
- package/dist/src/duck/usage.js +326 -0
- package/dist/src/duck/users.js +69 -15
- package/dist/src/duck/verified.js +161 -1
- package/dist/src/duck/views.js +5 -5
- package/dist/src/lib/database-name.js +62 -0
- package/dist/src/lib/database-url.js +55 -10
- package/dist/src/lib/dotenv.js +17 -4
- package/dist/src/lib/instant.js +50 -0
- package/dist/src/lib/log-buffer.js +77 -0
- package/dist/src/lib/log-read.js +196 -0
- package/dist/src/lib/pg-numeric.js +390 -49
- package/dist/src/lib/project-version.js +23 -23
- package/dist/src/lib/secret-mask.js +142 -0
- package/dist/src/lib/table-sync.js +282 -0
- package/dist/src/web/app.css +706 -66
- package/dist/src/web/app.js +331 -409
- package/dist/src/web/charts.js +229 -0
- package/dist/src/web/index.html +82 -18
- package/dist/src/web/pages/queries.js +192 -0
- package/dist/src/web/pages/status.js +611 -0
- package/dist/src/web/pages/syncs.js +155 -0
- package/dist/src/web/pages/tables.js +439 -0
- package/dist/src/web/pages/usage.js +397 -0
- package/dist/src/web/pages/users.js +219 -0
- package/dist/src/web/shared.js +584 -0
- package/dist/src/web/vendor/README.md +25 -0
- package/dist/src/web/vendor/geist-1.7.2/Geist-Variable.woff2 +0 -0
- package/dist/src/web/vendor/geist-1.7.2/GeistMono-Variable.woff2 +0 -0
- package/dist/src/web/vendor/geist-1.7.2/LICENSE.txt +92 -0
- package/dist/src/web/vendor/lucide-1.56.0/LICENSE.txt +43 -0
- package/dist/src/web/vendor/lucide-1.56.0/chart-column.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/layout-dashboard.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/logs.mjs +34 -0
- package/dist/src/web/vendor/lucide-1.56.0/menu.mjs +28 -0
- package/dist/src/web/vendor/lucide-1.56.0/square-terminal.mjs +28 -0
- package/dist/src/web/vendor/lucide-1.56.0/table.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/users.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/x.mjs +27 -0
- package/dist/src/web/vendor/uplot-1.6.32/LICENSE.txt +21 -0
- package/dist/src/web/vendor/uplot-1.6.32/uPlot.esm.js +6140 -0
- package/dist/src/web/vendor/uplot-1.6.32/uPlot.min.css +1 -0
- package/dist/src/web/vendor/zilla-slab-5.3.0/LICENSE.txt +93 -0
- package/dist/src/web/vendor/zilla-slab-5.3.0/zilla-slab-latin-700-normal.woff2 +0 -0
- package/package.json +12 -4
package/dist/src/duck/token.js
CHANGED
|
@@ -14,11 +14,14 @@ export function mintToken(apiKey, seconds, now = Date.now()) {
|
|
|
14
14
|
const expiry = Math.floor(now / 1000) + seconds;
|
|
15
15
|
return { token: `${PREFIX}_${expiry}_${mac(apiKey, expiry)}`, expiresAt: new Date(expiry * 1000).toISOString() };
|
|
16
16
|
}
|
|
17
|
+
const TOKEN_RE = /^mason_(\d{1,12})_([A-Za-z0-9_-]{43})$/;
|
|
18
|
+
/** Whether a password is a token by its shape, good or not. */
|
|
19
|
+
export const isToken = (password) => TOKEN_RE.test(password);
|
|
17
20
|
/** null when the token is good; else why not, in a sentence a person reads after "password authentication failed" */
|
|
18
21
|
export function tokenProblem(apiKey, token, now = Date.now()) {
|
|
19
22
|
if (token === apiKey)
|
|
20
23
|
return "the API key is not a password here: mason user add <name> makes a user whose password does not expire";
|
|
21
|
-
const m =
|
|
24
|
+
const m = TOKEN_RE.exec(token);
|
|
22
25
|
if (m === null)
|
|
23
26
|
return "not a Mason user's password or token: mason user add <name> makes a user whose password does not expire";
|
|
24
27
|
const expiry = Number(m[1]);
|
package/dist/src/duck/types.js
CHANGED
|
@@ -11,8 +11,11 @@
|
|
|
11
11
|
* UNION as its text. Each column that diverges carries the reason, and the runner logs it once per statement.
|
|
12
12
|
*/
|
|
13
13
|
import { Buffer } from "node:buffer";
|
|
14
|
-
import { DuckDBTypeId, DuckDBDateValue, DuckDBTimestampValue, DuckDBTimestampTZValue, DuckDBDecimalValue, DuckDBUUIDValue, DuckDBListValue, DuckDBArrayValue, DuckDBIntervalValue, DuckDBBlobValue, DuckDBTimeValue, DuckDBListType, DuckDBArrayType, DuckDBTimestampSecondsValue, DuckDBTimestampMillisecondsValue, DuckDBTimestampNanosecondsValue, } from "@duckdb/node-api";
|
|
14
|
+
import { DuckDBTypeId, DuckDBDateValue, DuckDBTimestampValue, DuckDBTimestampTZValue, DuckDBDecimalValue, DuckDBUUIDValue, DuckDBListValue, DuckDBArrayValue, DuckDBIntervalValue, DuckDBBlobValue, DuckDBTimeValue, DuckDBListType, DuckDBArrayType, DuckDBTimeTZValue, DuckDBTimestampSecondsValue, DuckDBTimestampMillisecondsValue, DuckDBTimestampNanosecondsValue, } from "@duckdb/node-api";
|
|
15
15
|
import { jsonbText } from "./jsonb.js";
|
|
16
|
+
import { gapMark } from "./gaps.js";
|
|
17
|
+
import { heldType, ZONED_HELD } from "./held-types.js";
|
|
18
|
+
import { pgEvalSync } from "./pg-eval.js";
|
|
16
19
|
import { EncodeError, encodeValue, OID, TIMESTAMP } from "../api/postgres/encode.js";
|
|
17
20
|
const MICROS_PER_DAY = 86400000000n;
|
|
18
21
|
const DATE_INFINITY = 2_147_483_647, DATE_NEG_INFINITY = -2_147_483_647;
|
|
@@ -124,7 +127,7 @@ function timestampColumn(name, tz) {
|
|
|
124
127
|
}
|
|
125
128
|
// a zone's offset before year 1 is its local mean time, which Mason does not compute
|
|
126
129
|
if (tz && s.timeZone.toUpperCase() !== "UTC")
|
|
127
|
-
throw new EncodeError(`Mason writes a timestamptz before year 1 only in UTC, not in ${s.timeZone} (
|
|
130
|
+
throw new EncodeError(`Mason writes a timestamptz before year 1 only in UTC, not in ${s.timeZone} (${gapMark("bc-timestamptz-zone")})`, "0A000", "bc-timestamptz-zone");
|
|
128
131
|
return Buffer.from(bcTimestampText(m, tz), "ascii");
|
|
129
132
|
}
|
|
130
133
|
return base.encode(v, format, s);
|
|
@@ -136,6 +139,12 @@ function timeText(micros) {
|
|
|
136
139
|
const secs = micros / 1000000n, frac = micros % 1000000n;
|
|
137
140
|
return `${pad(secs / 3600n)}:${pad((secs % 3600n) / 60n)}:${pad(secs % 60n)}${frac === 0n ? "" : `.${pad(frac, 6).replace(/0+$/, "")}`}`;
|
|
138
141
|
}
|
|
142
|
+
/** A timetz as timetz_out writes it: the time, then its offset as `+HH`, `+HH:MM` or `+HH:MM:SS` (DuckDB's offset, as
|
|
143
|
+
* Postgres writes it, is east of UTC; its stored zone is west) */
|
|
144
|
+
export function timetzText(v) {
|
|
145
|
+
const abs = Math.abs(v.offset), s = abs % 60, m = Math.trunc(abs / 60) % 60;
|
|
146
|
+
return `${timeText(v.micros)}${v.offset < 0 ? "-" : "+"}${pad(Math.trunc(abs / 3600))}${m !== 0 || s !== 0 ? `:${pad(m)}` : ""}${s !== 0 ? `:${pad(s)}` : ""}`;
|
|
147
|
+
}
|
|
139
148
|
/** An interval as Postgres's `interval_out` writes it under IntervalStyle postgres: `1 year 2 mons 3 days 04:05:06.7` */
|
|
140
149
|
export function intervalText(months, days, micros) {
|
|
141
150
|
const parts = [];
|
|
@@ -212,7 +221,7 @@ function arrayElement(text) {
|
|
|
212
221
|
/** Postgres's array OIDs of the element OIDs this file sends */
|
|
213
222
|
const ARRAY_OID = {
|
|
214
223
|
16: 1000, 17: 1001, 20: 1016, 21: 1005, 23: 1007, 25: 1009, 700: 1021, 701: 1022, 1082: 1182, 1083: 1183,
|
|
215
|
-
1114: 1115, 1184: 1185, 1186: 1187, 1700: 1231, 2950: 2951, 3802: 3807,
|
|
224
|
+
1114: 1115, 1184: 1185, 1186: 1187, 1266: 1270, 1700: 1231, 2950: 2951, 3802: 3807,
|
|
216
225
|
};
|
|
217
226
|
function custom(name, oid, size, text, binary, divergence = null) {
|
|
218
227
|
return {
|
|
@@ -267,7 +276,7 @@ export function wireColumn(name, t) {
|
|
|
267
276
|
case DuckDBTypeId.UHUGEINT:
|
|
268
277
|
case DuckDBTypeId.UBIGINT:
|
|
269
278
|
case DuckDBTypeId.BIGNUM:
|
|
270
|
-
return viaApi(name, "numeric", bigintText, `
|
|
279
|
+
return viaApi(name, "numeric", bigintText, `the engine's ${t.toString()} (a sum of integers is one), sent as numeric: Postgres says bigint for a sum of int4 and numeric for a sum of int8`);
|
|
271
280
|
case DuckDBTypeId.FLOAT: return viaApi(name, "float4", (v) => v);
|
|
272
281
|
case DuckDBTypeId.DOUBLE: return viaApi(name, "float8", (v) => v);
|
|
273
282
|
case DuckDBTypeId.DECIMAL: return viaApi(name, "numeric", (v) => v.toString());
|
|
@@ -284,6 +293,8 @@ export function wireColumn(name, t) {
|
|
|
284
293
|
case DuckDBTypeId.TIMESTAMP_TZ: return timestampColumn(name, true);
|
|
285
294
|
case DuckDBTypeId.TIME:
|
|
286
295
|
return custom(name, 1083, 8, (v) => timeText(v.micros), (v) => { const b = Buffer.alloc(8); b.writeBigInt64BE(v.micros); return b; });
|
|
296
|
+
case DuckDBTypeId.TIME_TZ:
|
|
297
|
+
return custom(name, 1266, 12, (v) => timetzText(v), (v) => { const t = v; const b = Buffer.alloc(12); b.writeBigInt64BE(t.micros, 0); b.writeInt32BE(-t.offset, 8); return b; });
|
|
287
298
|
case DuckDBTypeId.INTERVAL:
|
|
288
299
|
return custom(name, 1186, 16, (v) => { const i = v; return intervalText(i.months, i.days, i.micros); }, (v) => { const i = v; const b = Buffer.alloc(16); b.writeBigInt64BE(i.micros, 0); b.writeInt32BE(i.days, 8); b.writeInt32BE(i.months, 12); return b; });
|
|
289
300
|
case DuckDBTypeId.BLOB:
|
|
@@ -303,7 +314,7 @@ export function wireColumn(name, t) {
|
|
|
303
314
|
}
|
|
304
315
|
default: break;
|
|
305
316
|
}
|
|
306
|
-
return custom(name, 25, -1, (v) => String(v), null, `
|
|
317
|
+
return custom(name, 25, -1, (v) => String(v), null, `the engine's ${t.toString()} has no Postgres type here: sent as its text`);
|
|
307
318
|
}
|
|
308
319
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
309
320
|
// A COLUMN OF POSTGRES'S TYPE (docs/DESIGN.md "Postgres decides, DuckDB computes"): the catalog Postgres's
|
|
@@ -312,7 +323,7 @@ export function wireColumn(name, t) {
|
|
|
312
323
|
// int4, its DECIMAL as a numeric, its JSON as jsonb's canonical text. Where DuckDB's value cannot be written as
|
|
313
324
|
// Postgres's type (a timestamp where Postgres says date), DuckDB's own encoding stands and the column says why.
|
|
314
325
|
/** pg_type.typlen of the OIDs a result column may have */
|
|
315
|
-
const TYPLEN = { 16: 1, 18: 1, 19: 64, 20: 8, 21: 2, 23: 4, 26: 4, 700: 4, 701: 8, 1082: 4, 1083: 8, 1114: 8, 1184: 8, 1186: 16, 2950: 16 };
|
|
326
|
+
const TYPLEN = { 16: 1, 18: 1, 19: 64, 20: 8, 21: 2, 23: 4, 26: 4, 700: 4, 701: 8, 1082: 4, 1083: 8, 1114: 8, 1184: 8, 1186: 16, 1266: 12, 2950: 16 };
|
|
316
327
|
const PG_TEXT = new Set([25, 1043, 1042, 19, 18]);
|
|
317
328
|
const PG_INT = { 21: "int2", 23: "int4", 20: "int8" };
|
|
318
329
|
const DUCK_EXACT = new Set([DuckDBTypeId.TINYINT, DuckDBTypeId.SMALLINT, DuckDBTypeId.INTEGER, DuckDBTypeId.BIGINT, DuckDBTypeId.HUGEINT,
|
|
@@ -348,6 +359,45 @@ export function floatAsNumeric(x, digits = 15) {
|
|
|
348
359
|
}
|
|
349
360
|
return s === "-0" ? "0" : s;
|
|
350
361
|
}
|
|
362
|
+
/** the binary forms of the text-held types a driver asks for by default (pgjdbc: point, box): each a float8's bytes */
|
|
363
|
+
const HELD_BINARY = {
|
|
364
|
+
600: floats, 603: floats,
|
|
365
|
+
};
|
|
366
|
+
function floats(text) {
|
|
367
|
+
const parts = text.match(/-?(?:Infinity|NaN|[\d.]+(?:e[-+]?\d+)?)/g) ?? [];
|
|
368
|
+
const b = Buffer.alloc(parts.length * 8);
|
|
369
|
+
parts.forEach((p, i) => b.writeDoubleBE(Number(p), i * 8));
|
|
370
|
+
return b;
|
|
371
|
+
}
|
|
372
|
+
/** a time range's text in the session's zone: Postgres's own output, a value at a time, the last few remembered */
|
|
373
|
+
const zonedTexts = new Map();
|
|
374
|
+
function inZone(type, text, zone) {
|
|
375
|
+
const key = `${zone}\u0000${type}\u0000${text}`;
|
|
376
|
+
let written = zonedTexts.get(key);
|
|
377
|
+
if (written === undefined) {
|
|
378
|
+
written = pgEvalSync(`SELECT $1::${type}::text`, [text], zone)[0]?.[0] ?? text;
|
|
379
|
+
if (zonedTexts.size >= 1024)
|
|
380
|
+
zonedTexts.clear();
|
|
381
|
+
zonedTexts.set(key, written);
|
|
382
|
+
}
|
|
383
|
+
return written;
|
|
384
|
+
}
|
|
385
|
+
/** a column of a type held as its text. A value's text only: Mason writes the binary form of few of them */
|
|
386
|
+
function heldColumn(name, oid, own, size) {
|
|
387
|
+
const binary = HELD_BINARY[oid], zoned = ZONED_HELD.get(oid);
|
|
388
|
+
return {
|
|
389
|
+
name, oid, size, divergence: null,
|
|
390
|
+
encode: (v, format, s) => {
|
|
391
|
+
if (v === null)
|
|
392
|
+
return null;
|
|
393
|
+
if (format === 0)
|
|
394
|
+
return zoned !== undefined ? Buffer.from(inZone(zoned, str(v), s.timeZone), "utf8") : own.encode(v, 0, s);
|
|
395
|
+
if (binary !== undefined)
|
|
396
|
+
return binary(str(v));
|
|
397
|
+
throw new EncodeError(`column ${JSON.stringify(name)} (type OID ${oid}) is held as its text and cannot be sent in binary by Mason: ask for text (${gapMark("binary-held-type")})`, "0A000", "binary-held-type");
|
|
398
|
+
},
|
|
399
|
+
};
|
|
400
|
+
}
|
|
351
401
|
/** The wire column of a result column whose Postgres type the analysis gave, holding DuckDB values of type `t`. */
|
|
352
402
|
export function pgTypedColumn(name, oid, t, typmod = -1) {
|
|
353
403
|
const own = wireColumn(name, t);
|
|
@@ -359,6 +409,16 @@ export function pgTypedColumn(name, oid, t, typmod = -1) {
|
|
|
359
409
|
return retyped(viaApi(name, "numeric", (v) => padDecimals(exactText(v), (typmod - 4) & 0xffff)));
|
|
360
410
|
if (own.oid === oid)
|
|
361
411
|
return retyped(own);
|
|
412
|
+
// a type the copy holds as its text, or an array of one (src/duck/held-types.ts): the text, under Postgres's OID
|
|
413
|
+
const held = heldType(oid);
|
|
414
|
+
if (held !== undefined && (own.oid === 25 || own.oid === 1009))
|
|
415
|
+
return heldColumn(name, oid, own, held.oid === oid ? held.size : -1);
|
|
416
|
+
// an oid element of an oid[], held as its digits
|
|
417
|
+
if (oid === 26 && own.oid === 25)
|
|
418
|
+
return custom(name, 26, 4, str, (v) => { const b = Buffer.alloc(4); b.writeUInt32BE(Number(str(v))); return b; });
|
|
419
|
+
// an oid: DuckDB's UINTEGER
|
|
420
|
+
if (oid === 26 && DUCK_EXACT.has(t.typeId))
|
|
421
|
+
return custom(name, 26, 4, (v) => exactText(v), (v) => { const b = Buffer.alloc(4); b.writeUInt32BE(Number(exactText(v))); return b; });
|
|
362
422
|
// an inet: stored as Postgres's text with its mask (`10.3.6.45/32`), sent as inet_out writes it (a full mask dropped)
|
|
363
423
|
if (oid === 869 && own.oid === 25)
|
|
364
424
|
return custom(name, 869, -1, (v) => inetOut(str(v)), (v) => inetBytes(str(v)));
|
|
@@ -399,16 +459,13 @@ export function pgTypedColumn(name, oid, t, typmod = -1) {
|
|
|
399
459
|
if (oid === 1700 && DUCK_FLOAT.has(t.typeId)) {
|
|
400
460
|
// a double that overflowed is Postgres's numeric overflow (its exact arithmetic raises it)
|
|
401
461
|
return { ...retyped(viaApi(name, "numeric", (v) => { if (v === Infinity || v === -Infinity)
|
|
402
|
-
throw new EncodeError("value overflows numeric format", "22003"); return floatAsNumeric(v); })), divergence: `Postgres computes this numeric exactly;
|
|
462
|
+
throw new EncodeError("value overflows numeric format", "22003"); return floatAsNumeric(v); })), divergence: `Postgres computes this numeric exactly; the engine computed it as a double (${t.toString()}), sent with 15 significant digits` };
|
|
403
463
|
}
|
|
404
|
-
return { ...own, divergence: `Postgres says type OID ${oid};
|
|
464
|
+
return { ...own, divergence: `Postgres says type OID ${oid}; the engine computed ${t.toString()}, sent as its own type` };
|
|
405
465
|
}
|
|
406
|
-
/** How a cell of an exact numeric (src/duck/rewrite.ts ExactColumn) reads as a decimal text, by DuckDB's type: an exact
|
|
407
|
-
* number's digits, a float as Postgres's cast to numeric writes it — or null where the cell needs digits DuckDB did not
|
|
408
|
-
* keep (a double where the program wants exact ones). */
|
|
409
466
|
export function cellText(t, conv) {
|
|
410
467
|
// a numeric's text as Postgres's numeric_out wrote it (a text read by numeric_in, src/duck/pg-eval.ts)
|
|
411
|
-
if (conv === "text")
|
|
468
|
+
if (conv === "text" || conv === "parsed")
|
|
412
469
|
return t.typeId === DuckDBTypeId.VARCHAR ? (v) => String(v) : null;
|
|
413
470
|
if (DUCK_EXACT.has(t.typeId))
|
|
414
471
|
return exactText;
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { MASON_SCHEMA } from "./store.js";
|
|
2
|
+
import { History, Streak, cutoff } from "./history.js";
|
|
3
|
+
import { outcomeOf } from "../api/request-log.js";
|
|
4
|
+
export const USAGE_TABLE = `${MASON_SCHEMA}.usage`;
|
|
5
|
+
export const SIZES_TABLE = `${MASON_SCHEMA}.copy_sizes`;
|
|
6
|
+
/**
|
|
7
|
+
* HOW OFTEN A SAMPLE IS TAKEN: a minute. A chart of a day is 1,440 points and of the retention's 7 days 10,080 (a few
|
|
8
|
+
* hundred KB of rows); a statement's p95 over a minute of a BI tool's traffic is a number, over 5 s mostly one statement;
|
|
9
|
+
* and a sample is a read of the state the commands already ask every few seconds. No setting: nothing an operator
|
|
10
|
+
* would tune it for has been met.
|
|
11
|
+
*/
|
|
12
|
+
export const SAMPLE_SECONDS = 60;
|
|
13
|
+
/** how often each copy's own bytes are measured (`copySizes`): its cost grows with the copy, see above */
|
|
14
|
+
export const SIZE_EVERY_MS = 10 * 60_000;
|
|
15
|
+
const COLUMNS = ["sampled_at", "seconds", "copy_bytes", "copy_rows", "tables_added", "tables_copied", "tables_failed", "oldest_sync_seconds",
|
|
16
|
+
"statements", "statements_refused", "statements_failed", "statements_cancelled", "rows_sent", "bytes_sent", "duration_p50_ms", "duration_p95_ms",
|
|
17
|
+
"stream_lag_bytes", "stream_lag_seconds", "rss_bytes", "heap_used_bytes", "copy_memory_bytes", "copy_memory_limit_bytes", "slot_wal_held_bytes", "disk_free_bytes", "sessions_open"];
|
|
18
|
+
/** the samples' DDL: one row a sample; a level is as of `sampled_at`, a count is of the `seconds` before it */
|
|
19
|
+
export const USAGE_DDL = `CREATE TABLE IF NOT EXISTS ${USAGE_TABLE} (sampled_at TIMESTAMPTZ, seconds DOUBLE, copy_bytes BIGINT, copy_rows BIGINT, ` +
|
|
20
|
+
`tables_added INTEGER, tables_copied INTEGER, tables_failed INTEGER, oldest_sync_seconds DOUBLE, statements BIGINT, statements_refused BIGINT, ` +
|
|
21
|
+
`statements_failed BIGINT, statements_cancelled BIGINT, rows_sent BIGINT, bytes_sent BIGINT, duration_p50_ms INTEGER, duration_p95_ms INTEGER, stream_lag_bytes BIGINT, ` +
|
|
22
|
+
`stream_lag_seconds DOUBLE, rss_bytes BIGINT, heap_used_bytes BIGINT, copy_memory_bytes BIGINT, copy_memory_limit_bytes BIGINT)`;
|
|
23
|
+
/** the columns added since the first samples (a file a run before them made): each added where it is missing */
|
|
24
|
+
export const USAGE_ADDED = ["slot_wal_held_bytes BIGINT", "disk_free_bytes BIGINT", "sessions_open INTEGER"];
|
|
25
|
+
/** each copy's own bytes, every SIZE_EVERY_MS */
|
|
26
|
+
export const SIZES_DDL = `CREATE TABLE IF NOT EXISTS ${SIZES_TABLE} (sampled_at TIMESTAMPTZ, table_name VARCHAR, bytes BIGINT)`;
|
|
27
|
+
const noInterval = () => ({ statements: 0, outcomes: { refused: 0, failed: 0, cancelled: 0 }, rowsSent: 0, bytesSent: 0, durations: [], completed: 0 });
|
|
28
|
+
/**
|
|
29
|
+
* THE COMPLETED STATEMENTS' DURATIONS ONE INTERVAL KEEPS, at most: an interval is a minute, but one whose sample cannot
|
|
30
|
+
* be taken is carried into the next (`sample`), so while samples keep failing it would otherwise keep every statement's
|
|
31
|
+
* duration for as long as they fail. Up to this many the p50 / p95 are exact; past it the interval keeps a uniform
|
|
32
|
+
* random sample of this many (a reservoir: the n-th duration replaces a kept one with probability KEPT / n), whose
|
|
33
|
+
* quantiles estimate the interval's — 100,000 numbers are ~800 KB, ~1,700 statements a second for a minute.
|
|
34
|
+
*/
|
|
35
|
+
export const DURATIONS_KEPT = 100_000;
|
|
36
|
+
/**
|
|
37
|
+
* The q-quantile of sorted values as DuckDB's `quantile_cont` gives it (linear between the two nearest ranks), rounded
|
|
38
|
+
* as the request log's summary rounds it, or null for none: the summary's own definition of a p50 and a p95
|
|
39
|
+
* (tests/duck-usage.test.ts holds the two equal).
|
|
40
|
+
*/
|
|
41
|
+
export function quantileCont(sorted, q) {
|
|
42
|
+
if (sorted.length === 0)
|
|
43
|
+
return null;
|
|
44
|
+
const at = (sorted.length - 1) * q, lo = Math.floor(at), hi = Math.ceil(at);
|
|
45
|
+
return Math.round(sorted[lo] + (sorted[hi] - sorted[lo]) * (at - lo));
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* `quantileCont` over a HISTOGRAM: `[value, count]` pairs sorted by value (each count at least 0), as though every
|
|
49
|
+
* value were listed its count of times — the same ranks and the same interpolation, rounded the same way, without
|
|
50
|
+
* listing them (the request log's summary reads a week of durations this way: src/duck/requests.ts `summary`). Null
|
|
51
|
+
* for none; tests/duck-usage.test.ts holds it equal to DuckDB's `quantile_cont`.
|
|
52
|
+
*/
|
|
53
|
+
export function quantileContOf(histogram, q) {
|
|
54
|
+
const n = histogram.reduce((s, [, c]) => s + c, 0);
|
|
55
|
+
if (n === 0)
|
|
56
|
+
return null;
|
|
57
|
+
const at = (n - 1) * q, lo = Math.floor(at), hi = Math.ceil(at);
|
|
58
|
+
// the value at a 0-based rank: the first whose running count passes it
|
|
59
|
+
const valueAt = (rank) => {
|
|
60
|
+
let seen = 0;
|
|
61
|
+
for (const [v, c] of histogram) {
|
|
62
|
+
seen += c;
|
|
63
|
+
if (rank < seen)
|
|
64
|
+
return v;
|
|
65
|
+
}
|
|
66
|
+
return histogram[histogram.length - 1][0];
|
|
67
|
+
};
|
|
68
|
+
const a = valueAt(lo), b = hi === lo ? a : valueAt(hi);
|
|
69
|
+
return Math.round(a + (b - a) * (at - lo));
|
|
70
|
+
}
|
|
71
|
+
export class UsageSampler {
|
|
72
|
+
interval = noInterval();
|
|
73
|
+
totals = { completed: 0, refused: 0, failed: 0, cancelled: 0, completedMs: 0 };
|
|
74
|
+
latest = null;
|
|
75
|
+
last;
|
|
76
|
+
timer = null;
|
|
77
|
+
sampling = Promise.resolve();
|
|
78
|
+
take = null;
|
|
79
|
+
lastSized = -Infinity;
|
|
80
|
+
sizedOnce = false;
|
|
81
|
+
/** a streak of failures, said once: what failed (a sample taken, written, sized), and how many */
|
|
82
|
+
lost = new Map();
|
|
83
|
+
closed = false;
|
|
84
|
+
history;
|
|
85
|
+
log;
|
|
86
|
+
options;
|
|
87
|
+
days;
|
|
88
|
+
memoryLimitBytes;
|
|
89
|
+
sampleSeconds;
|
|
90
|
+
constructor(history, log, memoryLimitBytes, options) {
|
|
91
|
+
this.history = history;
|
|
92
|
+
this.log = log;
|
|
93
|
+
this.options = options;
|
|
94
|
+
this.days = options.days;
|
|
95
|
+
this.memoryLimitBytes = memoryLimitBytes;
|
|
96
|
+
this.sampleSeconds = (options.sampleMs ?? SAMPLE_SECONDS * 1000) / 1000;
|
|
97
|
+
this.last = this.now();
|
|
98
|
+
}
|
|
99
|
+
/** The samples of a store: their tables made, the rows past the retention deleted, and a trim every hour. Sampling
|
|
100
|
+
* begins with `start`, once the run's facts can be read. */
|
|
101
|
+
static async open(store, log, options) {
|
|
102
|
+
const history = await History.open(store, [USAGE_DDL, ...USAGE_ADDED.map((c) => `ALTER TABLE ${USAGE_TABLE} ADD COLUMN IF NOT EXISTS ${c}`), SIZES_DDL]);
|
|
103
|
+
const u = new UsageSampler(history, log, store.memoryLimitBytes, options);
|
|
104
|
+
await history.keepTrimmed(() => u.trim(), (e) => log(`usage: deleting the samples older than ${u.days} days failed: ${e.message}`));
|
|
105
|
+
return u;
|
|
106
|
+
}
|
|
107
|
+
now() { return this.options.now?.() ?? Date.now(); }
|
|
108
|
+
/** One row of the listener's, as the request log records it: counted in the interval it ends, by the summary's rule
|
|
109
|
+
* (a refused login is no statement; `outcomeOf`, the one outcome rule). Never throws. */
|
|
110
|
+
count(row) {
|
|
111
|
+
if (row.method === "CONNECT")
|
|
112
|
+
return;
|
|
113
|
+
const i = this.interval;
|
|
114
|
+
i.statements++;
|
|
115
|
+
const outcome = outcomeOf(row.sqlstate);
|
|
116
|
+
if (outcome === "ok") {
|
|
117
|
+
this.totals.completed++;
|
|
118
|
+
this.totals.completedMs += row.durationMs;
|
|
119
|
+
}
|
|
120
|
+
else
|
|
121
|
+
this.totals[outcome]++;
|
|
122
|
+
if (outcome === "ok") {
|
|
123
|
+
i.completed++;
|
|
124
|
+
const kept = this.options.durationsKept ?? DURATIONS_KEPT;
|
|
125
|
+
if (i.durations.length < kept)
|
|
126
|
+
i.durations.push(row.durationMs);
|
|
127
|
+
else {
|
|
128
|
+
const j = Math.floor((this.options.random?.() ?? Math.random()) * i.completed);
|
|
129
|
+
if (j < kept)
|
|
130
|
+
i.durations[j] = row.durationMs;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else
|
|
134
|
+
i.outcomes[outcome]++;
|
|
135
|
+
i.rowsSent += row.rows ?? 0;
|
|
136
|
+
i.bytesSent += row.bytesSent ?? 0;
|
|
137
|
+
}
|
|
138
|
+
/** The statements since the process started, by outcome (a copy: the counter itself is never handed out). */
|
|
139
|
+
statementTotals() { return { ...this.totals }; }
|
|
140
|
+
/** The last sample's p50 and p95 of its completed statements; null before the first sample. */
|
|
141
|
+
lastDurations() { return this.latest; }
|
|
142
|
+
/** Takes a sample every interval from `take` (the run's facts), and each copy's bytes every SIZE_EVERY_MS. */
|
|
143
|
+
start(take) {
|
|
144
|
+
this.take = take;
|
|
145
|
+
this.schedule();
|
|
146
|
+
}
|
|
147
|
+
schedule() {
|
|
148
|
+
if (this.closed)
|
|
149
|
+
return;
|
|
150
|
+
// one after the other, never two at once: a sample slower than its interval delays the next
|
|
151
|
+
this.timer = setTimeout(() => { this.timer = null; this.sampling = this.sample().then(() => this.schedule()); }, this.options.sampleMs ?? SAMPLE_SECONDS * 1000);
|
|
152
|
+
this.timer.unref();
|
|
153
|
+
}
|
|
154
|
+
/** a failure of `what`, said once per streak; `ok` ends the streak, said with what it cost */
|
|
155
|
+
failure(what, e) {
|
|
156
|
+
const streak = this.lost.get(what) ?? new Streak();
|
|
157
|
+
this.lost.set(what, streak);
|
|
158
|
+
streak.failed(1, () => this.log(`usage: ${what} failed: ${e.message}; said once until one succeeds`));
|
|
159
|
+
}
|
|
160
|
+
ok(what) {
|
|
161
|
+
this.lost.get(what)?.succeeded((n) => this.log(`usage: ${what} again; ${n} time(s) it was not`));
|
|
162
|
+
}
|
|
163
|
+
/** One sample now: the run's facts, the interval's requests, the memory; written. Resolves whatever happens (a failure is said). */
|
|
164
|
+
async sample() {
|
|
165
|
+
if (this.take === null)
|
|
166
|
+
return;
|
|
167
|
+
let facts;
|
|
168
|
+
let copyMemory;
|
|
169
|
+
try {
|
|
170
|
+
facts = await this.take();
|
|
171
|
+
copyMemory = await this.copyMemory();
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
// Tolerated, narrowly: a sample is history, and the run goes on without it. The interval's counts are KEPT for
|
|
175
|
+
// the next sample, whose `seconds` covers both, so no request goes uncounted.
|
|
176
|
+
this.failure("taking a sample", e);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
this.ok("taking a sample");
|
|
180
|
+
const now = this.now();
|
|
181
|
+
const i = this.interval;
|
|
182
|
+
this.interval = noInterval();
|
|
183
|
+
const seconds = (now - this.last) / 1000;
|
|
184
|
+
this.last = now;
|
|
185
|
+
const sorted = i.durations.sort((a, b) => a - b);
|
|
186
|
+
const mem = this.options.memory?.() ?? process.memoryUsage();
|
|
187
|
+
// the p50 and p95 written are the ones GET /metrics says (lastDurations): one computation
|
|
188
|
+
this.latest = { at: new Date(now).toISOString(), p50Ms: quantileCont(sorted, 0.5), p95Ms: quantileCont(sorted, 0.95) };
|
|
189
|
+
const values = [this.latest.at, seconds, facts.copyBytes, facts.totals.rows, facts.totals.added, facts.totals.copied, facts.totals.failed,
|
|
190
|
+
facts.oldestSyncSeconds, i.statements, i.outcomes.refused, i.outcomes.failed, i.outcomes.cancelled, i.rowsSent, i.bytesSent, this.latest.p50Ms, this.latest.p95Ms,
|
|
191
|
+
facts.streamLag?.bytes ?? null, facts.streamLag?.seconds ?? null, mem.rss, mem.heapUsed, copyMemory, this.memoryLimitBytes, facts.slotWalHeldBytes, facts.diskFreeBytes, facts.sessionsOpen];
|
|
192
|
+
const sql = `INSERT INTO ${USAGE_TABLE} (${COLUMNS.join(", ")}) VALUES (${COLUMNS.map((c, j) => `$${j + 1}${c === "sampled_at" ? "::TIMESTAMPTZ" : ""}`).join(", ")})`;
|
|
193
|
+
try {
|
|
194
|
+
if (this.options.insert !== undefined)
|
|
195
|
+
await this.options.insert(sql, values);
|
|
196
|
+
else
|
|
197
|
+
await this.history.writer.run(sql, values);
|
|
198
|
+
this.ok(`writing a sample to ${USAGE_TABLE}`);
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
// Tolerated, narrowly, as the request log's: a sample that cannot be written must not stop the run. SAID.
|
|
202
|
+
this.failure(`writing a sample to ${USAGE_TABLE}`, e);
|
|
203
|
+
}
|
|
204
|
+
if (now - this.lastSized >= (this.options.sizeEveryMs ?? SIZE_EVERY_MS)) {
|
|
205
|
+
this.lastSized = now;
|
|
206
|
+
await this.size(facts.copied, now);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/** the copy's memory in use: DuckDB's buffer manager's counters, summed */
|
|
210
|
+
async copyMemory() {
|
|
211
|
+
const v = (await this.history.reader.runAndReadAll("SELECT sum(memory_usage_bytes) FROM duckdb_memory()")).getRowsJS()[0]?.[0];
|
|
212
|
+
return v === null || v === undefined ? null : Number(v);
|
|
213
|
+
}
|
|
214
|
+
/** Each served copy's own bytes, written as one batch (see the module's comment for how, and what it costs). */
|
|
215
|
+
async size(copied, now) {
|
|
216
|
+
const t0 = performance.now();
|
|
217
|
+
const rows = [];
|
|
218
|
+
try {
|
|
219
|
+
const want = new Set(copied);
|
|
220
|
+
const tables = (await this.history.reader.runAndReadAll(`SELECT schema_name, table_name FROM duckdb_tables() WHERE NOT internal AND database_name = current_database() AND schema_name <> '${MASON_SCHEMA}'`))
|
|
221
|
+
.getRowsJS();
|
|
222
|
+
// THE COPY'S OWN database: pragma_database_size() has a row for every attached one, and the in-memory catalog the
|
|
223
|
+
// visible names live in (src/duck/kept.ts) has block_size 0 — read first, it made every copy "0 B"
|
|
224
|
+
const block = Number((await this.history.reader.runAndReadAll("SELECT block_size FROM pragma_database_size() WHERE database_name = current_database()")).getRowsJS()[0][0]);
|
|
225
|
+
for (const [schema, table] of tables) {
|
|
226
|
+
const name = `${schema}.${table}`;
|
|
227
|
+
if (!want.has(name))
|
|
228
|
+
continue;
|
|
229
|
+
const q = `${quote(schema)}.${quote(table)}`;
|
|
230
|
+
let blocks;
|
|
231
|
+
try {
|
|
232
|
+
blocks = Number((await this.history.reader.runAndReadAll(`WITH s AS (SELECT block_id, additional_block_ids FROM pragma_storage_info($1) WHERE persistent), ` +
|
|
233
|
+
`b AS (SELECT block_id FROM s WHERE block_id >= 0 UNION SELECT unnest(additional_block_ids) FROM s) SELECT count(*) FROM b`, [q])).getRowsJS()[0][0]);
|
|
234
|
+
}
|
|
235
|
+
catch (e) {
|
|
236
|
+
// a copy dropped since the list was read (mason remove; a copy swapped in again): not sized this time, by design.
|
|
237
|
+
// Anything else is the sizing's failure, said
|
|
238
|
+
if (/does not exist/i.test(e.message))
|
|
239
|
+
continue;
|
|
240
|
+
throw e;
|
|
241
|
+
}
|
|
242
|
+
// NO BLOCK of its own yet is not 0 bytes: its rows are still in the write-ahead log (a small copy is written to
|
|
243
|
+
// the file only at a checkpoint), so it is not measured this time — a page said "0.0 KiB" for a 5,000-row copy
|
|
244
|
+
if (blocks === 0)
|
|
245
|
+
continue;
|
|
246
|
+
rows.push([name, blocks * block]);
|
|
247
|
+
}
|
|
248
|
+
if (rows.length > 0) {
|
|
249
|
+
const sql = `INSERT INTO ${SIZES_TABLE} (sampled_at, table_name, bytes) VALUES ` + rows.map((_, i) => `($1::TIMESTAMPTZ, $${i * 2 + 2}, $${i * 2 + 3})`).join(", ");
|
|
250
|
+
const values = [new Date(now).toISOString(), ...rows.flat()];
|
|
251
|
+
if (this.options.insert !== undefined)
|
|
252
|
+
await this.options.insert(sql, values);
|
|
253
|
+
else
|
|
254
|
+
await this.history.writer.run(sql, values);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch (e) {
|
|
258
|
+
this.failure("measuring each synced table's size", e);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
this.ok("measuring each synced table's size");
|
|
262
|
+
// the first measure of a run is said with its time: what it costs on this machine's copies, read from the log
|
|
263
|
+
if (!this.sizedOnce) {
|
|
264
|
+
this.sizedOnce = true;
|
|
265
|
+
this.log(`usage: measured ${rows.length} synced tables' own size in ${Math.round(performance.now() - t0)} ms (every ${Math.round((this.options.sizeEveryMs ?? SIZE_EVERY_MS) / 60_000)} min)`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/** Deletes the samples and sizes older than the retention, counted back from the module's ONE clock (`now`, which
|
|
269
|
+
* stamps each sample), never the engine's `now()` (src/duck/events.ts `trim` says why). */
|
|
270
|
+
async trim() {
|
|
271
|
+
const before = cutoff(this.now(), this.days);
|
|
272
|
+
await this.history.writer.run(`DELETE FROM ${USAGE_TABLE} WHERE sampled_at < $1::TIMESTAMPTZ`, [before]);
|
|
273
|
+
await this.history.writer.run(`DELETE FROM ${SIZES_TABLE} WHERE sampled_at < $1::TIMESTAMPTZ`, [before]);
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* The samples from `since` until `until`, oldest first; with `stepSeconds`, re-bucketed in ONE query: buckets of that
|
|
277
|
+
* many seconds from `since`, each count summed and each level at its highest (the disk free at its lowest). Committed rows only.
|
|
278
|
+
*/
|
|
279
|
+
async samples(o) {
|
|
280
|
+
const values = [o.since, o.until];
|
|
281
|
+
const range = `sampled_at >= $1::TIMESTAMPTZ AND sampled_at < $2::TIMESTAMPTZ`;
|
|
282
|
+
let sql;
|
|
283
|
+
if (o.stepSeconds === undefined) {
|
|
284
|
+
sql = `SELECT epoch_ms(sampled_at) AS at, ${COLUMNS.slice(1).join(", ")} FROM ${USAGE_TABLE} WHERE ${range} ORDER BY sampled_at`;
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
values.push(o.stepSeconds);
|
|
288
|
+
const sum = new Set(["seconds", "statements", "statements_refused", "statements_failed", "statements_cancelled", "rows_sent", "bytes_sent"]);
|
|
289
|
+
sql = `SELECT epoch_ms(time_bucket(to_seconds($3::BIGINT), sampled_at, $1::TIMESTAMPTZ)) AS at, ` +
|
|
290
|
+
COLUMNS.slice(1).map((c) => `${sum.has(c) ? "sum" : c === "disk_free_bytes" ? "min" : "max"}(${c}) AS ${c}`).join(", ") +
|
|
291
|
+
` FROM ${USAGE_TABLE} WHERE ${range} GROUP BY 1 ORDER BY 1`;
|
|
292
|
+
}
|
|
293
|
+
const rows = (await this.history.reader.runAndReadAll(sql, values)).getRowObjectsJson();
|
|
294
|
+
return rows.map(sampleOf);
|
|
295
|
+
}
|
|
296
|
+
/** Each copy's own bytes as last measured within the retention, by table. */
|
|
297
|
+
async copySizes() {
|
|
298
|
+
const rows = (await this.history.reader.runAndReadAll(`SELECT table_name, arg_max(bytes, sampled_at) AS bytes, epoch_ms(max(sampled_at)) AS at FROM ${SIZES_TABLE} WHERE bytes > 0 GROUP BY table_name ORDER BY table_name`))
|
|
299
|
+
.getRowsJS();
|
|
300
|
+
return rows.map(([table, bytes, at]) => ({ table, bytes: Number(bytes), at: new Date(Number(at)).toISOString() }));
|
|
301
|
+
}
|
|
302
|
+
/** Stops sampling, waits for a sample in flight, and closes the samples' connections (mason stop). */
|
|
303
|
+
async close() {
|
|
304
|
+
this.closed = true;
|
|
305
|
+
if (this.timer !== null) {
|
|
306
|
+
clearTimeout(this.timer);
|
|
307
|
+
this.timer = null;
|
|
308
|
+
}
|
|
309
|
+
this.history.stopTrimming();
|
|
310
|
+
await this.sampling;
|
|
311
|
+
this.history.closeConnections();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const quote = (name) => `"${name.replaceAll('"', '""')}"`;
|
|
315
|
+
const num = (v) => (v === null || v === undefined ? null : Number(v));
|
|
316
|
+
function sampleOf(r) {
|
|
317
|
+
return {
|
|
318
|
+
at: new Date(Number(r.at)).toISOString(), seconds: Number(r.seconds), copyBytes: num(r.copy_bytes), copyRows: Number(r.copy_rows ?? 0),
|
|
319
|
+
tablesAdded: Number(r.tables_added ?? 0), tablesCopied: Number(r.tables_copied ?? 0), tablesFailed: Number(r.tables_failed ?? 0), oldestSyncSeconds: num(r.oldest_sync_seconds),
|
|
320
|
+
statements: Number(r.statements ?? 0), statementsRefused: Number(r.statements_refused ?? 0), statementsFailed: Number(r.statements_failed ?? 0),
|
|
321
|
+
statementsCancelled: Number(r.statements_cancelled ?? 0), rowsSent: Number(r.rows_sent ?? 0), bytesSent: Number(r.bytes_sent ?? 0), durationP50Ms: num(r.duration_p50_ms), durationP95Ms: num(r.duration_p95_ms),
|
|
322
|
+
streamLagBytes: num(r.stream_lag_bytes), streamLagSeconds: num(r.stream_lag_seconds),
|
|
323
|
+
rssBytes: Number(r.rss_bytes), heapUsedBytes: Number(r.heap_used_bytes), copyMemoryBytes: num(r.copy_memory_bytes), copyMemoryLimitBytes: Number(r.copy_memory_limit_bytes),
|
|
324
|
+
slotWalHeldBytes: num(r.slot_wal_held_bytes), diskFreeBytes: num(r.disk_free_bytes), sessionsOpen: num(r.sessions_open),
|
|
325
|
+
};
|
|
326
|
+
}
|