@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/cli/dev.js
CHANGED
|
@@ -29,16 +29,19 @@ import * as path from "node:path";
|
|
|
29
29
|
import { choosePgPort, freePort } from "./ports.js";
|
|
30
30
|
import { failed } from "./command.js";
|
|
31
31
|
import { checkSource, connectSource, maskedUrl, readSourceFacts, remoteConfirmation, remoteWarning } from "./preflight.js";
|
|
32
|
-
import { createRunServer, metricsFacts, usageFacts } from "./run-server.js";
|
|
32
|
+
import { createRunServer, listenOnLoopback, metricsFacts, usageFacts } from "./run-server.js";
|
|
33
|
+
import { readGenerated } from "./generated-files.js";
|
|
33
34
|
import { addedTables, effectiveSync, entryName, replicatedEntries, rereadSources, shownPath, syncWritten } from "./sources.js";
|
|
34
35
|
import { newToken } from "./security.js";
|
|
35
|
-
import { liveRun } from "./runs.js";
|
|
36
|
-
import { DEMO_DIR, DUCK_FILE, projectSource, startSettingRefusal, theProject, writeConfig } from "./project.js";
|
|
36
|
+
import { alive, liveRun } from "./runs.js";
|
|
37
|
+
import { DEMO_DIR, DUCK_FILE, projectReadOnlySetting, projectSource, startSettingRefusal, theProject, writeConfig } from "./project.js";
|
|
37
38
|
import { closedWithin, closingOnce, DEMO_STILL_CLOSING, demoChanges, demoChangesRefusal, demoPort, demoSettings, openDemo } from "./demo.js";
|
|
38
39
|
import { postgresApiUrl, readPgTls } from "../api/postgres/listener.js";
|
|
39
40
|
import { connectDatabase } from "../lib/database-name.js";
|
|
40
41
|
import { certificateFingerprint, elsewhereTip, ensureSelfSigned, listenerText, runListenerSettings, selfSignedPair, SELF_SIGNED_FILES } from "./pg-tls.js";
|
|
41
42
|
import { RunShares } from "./share-run.js";
|
|
43
|
+
import { PageShares } from "./page-share.js";
|
|
44
|
+
import { sweepTunnelFiles } from "./tunnel.js";
|
|
42
45
|
import { startPostgresApi, timeoutMs } from "../api/postgres/server.js";
|
|
43
46
|
import { RequestLedger, REQUESTS_TABLE } from "../duck/requests.js";
|
|
44
47
|
import { historyDaysSetting } from "../duck/history.js";
|
|
@@ -47,17 +50,19 @@ import { USAGE_TABLE, UsageSampler } from "../duck/usage.js";
|
|
|
47
50
|
import { withPlaceholders } from "./pg-url.js";
|
|
48
51
|
import { UI } from "./ui.js";
|
|
49
52
|
import { cliPinProblem, ensurePin } from "./pin.js";
|
|
50
|
-
import { DEFAULT_MEMORY_LIMIT, DuckStore, DUCKDB_VERSION } from "../duck/store.js";
|
|
51
|
-
import { copyChunkSecondsOf, copyParallelOf, Sync, syncModeOf } from "../duck/sync.js";
|
|
53
|
+
import { DEFAULT_MEMORY_LIMIT, DuckStore, DUCKDB_VERSION, machineMemory, memoryProblems, memoryProblemText, memoryWarning } from "../duck/store.js";
|
|
54
|
+
import { copyChunkSecondsOf, copyParallelOf, humanBytes, Sync, syncModeOf } from "../duck/sync.js";
|
|
52
55
|
import { storeJsonPathsOf } from "../duck/kept.js";
|
|
53
|
-
import { readLibcOrder } from "../duck/libc-order.js";
|
|
54
|
-
import { isLibcCollation } from "../duck/
|
|
56
|
+
import { libcOrderNote, readLibcOrder } from "../duck/libc-order.js";
|
|
57
|
+
import { isLibcCollation } from "../duck/collation.js";
|
|
58
|
+
import { pgIdent } from "../cdc/pg-wire.js";
|
|
55
59
|
import { listenerLogins, LiveUsers, readUsers, USERS_FILE } from "../duck/users.js";
|
|
56
60
|
import { exampleRole, LiveRoles, missingAttributesText, noRoleWayOut, readRoles, resolveRoles, rolesDefaultLine, rolesFile, usersMissingAttributes, usersWithNoRole, usersWithoutRole } from "./roles.js";
|
|
57
61
|
import { READY_LINE, readRestarts } from "./supervise.js";
|
|
58
62
|
import { dropOwnReplication, dropStatements, liveReadinessFailure, madeReplication, namedPublication, projectReplication, recordRefusal, replicationReadiness, staleReplication } from "./replication-mode.js";
|
|
59
63
|
import { textEntry } from "../lib/log.js";
|
|
60
64
|
import { LogBuffer } from "../lib/log-buffer.js";
|
|
65
|
+
import { logPath } from "../lib/log-read.js";
|
|
61
66
|
import { BIG_TABLE_ROWS, syncText } from "../lib/table-sync.js";
|
|
62
67
|
const say = (m) => { process.stdout.write(`${m}\n`); };
|
|
63
68
|
/**
|
|
@@ -115,6 +120,9 @@ async function closeDemo(hosted, closed) {
|
|
|
115
120
|
}
|
|
116
121
|
/** how soon a pass that failed (the database away) is tried again, and replication mode's pass when a table waits */
|
|
117
122
|
const PASS_RETRY_MS = 5000;
|
|
123
|
+
/** how long a libc collation's order may take to read from the source (a few seconds of read-only SELECTs): past it the
|
|
124
|
+
* read is given up, and the statements that need it refused by name */
|
|
125
|
+
const LIBC_READ_MS = 120_000;
|
|
118
126
|
/** the tables by their sync, as the start line says them: ` (2 polled, every 5s and 30s; 1 replicated; 1 off)` */
|
|
119
127
|
function syncCounts(tables) {
|
|
120
128
|
const polled = tables.flatMap((t) => t.sync?.mode === "poll" ? [t.sync.every] : []);
|
|
@@ -201,11 +209,29 @@ async function run(o, hosted) {
|
|
|
201
209
|
const setting = startSettingRefusal(project, process.env, { serve: o.serve !== undefined });
|
|
202
210
|
if (setting !== null)
|
|
203
211
|
return failed("start", setting, { json: false });
|
|
212
|
+
// THE PROJECT'S FILES DEPLOYED READ-ONLY (MASON_PROJECT_READONLY=1): recorded in the config at every start, and removed
|
|
213
|
+
// at a start without, so a command beside the run — up or down, whatever its own environment — is refused as the run
|
|
214
|
+
// is (src/cli/project.ts projectWriteRefusal reads the record and the environment; a value not 0 or 1 is refused above)
|
|
215
|
+
if (projectReadOnlySetting(process.env) === true) {
|
|
216
|
+
if (cfg.projectReadOnly !== true) {
|
|
217
|
+
cfg.projectReadOnly = true;
|
|
218
|
+
writeConfig(home, cfg);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
else if (cfg.projectReadOnly !== undefined) {
|
|
222
|
+
delete cfg.projectReadOnly;
|
|
223
|
+
writeConfig(home, cfg);
|
|
224
|
+
}
|
|
204
225
|
// the history's days (MASON_HISTORY_DAYS, a number of days; the old MASON_REQUEST_LOG_DAYS refused, named), read
|
|
205
226
|
// before anything opens (src/duck/history.ts); every history below keeps them
|
|
206
227
|
const keptDays = historyDaysSetting(process.env);
|
|
207
228
|
if (typeof keptDays !== "number")
|
|
208
229
|
return failed("start", { code: "refused", ...keptDays }, { json: false });
|
|
230
|
+
// MASON_DUCKDB_MEMORY not an amount (src/duck/store.ts memoryProblems), refused before anything opens (mason serve has
|
|
231
|
+
// said it already, with its other settings)
|
|
232
|
+
const memory = memoryProblems(process.env)[0];
|
|
233
|
+
if (memory !== undefined)
|
|
234
|
+
return failed("start", { code: "refused", ...memory }, { json: false });
|
|
209
235
|
// THE SOURCE (src/cli/sources.ts): the database and the tables to copy, from the project's sources file; a server's
|
|
210
236
|
// URL from its environment first (mason serve is configured by it alone). Read here, its failure said below where it
|
|
211
237
|
// always was (after the listener's checks): a demo project's port is needed first
|
|
@@ -239,7 +265,7 @@ async function run(o, hosted) {
|
|
|
239
265
|
return failed("start", pgTls, { json: false });
|
|
240
266
|
const chosen = new Set([pg.port, ...demoPorts]);
|
|
241
267
|
// a server's project folder is the team's, committed, and may be mounted read-only: mason serve writes nothing there
|
|
242
|
-
const wrotePin = o.serve ? null : ensurePin(folder, o.version);
|
|
268
|
+
const wrotePin = o.serve ? null : ensurePin(folder, o.version, project.root);
|
|
243
269
|
if (wrotePin)
|
|
244
270
|
say(` wrote ${path.relative(project.root, wrotePin)}: version ${o.version}, the Mason this project is written for — commit it`);
|
|
245
271
|
if ("message" in src)
|
|
@@ -248,7 +274,7 @@ async function run(o, hosted) {
|
|
|
248
274
|
const demoFile = demoChangesRefusal(cfg, src, shownPath(project.root, src.file));
|
|
249
275
|
if (demoFile)
|
|
250
276
|
return failed("start", demoFile, { json: false });
|
|
251
|
-
const logFile =
|
|
277
|
+
const logFile = logPath(home, "run");
|
|
252
278
|
const log = fs.createWriteStream(logFile, { flags: "a" });
|
|
253
279
|
// one entry a line: a multi-line message's further lines indented under it (src/lib/log.ts `textEntry`);
|
|
254
280
|
// mason serve gets the same entry, for the container's stdout (src/cli/serve.ts)
|
|
@@ -283,7 +309,7 @@ async function run(o, hosted) {
|
|
|
283
309
|
const db = maskedUrl(src.resolved);
|
|
284
310
|
begin(`Connecting to ${db}`);
|
|
285
311
|
const checkSaid = [];
|
|
286
|
-
const checked = await checkSource(src.resolved, { remote: cfg.remote, cmd: "mason start", say: (m) => checkSaid.push(m),
|
|
312
|
+
const checked = await checkSource(src.resolved, { remote: cfg.remote, cmd: "mason start", say: (m) => checkSaid.push(m), name: src.from,
|
|
287
313
|
// the checklist's line says it is connecting; everything else checkSource says is the answer
|
|
288
314
|
progress: () => undefined });
|
|
289
315
|
if ("needsRemote" in checked) {
|
|
@@ -400,7 +426,10 @@ async function run(o, hosted) {
|
|
|
400
426
|
if (rolesRead !== null && "message" in rolesRead)
|
|
401
427
|
return failed("start", rolesRead, { json: false });
|
|
402
428
|
if (rolesRead !== null) {
|
|
403
|
-
|
|
429
|
+
// a role may name a generated table, whose columns the catalog knows only once it is built (src/duck/generated.ts)
|
|
430
|
+
const generatedFiles = readGenerated(folder, project.root);
|
|
431
|
+
const generatedNames = [...generatedFiles.definitions, ...generatedFiles.refused].map((g) => ({ source: g.name, columns: [], pending: true }));
|
|
432
|
+
const resolved = resolveRoles(rolesRead, [...firstTables, ...generatedNames], project.root);
|
|
404
433
|
if ("message" in resolved)
|
|
405
434
|
return failed("start", resolved, { json: false });
|
|
406
435
|
}
|
|
@@ -436,8 +465,10 @@ async function run(o, hosted) {
|
|
|
436
465
|
let store;
|
|
437
466
|
// its memory bounded (src/duck/store.ts DEFAULT_MEMORY_LIMIT): past it a statement spills, or fails alone
|
|
438
467
|
const memoryLimit = process.env.MASON_DUCKDB_MEMORY || undefined;
|
|
468
|
+
// what statements may spill (src/duck/store.ts spillCeiling): past it a statement fails alone, and the disk is kept
|
|
469
|
+
const spillLimit = process.env.MASON_SPILL_LIMIT || undefined;
|
|
439
470
|
try {
|
|
440
|
-
store = await DuckStore.open(duckFile, (l) => logLine(`[engine] ${l}`), memoryLimit === undefined ? {} : { memoryLimit });
|
|
471
|
+
store = await DuckStore.open(duckFile, (l) => logLine(`[engine] ${l}`), { ...(memoryLimit === undefined ? {} : { memoryLimit }), ...(spillLimit === undefined ? {} : { spillLimit }) });
|
|
441
472
|
}
|
|
442
473
|
catch (e) {
|
|
443
474
|
fail("Opening the synced data");
|
|
@@ -454,7 +485,16 @@ async function run(o, hosted) {
|
|
|
454
485
|
else
|
|
455
486
|
say(` ! the engine's thread stopped: ${reason}`);
|
|
456
487
|
});
|
|
457
|
-
done(`the synced data in ${path.relative(project.root, duckFile)} (mason destroy deletes it; engine ${DUCKDB_VERSION}), at most ${(memoryLimit ?? DEFAULT_MEMORY_LIMIT).trim()} of memory (MASON_DUCKDB_MEMORY), spilling to ${path.relative(project.root, duckFile)}.tmp`);
|
|
488
|
+
done(`the synced data in ${path.relative(project.root, duckFile)} (mason destroy deletes it; engine ${DUCKDB_VERSION}), at most ${(memoryLimit ?? DEFAULT_MEMORY_LIMIT).trim()} of memory (MASON_DUCKDB_MEMORY), spilling at most ${humanBytes(store.spillLimitBytes)} to ${path.relative(project.root, duckFile)}.tmp (MASON_SPILL_LIMIT)`);
|
|
489
|
+
// UNDER THE MEMORY FLOOR the run goes on and warns (src/duck/store.ts memoryWarning, the one text): here at its start,
|
|
490
|
+
// in its log, and in its warnings, which mason status, --json and the web page read for as long as it runs
|
|
491
|
+
const memoryWarned = memoryWarning(machineMemory(), process.env);
|
|
492
|
+
if (memoryWarned !== null) {
|
|
493
|
+
// the log (mason serve's stdout among its writers), and mason start's own terminal
|
|
494
|
+
logLine(`[engine] warning: ${memoryProblemText(memoryWarned)}`);
|
|
495
|
+
if (o.serve === undefined)
|
|
496
|
+
say(` ! ${memoryProblemText(memoryWarned)}`);
|
|
497
|
+
}
|
|
458
498
|
const syncMode = syncModeOf(process.env);
|
|
459
499
|
// THE SYNC'S EVENTS (src/duck/events.ts): its failures and notable events, kept as the request log is, before the sync
|
|
460
500
|
// that records them starts
|
|
@@ -504,7 +544,8 @@ async function run(o, hosted) {
|
|
|
504
544
|
logLine(`[sync] ${r.said}`);
|
|
505
545
|
sources = { file: r.file, tables: r.tables, problem: r.problem };
|
|
506
546
|
sync.setViews(facts.views ?? []);
|
|
507
|
-
|
|
547
|
+
sync.setCollations(facts.collations ?? []);
|
|
548
|
+
return sync.setTables(sources.tables);
|
|
508
549
|
};
|
|
509
550
|
if (pg.said)
|
|
510
551
|
say(` ${pg.said}`);
|
|
@@ -513,6 +554,7 @@ async function run(o, hosted) {
|
|
|
513
554
|
// A LIBC COLLATION'S ORDER IS READ FROM THE DATABASE (src/duck/libc-order.ts): once, a few read-only SELECTs, kept in
|
|
514
555
|
// the copy and read again when the database's collation version moves (an OS upgrade's glibc)
|
|
515
556
|
let libc;
|
|
557
|
+
let textOrder = null;
|
|
516
558
|
const coll = facts.collation;
|
|
517
559
|
if (coll !== undefined && isLibcCollation(coll.provider, coll.collate)) {
|
|
518
560
|
const reading = `Reading how ${source.database} orders text (${coll.collate}${coll.version ? `, version ${coll.version}` : ""})`;
|
|
@@ -532,6 +574,10 @@ async function run(o, hosted) {
|
|
|
532
574
|
libc = r.order;
|
|
533
575
|
if (r.read)
|
|
534
576
|
done(`Read how ${source.database} orders text (${coll.collate}): kept with the synced data, read again only when its collation version moves`);
|
|
577
|
+
// what the read leaves to say is in the log and in mason status's Text order; an order not followed at the start too
|
|
578
|
+
textOrder = libcOrderNote(r.order);
|
|
579
|
+
if (textOrder !== null)
|
|
580
|
+
(r.order.unread ? say : logLine)(` ! ${textOrder}`);
|
|
535
581
|
}
|
|
536
582
|
catch (e) {
|
|
537
583
|
fail(reading);
|
|
@@ -542,6 +588,49 @@ async function run(o, hosted) {
|
|
|
542
588
|
return 1;
|
|
543
589
|
}
|
|
544
590
|
}
|
|
591
|
+
// EVERY OTHER LIBC COLLATION'S ORDER (src/duck/collation.ts), read the same way under its own name: a synced column's
|
|
592
|
+
// at the start, one a statement names first on its first use (the plan waits for it, under a bound). A column's that
|
|
593
|
+
// cannot be read is said, and its statements refused by name (libc-locale-unread) until a read succeeds
|
|
594
|
+
const readNamed = async (sc) => {
|
|
595
|
+
const c = await connectSource(source);
|
|
596
|
+
if ("failure" in c)
|
|
597
|
+
throw new Error(`could not connect to read it: ${c.failure.message}`);
|
|
598
|
+
const read = readLibcOrder(async (sql) => (await c.pg.query(sql)).rows, sc.collate ?? "C", sc.version ?? null, `${pgIdent(sc.schema)}.${pgIdent(sc.name)}`);
|
|
599
|
+
// (a read given up at the bound ends when its connection closes: said, as the race has moved on)
|
|
600
|
+
read.catch((e) => logLine(`[engine] the read of the collation ${sc.name} ended: ${e.message}`));
|
|
601
|
+
let timer;
|
|
602
|
+
const bound = new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(`the read took longer than ${LIBC_READ_MS / 1000} s`)), LIBC_READ_MS); });
|
|
603
|
+
try {
|
|
604
|
+
return await Promise.race([read, bound]);
|
|
605
|
+
}
|
|
606
|
+
finally {
|
|
607
|
+
clearTimeout(timer);
|
|
608
|
+
c.pg.close();
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
const libcOrders = new Map();
|
|
612
|
+
const byName = new Map((facts.collations ?? []).map((c) => [`${c.schema}\u0000${c.name}`, c]));
|
|
613
|
+
const columnLibc = new Map();
|
|
614
|
+
for (const t of sources.tables)
|
|
615
|
+
for (const col of t.columns) {
|
|
616
|
+
const sc = col.collation === undefined ? undefined : byName.get(`${col.collation.schema}\u0000${col.collation.name}`);
|
|
617
|
+
if (sc !== undefined && sc.provider === "c" && sc.collate !== null && isLibcCollation("c", sc.collate) && !(coll !== undefined && isLibcCollation(coll.provider, coll.collate) && sc.collate === coll.collate))
|
|
618
|
+
columnLibc.set(sc.collate, sc);
|
|
619
|
+
}
|
|
620
|
+
for (const sc of columnLibc.values()) {
|
|
621
|
+
try {
|
|
622
|
+
const r = await store.libcOrder(sc.collate, sc.version ?? null, () => readNamed(sc));
|
|
623
|
+
libcOrders.set(sc.collate, r.order);
|
|
624
|
+
if (r.read)
|
|
625
|
+
logLine(`[engine] read how ${source.database} orders text under the collation ${sc.name} (${sc.collate}): kept with the synced data`);
|
|
626
|
+
const note = libcOrderNote(r.order);
|
|
627
|
+
if (note !== null)
|
|
628
|
+
logLine(`[engine] ! ${note}`);
|
|
629
|
+
}
|
|
630
|
+
catch (e) {
|
|
631
|
+
say(` ! the order of the collation ${sc.name} (${sc.collate}) a synced column has could not be read: ${e.message}; its sorts are refused until it is`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
545
634
|
begin("Starting the Postgres listener");
|
|
546
635
|
// ONE STATEMENT'S LIMIT (server.ts armTimeout): a runaway query must not hold DuckDB for ever; a session's
|
|
547
636
|
// statement_timeout may only shorten it. Postgres's units (5min, 90s, 300000); 0 for none
|
|
@@ -586,7 +675,10 @@ async function run(o, hosted) {
|
|
|
586
675
|
}
|
|
587
676
|
let duckListener;
|
|
588
677
|
try {
|
|
589
|
-
duckListener = await store.listener(facts.collation, undefined, libc, () => sync.state(), sync.kept.enabled ? sync.kept : undefined)
|
|
678
|
+
duckListener = await store.listener(facts.collation, undefined, libc, () => [...sync.state(), ...sync.generated.copies()], sync.kept.enabled ? sync.kept : undefined, { ...(facts.collations !== undefined ? { collations: facts.collations } : {}), libcOrders, readLibc: async (sc) => {
|
|
679
|
+
logLine(`[engine] reading how ${source.database} orders text under the collation ${sc.name} (${sc.collate}), which a statement names first`);
|
|
680
|
+
return readNamed(sc);
|
|
681
|
+
} });
|
|
590
682
|
}
|
|
591
683
|
catch (e) {
|
|
592
684
|
fail("Starting the Postgres listener");
|
|
@@ -604,11 +696,13 @@ async function run(o, hosted) {
|
|
|
604
696
|
const users = new LiveUsers(path.join(home, USERS_FILE), cfg.apiKey);
|
|
605
697
|
// THE ROLES (src/cli/roles.ts): the file read again when it or the copied tables change; a session with a role is
|
|
606
698
|
// held to it at every statement, and the catalog is rebuilt with the roles (src/api/postgres/server.ts)
|
|
607
|
-
const liveRoles = new LiveRoles(folder, project.root, () => sources.tables);
|
|
699
|
+
const liveRoles = new LiveRoles(folder, project.root, () => [...sources.tables, ...sync.generated.named()]);
|
|
608
700
|
let api;
|
|
609
701
|
try {
|
|
610
702
|
api = await startPostgresApi({
|
|
611
703
|
port: pgAddress.port, host: pgAddress.host, tls: pgTls.tls, database, masonVersion: o.version, product: "mason-data", statementTimeoutMs,
|
|
704
|
+
// behind a TCP proxy (MASON_PG_PROXY_FROM, src/api/postgres/proxy.ts): its PROXY header read from its addresses alone
|
|
705
|
+
...(listen.proxyFrom !== undefined ? { proxyFrom: listen.proxyFrom } : {}),
|
|
612
706
|
...listenerLogins(users, cfg.apiKey),
|
|
613
707
|
roles: () => liveRoles.now(),
|
|
614
708
|
schema: () => sync.schema(),
|
|
@@ -629,7 +723,7 @@ async function run(o, hosted) {
|
|
|
629
723
|
await store.close();
|
|
630
724
|
return 1;
|
|
631
725
|
}
|
|
632
|
-
done(`Postgres listener on ${listenerText(pgTls.listener, api.port)}; ${statementTimeoutMs === 0 ? "statements without a time limit" : `each statement at most ${timeoutSetting.trim()}`} (MASON_STATEMENT_TIMEOUT); ` +
|
|
726
|
+
done(`Postgres listener on ${listenerText(pgTls.listener, api.port)}; ${listen.proxyFrom !== undefined ? `a PROXY protocol v2 header required from ${listen.proxyFrom.entries.join(", ")} (MASON_PG_PROXY_FROM); ` : ""}${statementTimeoutMs === 0 ? "statements without a time limit" : `each statement at most ${timeoutSetting.trim()}`} (MASON_STATEMENT_TIMEOUT); ` +
|
|
633
727
|
`every request recorded for ${keptDays} day${keptDays === 1 ? "" : "s"} (MASON_HISTORY_DAYS; mason logs --requests)`);
|
|
634
728
|
{
|
|
635
729
|
// a user whose role the file no longer names reads nothing until it is added again (fail closed): said here, by name
|
|
@@ -656,6 +750,16 @@ async function run(o, hosted) {
|
|
|
656
750
|
}
|
|
657
751
|
}
|
|
658
752
|
// WHAT mason share OPENS (src/cli/share-run.ts): a second socket of this listener, on a run on a laptop alone
|
|
753
|
+
// a tunnel's ssh config a run or a mason share killed with -9 left behind (src/cli/tunnel.ts): removed unless its
|
|
754
|
+
// process lives, so a run stopped with the page's share open leaves nothing at the next start
|
|
755
|
+
if (o.serve === undefined) {
|
|
756
|
+
try {
|
|
757
|
+
sweepTunnelFiles(home, alive);
|
|
758
|
+
}
|
|
759
|
+
catch (e) {
|
|
760
|
+
logLine(`share: the tunnels' files left behind could not be swept: ${e.message}`);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
659
763
|
const shares = o.serve !== undefined ? undefined : new RunShares({
|
|
660
764
|
listener: api, database, log: logLine,
|
|
661
765
|
// the project's own certificate, as --self-signed-tls serves it: made once, and recorded as that flag records it
|
|
@@ -676,22 +780,22 @@ async function run(o, hosted) {
|
|
|
676
780
|
}
|
|
677
781
|
},
|
|
678
782
|
});
|
|
679
|
-
const setupPort = await freePort(o.port, chosen);
|
|
783
|
+
const setupPort = o.exactPort === true ? o.port : await freePort(o.port, chosen);
|
|
680
784
|
const token = newToken();
|
|
681
785
|
const runCtx = {
|
|
682
|
-
source, facts: () => facts, warnings,
|
|
786
|
+
source, facts: () => facts, warnings: [...warnings, ...(memoryWarned === null ? [] : [memoryProblemText(memoryWarned)])], textOrder,
|
|
683
787
|
// the slot and publication while a table's sync is replication (the run streams), as the sync says it now
|
|
684
788
|
replication: () => sync.streams() ? repl : null, port: setupPort, token, folder: () => folder, root: project.root, home,
|
|
685
789
|
version: o.version, startedAt,
|
|
686
|
-
// the web page (mason ui)
|
|
687
|
-
page: o.serve
|
|
790
|
+
// the web page (mason ui): on loopback alone, so mason serve's is reached through mason ui --ssh
|
|
791
|
+
page: true, served: o.serve !== undefined,
|
|
688
792
|
demo: cfg.demo === true,
|
|
689
793
|
users: { file: path.join(home, USERS_FILE), apiKey: cfg.apiKey, database, postgresUrl },
|
|
690
794
|
// the roles as the listener holds them: their row filters checked by its catalog (a filter Postgres refuses is a problem)
|
|
691
795
|
roles: () => api.roles(),
|
|
692
|
-
...(shares !== undefined ? { shares } : {}),
|
|
796
|
+
...(shares !== undefined ? { shares, pageShares: new PageShares({ shares, home, log: logLine }) } : {}),
|
|
693
797
|
// the sources file is the list of what is added: a change to it is read by the sync at once
|
|
694
|
-
sources: () => sources, sourcesChanged: () => { added(); }, readSource: readSources, runUrl: src.resolved,
|
|
798
|
+
sources: () => sources, sourcesChanged: () => { void added(); }, readSource: readSources, runUrl: src.resolved,
|
|
695
799
|
// a table set to sync: replication while the run is up, in a run that never checked the database for it: the
|
|
696
800
|
// start's own checks, now, and the edit refused when they fail (src/cli/run-server.ts /api/pick)
|
|
697
801
|
checkReplication: async (file) => {
|
|
@@ -717,32 +821,42 @@ async function run(o, hosted) {
|
|
|
717
821
|
// the statements running now, and a cancel of one: the listener's own (a CancelRequest's path)
|
|
718
822
|
listener: api,
|
|
719
823
|
duck: { copies: () => sync.state(), file: duckFile, poll: (sources) => sync.pollNow(sources), stream: (o) => sync.streamStatus(o), measured: (table) => sync.measured(table), disk: () => sync.diskStatus(), requests, events, usage,
|
|
720
|
-
restarts: () => readRestarts(home), jsonPaths: () => sync.kept.status() },
|
|
824
|
+
restarts: () => readRestarts(home), jsonPaths: () => sync.kept.status(), generated: () => sync.generated.status() },
|
|
721
825
|
};
|
|
722
826
|
const server = createRunServer(runCtx);
|
|
723
827
|
// a sample is /api/state's facts at its moment (src/cli/run-server.ts usageFacts): one computation of each
|
|
724
828
|
usage.start(() => usageFacts(runCtx));
|
|
725
|
-
await
|
|
829
|
+
await listenOnLoopback(server, setupPort);
|
|
726
830
|
const refreshFacts = setInterval(() => {
|
|
727
831
|
void (async () => {
|
|
728
832
|
const c = await connectSource(source);
|
|
729
833
|
if ("failure" in c)
|
|
730
834
|
return; // the commands keep the last catalog; the sync says loudly if the source is away
|
|
835
|
+
const before = facts.collations;
|
|
731
836
|
try {
|
|
732
|
-
facts = await readSourceFacts(c.pg);
|
|
837
|
+
facts = await readSourceFacts(c.pg, undefined, { collations: false });
|
|
733
838
|
}
|
|
734
839
|
finally {
|
|
735
840
|
c.pg.close();
|
|
736
841
|
}
|
|
842
|
+
if (before !== undefined)
|
|
843
|
+
facts.collations = before;
|
|
737
844
|
})().catch((e) => logLine(`catalog refresh failed: ${e.message}`));
|
|
738
845
|
}, 10_000);
|
|
739
|
-
added();
|
|
846
|
+
const tablesSet = added();
|
|
740
847
|
cfg.running = { pid: process.pid, controlUrl: `http://127.0.0.1:${setupPort}`, token, postgresUrl, postgresListener: pgTls.listener };
|
|
741
848
|
cfg.lastRun = new Date().toISOString();
|
|
742
849
|
writeConfig(home, cfg);
|
|
743
850
|
done(`Mason is up: ${sync.state().length} table(s) added, kept current: ${syncMode === "poll" ? `as each table's sync: says${syncCounts(sources.tables)}` +
|
|
744
851
|
`${sync.streams() ? `; replicated tables streamed as they change (logical replication: slot ${repl.slot}, publication ${repl.publication}${repl.owned ? "" : ", read as it is"})` : ""}`
|
|
745
852
|
: `each table read again whole every ${PASS_RETRY_MS / 1000} s (MASON_DUCK_SYNC=recopy)`}`);
|
|
853
|
+
// THE GENERATED TABLES (src/duck/generated.ts): the folder read and each file held to the catalog, what is refused said
|
|
854
|
+
// here too; a refusal stops nothing, and their SQL is checked again as the tables they read are copied
|
|
855
|
+
await tablesSet;
|
|
856
|
+
await sync.generated.start(() => readGenerated(folder, project.root), api.planner());
|
|
857
|
+
for (const g of sync.generated.status())
|
|
858
|
+
for (const r of g.refusals)
|
|
859
|
+
say(textEntry(" ! ", r.text).trimEnd());
|
|
746
860
|
// THE FILE'S DEFAULT, SAID ONCE (issue 33: nothing Mason does to your database is a hidden default)
|
|
747
861
|
const defaultLine = syncDefaultLine(sources.file, shownPath(project.root, sources.file.file), syncMode === "poll" ? copiedBefore : 0);
|
|
748
862
|
say(` ${defaultLine}`);
|
|
@@ -792,6 +906,13 @@ async function run(o, hosted) {
|
|
|
792
906
|
catch (e) {
|
|
793
907
|
say(` closing the share: ${e.message}`);
|
|
794
908
|
}
|
|
909
|
+
// before the listener: a generated table's review plans on its catalog, so it ends first
|
|
910
|
+
try {
|
|
911
|
+
await sync.generated.stop();
|
|
912
|
+
}
|
|
913
|
+
catch (e) {
|
|
914
|
+
say(` stopping the generated tables: ${e.message}`);
|
|
915
|
+
}
|
|
795
916
|
try {
|
|
796
917
|
await api.close();
|
|
797
918
|
}
|
package/dist/src/cli/doctor.js
CHANGED
|
@@ -6,15 +6,18 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { failed, jsonText, nextBlock, parseCommand } from "./command.js";
|
|
8
8
|
import { out as styleOut, wrap } from "./style.js";
|
|
9
|
-
import { heldPgPort, namedPgPort, portHolder } from "./ports.js";
|
|
9
|
+
import { DEFAULT_UI_PORT, heldPgPort, namedPgPort, portHolder } from "./ports.js";
|
|
10
10
|
import { callRun, RunUnreachable } from "./control.js";
|
|
11
11
|
import { readiness } from "./commands.js";
|
|
12
12
|
import { connectSource, maskedUrl, parseSource, readSourceFacts, versionProblem } from "./preflight.js";
|
|
13
13
|
import { theRun } from "./runs.js";
|
|
14
14
|
import { projectSource } from "./project.js";
|
|
15
15
|
import { DEFAULT_PG_PORT } from "../api/postgres/listener.js";
|
|
16
|
-
import { defaultMemoryLimit,
|
|
16
|
+
import { defaultMemoryLimit, machineMemory, memoryLimitBytes, memoryProblems, memoryProblemText, memorySize, memoryWarning, OUTSIDE_RESERVE_BYTES } from "../duck/store.js";
|
|
17
17
|
import { probeExtensions } from "../duck/extensions.js";
|
|
18
|
+
import { readRoleFacts, roleChecks, roleUnknown, ROLE_CODES } from "./source-role.js";
|
|
19
|
+
import { replicatedEntries, shownPath } from "./sources.js";
|
|
20
|
+
import { namedPublication } from "./replication-mode.js";
|
|
18
21
|
const out = (m) => { process.stdout.write(`${m}\n`); };
|
|
19
22
|
/**
|
|
20
23
|
* THE NODE THE PACKAGE NEEDS, major.minor: cli/package.json's `engines`, this check, `mason --help` and the README read
|
|
@@ -26,19 +29,31 @@ const out = (m) => { process.stdout.write(`${m}\n`); };
|
|
|
26
29
|
export const NODE_MIN = "22.13";
|
|
27
30
|
export const DOCTOR_USAGE = `usage: mason doctor [--json]
|
|
28
31
|
|
|
29
|
-
Says whether this machine is ready for Mason — Node ${NODE_MIN} or later, memory (the engine
|
|
30
|
-
|
|
32
|
+
Says whether this machine is ready for Mason — Node ${NODE_MIN} or later, memory (what the engine may use and what it
|
|
33
|
+
leaves beside it; MASON_DUCKDB_MEMORY=4GiB sets it), Mason's postgres and inet extensions (installed with Mason for this
|
|
31
34
|
platform, so nothing is downloaded; else the first sync downloads them), the ports Mason listens on — with the way
|
|
32
|
-
out for anything missing.
|
|
33
|
-
project's folder it checks the project's database too; with Mason running for it, the run in a few lines instead: its
|
|
34
|
-
database, whether every added table is synced, the statements of the last day it refused or answered slowly.
|
|
35
|
+
out for anything missing.
|
|
35
36
|
|
|
36
37
|
mason doctor
|
|
37
38
|
|
|
38
|
-
--json as data:
|
|
39
|
-
or null when it could not tell
|
|
39
|
+
--json as data: its shape is at the end
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
In a project's folder it checks the project's database too; with Mason running for it, the run in a few lines instead: its
|
|
42
|
+
database, whether every added table is synced, the statements of the last day it refused or answered slowly. Both
|
|
43
|
+
say whether the user Mason connects as could change a synced table's rows (a superuser, BYPASSRLS, an owner of a
|
|
44
|
+
synced table, a write privilege on one or CREATE on its schema; or a write that reaches its rows through a
|
|
45
|
+
partition, an inheritance child or parent, a foreign key's action, a view or a rule; SECURITY DEFINER code it
|
|
46
|
+
reaches; a foreign server; ADMIN OPTION or a server role; directly or through a role it is a member of), with the
|
|
47
|
+
statements that make a role that only reads.
|
|
48
|
+
|
|
49
|
+
Exit status: 0 nothing failed — a warning (!) and a check that could not tell (?) are said, and leave it 0; 1
|
|
50
|
+
something failed (said, with the way out).
|
|
51
|
+
|
|
52
|
+
The --json answer: { ready, verdict, checks: [{ name, ok, detail, warn, code, fix }], next: [{ command, what }] }; ok
|
|
53
|
+
is true, false, or null when it could not tell; warn (only when true): fine to run, but said loudly; code
|
|
54
|
+
(the role's lines): ${ROLE_CODES.slice(0, 5).join(", ")},
|
|
55
|
+
${ROLE_CODES.slice(5).join(", ")};
|
|
56
|
+
fix (with code): the statements and the edit that make a role that only reads
|
|
42
57
|
`;
|
|
43
58
|
export const DOCTOR = {
|
|
44
59
|
name: "doctor",
|
|
@@ -48,7 +63,7 @@ export const DOCTOR = {
|
|
|
48
63
|
json: ["ready", "verdict", "checks", "next"],
|
|
49
64
|
};
|
|
50
65
|
/** The ports a mason start takes by default, and what listens on each. */
|
|
51
|
-
export const PORTS = [[
|
|
66
|
+
export const PORTS = [[DEFAULT_UI_PORT, "the mason commands"], [DEFAULT_PG_PORT, "the Postgres API"]];
|
|
52
67
|
/** Node's version against the package's floor, major then minor. */
|
|
53
68
|
export function nodeCheck(version = process.versions.node) {
|
|
54
69
|
const [major, minor] = version.split(".").map(Number);
|
|
@@ -57,19 +72,30 @@ export function nodeCheck(version = process.versions.node) {
|
|
|
57
72
|
? { name: "node", ok: true, detail: `Node ${version}` }
|
|
58
73
|
: { name: "node", ok: false, detail: `Node ${version} is too old: mason needs ${NODE_MIN} or later (nodejs.org, or nvm install ${minMajor})` };
|
|
59
74
|
}
|
|
60
|
-
/**
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
75
|
+
/**
|
|
76
|
+
* The machine's memory and what the copy's engine may use of it (src/duck/store.ts DEFAULT_MEMORY_LIMIT,
|
|
77
|
+
* MASON_DUCKDB_MEMORY): what a start refuses is said in the start's own words (`memoryProblems`), and what it warns of
|
|
78
|
+
* under the floor in the run's own words (`memoryWarning`), a warning, never a failure, since the start goes on; so is
|
|
79
|
+
* a share the operator set that leaves less than OUTSIDE_RESERVE_BYTES beside it.
|
|
80
|
+
*/
|
|
81
|
+
export function memoryCheck(memory = machineMemory(), env = process.env) {
|
|
82
|
+
const problems = memoryProblems(env);
|
|
83
|
+
if (problems.length)
|
|
84
|
+
return { name: "memory", ok: false, detail: problems.map(memoryProblemText).join("; ") };
|
|
85
|
+
const warned = memoryWarning(memory, env);
|
|
86
|
+
const set = env.MASON_DUCKDB_MEMORY || undefined;
|
|
87
|
+
const share = set ?? defaultMemoryLimit(memory.bytes);
|
|
88
|
+
const outside = memory.bytes - memoryLimitBytes(share);
|
|
89
|
+
if (warned !== null) {
|
|
90
|
+
return { name: "memory", ok: true, warn: true,
|
|
91
|
+
detail: `${memoryProblemText(warned)} (the engine uses up to ${share.trim()}, the default, leaving ${memorySize(Math.max(0, outside))} beside it)` };
|
|
66
92
|
}
|
|
67
|
-
const share = set ?? defaultMemoryLimit(totalBytes);
|
|
68
|
-
const low = totalBytes < 2 * 1024 ** 3;
|
|
69
93
|
return {
|
|
70
|
-
name: "memory", ok:
|
|
71
|
-
detail: `${
|
|
72
|
-
(
|
|
94
|
+
name: "memory", ok: true, ...(outside < OUTSIDE_RESERVE_BYTES ? { warn: true } : {}),
|
|
95
|
+
detail: `${memorySize(memory.bytes)}; the engine uses up to ${share.trim()}${set !== undefined ? " (MASON_DUCKDB_MEMORY)" : " (the default; MASON_DUCKDB_MEMORY sets it)"}, ` +
|
|
96
|
+
`leaving ${memorySize(Math.max(0, outside))} for Mason beside it` +
|
|
97
|
+
(outside < OUTSIDE_RESERVE_BYTES ? `, under the ${memorySize(OUTSIDE_RESERVE_BYTES)} it needs (measured, with a margin): the system may stop it in a first sync, ` +
|
|
98
|
+
"a generated table's build or a burst of writes (a smaller MASON_DUCKDB_MEMORY, or more memory)" : ""),
|
|
73
99
|
};
|
|
74
100
|
}
|
|
75
101
|
/** The copy's extensions (src/duck/extensions.ts): shipped with Mason and loaded by an engine of their own as the run's
|
|
@@ -119,25 +145,72 @@ function portOf(url) {
|
|
|
119
145
|
return null;
|
|
120
146
|
} // not a URL: no port to compare with
|
|
121
147
|
}
|
|
122
|
-
/**
|
|
123
|
-
|
|
124
|
-
|
|
148
|
+
/**
|
|
149
|
+
* The database a URL names: reachable, its version, what this user can read — and, given `role` (the project's synced
|
|
150
|
+
* tables), whether that user could write to it (src/cli/source-role.ts), on the same connection. `name` says where the
|
|
151
|
+
* URL is (`DATABASE_URL in .env`): a refusal names it, never the value (src/cli/preflight.ts shownUrl).
|
|
152
|
+
*/
|
|
153
|
+
export async function databaseChecks(url, o = {}) {
|
|
154
|
+
const parsed = parseSource(url, o.name);
|
|
125
155
|
if ("problem" in parsed)
|
|
126
|
-
return { name: "database", ok: false, detail: parsed.problem };
|
|
156
|
+
return [{ name: "database", ok: false, detail: parsed.problem }];
|
|
127
157
|
const connected = await connectSource(parsed.source, o);
|
|
128
158
|
if ("failure" in connected)
|
|
129
|
-
return { name: "database", ok: false, detail: `${maskedUrl(parsed.source.url)}: ${connected.failure.message}${connected.failure.hints.map((h) => `\n - ${h}`).join("")}` };
|
|
159
|
+
return [{ name: "database", ok: false, detail: `${maskedUrl(parsed.source.url)}: ${connected.failure.message}${connected.failure.hints.map((h) => `\n - ${h}`).join("")}` }];
|
|
160
|
+
try {
|
|
161
|
+
const facts = await readSourceFacts(connected.pg);
|
|
162
|
+
const tooOld = versionProblem(facts);
|
|
163
|
+
if (tooOld)
|
|
164
|
+
return [{ name: "database", ok: false, detail: `${maskedUrl(parsed.source.url)}: ${tooOld}` }];
|
|
165
|
+
const database = { name: "database", ok: true, detail: `${maskedUrl(parsed.source.url)}: Postgres ${facts.versionText}${facts.standby ? " (a read replica)" : ""}, ${facts.tables.length} table(s) this user can read` };
|
|
166
|
+
return o.role ? [database, ...await askRole(connected.pg, o.role)] : [database];
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
connected.pg.close();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/** The role's lines, from its one catalog read; a read that failed is "could not tell", never "only reads". */
|
|
173
|
+
async function askRole(pg, ask) {
|
|
130
174
|
let facts;
|
|
131
175
|
try {
|
|
132
|
-
facts = await
|
|
176
|
+
facts = await readRoleFacts(pg, ask.tables);
|
|
177
|
+
}
|
|
178
|
+
catch (e) {
|
|
179
|
+
return [roleUnknown(e.message)];
|
|
180
|
+
}
|
|
181
|
+
return roleChecks(facts, ask);
|
|
182
|
+
}
|
|
183
|
+
/** What the role check asks of a project's sources file: its tables, whether one streams, its publication, its URL. */
|
|
184
|
+
export function roleAsk(src, root, env = process.env) {
|
|
185
|
+
return {
|
|
186
|
+
tables: src.tables.map((e) => { const at = e.table.indexOf("."); return { schema: e.table.slice(0, at), table: e.table.slice(at + 1) }; }),
|
|
187
|
+
replicated: replicatedEntries(src).map((e) => e.table),
|
|
188
|
+
publication: namedPublication(src, env)?.name ?? null,
|
|
189
|
+
urlVariable: "env" in src.url ? src.url.env : null, urlFrom: src.from, file: shownPath(root, src.file), url: src.resolved,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* The role's lines with Mason running (its own checks never connect to the database): the project's sources file read
|
|
194
|
+
* and the database asked, on a connection of its own. What cannot be read is "could not tell".
|
|
195
|
+
*/
|
|
196
|
+
export async function runRoleChecks(project) {
|
|
197
|
+
if (project.config.demo === true)
|
|
198
|
+
return []; // the demo database is Mason's own, inside the run
|
|
199
|
+
const src = projectSource(project);
|
|
200
|
+
if ("message" in src)
|
|
201
|
+
return [roleUnknown(`${src.message} (mason status)`)];
|
|
202
|
+
const parsed = parseSource(src.resolved);
|
|
203
|
+
if ("problem" in parsed)
|
|
204
|
+
return [roleUnknown(parsed.problem)];
|
|
205
|
+
const connected = await connectSource(parsed.source);
|
|
206
|
+
if ("failure" in connected)
|
|
207
|
+
return [roleUnknown(`${maskedUrl(parsed.source.url)}: ${connected.failure.message}`)];
|
|
208
|
+
try {
|
|
209
|
+
return await askRole(connected.pg, roleAsk(src, project.root));
|
|
133
210
|
}
|
|
134
211
|
finally {
|
|
135
212
|
connected.pg.close();
|
|
136
213
|
}
|
|
137
|
-
const tooOld = versionProblem(facts);
|
|
138
|
-
if (tooOld)
|
|
139
|
-
return { name: "database", ok: false, detail: `${maskedUrl(parsed.source.url)}: ${tooOld}` };
|
|
140
|
-
return { name: "database", ok: true, detail: `${maskedUrl(parsed.source.url)}: Postgres ${facts.versionText}${facts.standby ? " (a read replica)" : ""}, ${facts.tables.length} table(s) this user can read` };
|
|
141
214
|
}
|
|
142
215
|
/** The run's health, from its own state: every table copied, failing, or still at work; and what it refused or answered slowly. */
|
|
143
216
|
export async function runChecks(run) {
|
|
@@ -185,7 +258,7 @@ export function nextSteps(checks, run, project) {
|
|
|
185
258
|
if (project === null)
|
|
186
259
|
return [
|
|
187
260
|
["mason init", "in your app's folder: the database from MASON_DATABASE_URL or DATABASE_URL in .env.local, .env or the shell"],
|
|
188
|
-
["mason init
|
|
261
|
+
["mason init - < db-url.txt", "or its URL on stdin"],
|
|
189
262
|
];
|
|
190
263
|
if (run === null)
|
|
191
264
|
return [["mason start", "Mason in the background, for this folder"]];
|
|
@@ -214,7 +287,7 @@ export function verdictOf(checks, run, project) {
|
|
|
214
287
|
}
|
|
215
288
|
/** A check's mark: ok, !! a failure, ? could not tell. */
|
|
216
289
|
function markOf(c, s) {
|
|
217
|
-
return c.ok === false ? s.red("!! ") : c.ok === null ? s.yellow("? ") : s.green("ok ");
|
|
290
|
+
return c.ok === false ? s.red("!! ") : c.ok === null ? s.yellow("? ") : c.warn === true ? s.yellow("! ") : s.green("ok ");
|
|
218
291
|
}
|
|
219
292
|
export async function doctor(argv, deps = {}) {
|
|
220
293
|
const f = parseCommand(DOCTOR, argv);
|
|
@@ -236,8 +309,13 @@ export async function doctor(argv, deps = {}) {
|
|
|
236
309
|
if (project) {
|
|
237
310
|
// the database the project's sources file names (src/cli/sources.ts): a file that cannot be read is the failing line
|
|
238
311
|
const src = projectSource(project);
|
|
239
|
-
|
|
240
|
-
:
|
|
312
|
+
if ("message" in src)
|
|
313
|
+
checks.push({ name: "database", ok: false, detail: `${src.message}${src.why ? `: ${src.why.replace(/\n/g, "; ")}` : ""}. ${src.fix?.split("\n")[0] ?? ""}`.trim() });
|
|
314
|
+
// the demo database is Mason's own, inside the run: whom it connects as is not the person's to change
|
|
315
|
+
else if (project.config.demo === true)
|
|
316
|
+
checks.push(...await databaseChecks(src.resolved, { demo: true, name: src.from }));
|
|
317
|
+
else
|
|
318
|
+
checks.push(...await databaseChecks(src.resolved, { role: roleAsk(src, project.root), name: src.from }));
|
|
241
319
|
}
|
|
242
320
|
// a project here whose config cannot be read: init would refuse, so "Ready: mason init" would send a person nowhere
|
|
243
321
|
if ("code" in which && which.code === "bad_config")
|
|
@@ -245,6 +323,8 @@ export async function doctor(argv, deps = {}) {
|
|
|
245
323
|
}
|
|
246
324
|
else {
|
|
247
325
|
checks.push(...await runChecks(run));
|
|
326
|
+
if (project)
|
|
327
|
+
checks.push(...await runRoleChecks(project));
|
|
248
328
|
}
|
|
249
329
|
const next = nextSteps(checks, run, project);
|
|
250
330
|
const { ready, verdict } = verdictOf(checks, run, project);
|
|
@@ -262,6 +342,12 @@ export async function doctor(argv, deps = {}) {
|
|
|
262
342
|
const mark = markOf(c, s);
|
|
263
343
|
out(` ${mark} ${c.name.padEnd(w)} ${wrap(c.detail, s.width, 2 + 3 + 1 + w + 2)}`);
|
|
264
344
|
}
|
|
345
|
+
// a way out several lines share, once, under them: SQL as what you type, the rest as its words
|
|
346
|
+
for (const fix of new Set(checks.map((c) => c.fix).filter((x) => x !== undefined))) {
|
|
347
|
+
out("");
|
|
348
|
+
for (const line of fix.split("\n"))
|
|
349
|
+
out(` ${/;(\s+--.*)?$/.test(line) ? s.cyan(line) : line.startsWith("--") ? s.dim(line) : wrap(line, s.width, 4)}`);
|
|
350
|
+
}
|
|
265
351
|
if (run !== null)
|
|
266
352
|
out(s.dim(` its synced data and its state in ${run.home}. mason status for the details`));
|
|
267
353
|
if (run === null || !ready)
|