@mason-data/cli 0.6.2 → 0.8.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 +147 -69
- package/dist/src/api/error.js +3 -0
- package/dist/src/api/postgres/catalog.js +258 -14
- package/dist/src/api/postgres/encode.js +3 -1
- package/dist/src/api/postgres/explain.js +3 -2
- package/dist/src/api/postgres/protocol.js +39 -23
- package/dist/src/api/postgres/server.js +1290 -223
- package/dist/src/api/postgres/translate.js +81 -9
- package/dist/src/api/request-log.js +18 -5
- package/dist/src/cdc/pg-wire.js +144 -38
- package/dist/src/cdc/poll.js +5 -5
- package/dist/src/cli/background.js +55 -8
- package/dist/src/cli/cancel.js +75 -0
- package/dist/src/cli/command.js +5 -3
- package/dist/src/cli/commands.js +843 -272
- package/dist/src/cli/control.js +59 -0
- package/dist/src/cli/demo.js +158 -0
- package/dist/src/cli/dev.js +458 -125
- package/dist/src/cli/doctor.js +25 -12
- package/dist/src/cli/guides.js +103 -31
- package/dist/src/cli/init.js +159 -58
- package/dist/src/cli/machine.js +18 -12
- package/dist/src/cli/main.js +5 -5
- package/dist/src/cli/mcp.js +735 -0
- package/dist/src/cli/metrics.js +79 -0
- package/dist/src/cli/pg-tls.js +13 -3
- package/dist/src/cli/pg-url.js +4 -0
- package/dist/src/cli/pin.js +13 -12
- package/dist/src/cli/preflight.js +24 -14
- package/dist/src/cli/project.js +48 -7
- package/dist/src/cli/readiness.js +38 -4
- package/dist/src/cli/registry.js +57 -35
- 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 +584 -0
- package/dist/src/cli/run-server.js +498 -133
- package/dist/src/cli/runs.js +1 -1
- package/dist/src/cli/security.js +10 -8
- package/dist/src/cli/serve.js +147 -40
- package/dist/src/cli/share-run.js +286 -0
- package/dist/src/cli/share.js +286 -0
- package/dist/src/cli/source-edit.js +168 -0
- package/dist/src/cli/sources.js +290 -39
- package/dist/src/cli/supervise.js +12 -1
- package/dist/src/cli/sync.js +55 -25
- package/dist/src/cli/tunnel.js +200 -0
- package/dist/src/cli/ui.js +9 -6
- package/dist/src/cli/usage.js +128 -0
- package/dist/src/cli/user.js +496 -40
- package/dist/src/cli/version.js +9 -7
- package/dist/src/demo/seed.js +240 -0
- package/dist/src/demo/source.js +655 -0
- package/dist/src/duck/cancel.js +10 -1
- package/dist/src/duck/distinct-key.js +228 -0
- package/dist/src/duck/events.js +134 -0
- package/dist/src/duck/executor-worker.js +25 -11
- package/dist/src/duck/executor.js +107 -53
- package/dist/src/duck/explain.js +158 -29
- package/dist/src/duck/extensions.js +160 -0
- package/dist/src/duck/functions.js +73 -19
- package/dist/src/duck/gaps.js +25 -11
- package/dist/src/duck/grants.js +176 -0
- package/dist/src/duck/history.js +123 -0
- package/dist/src/duck/jsonb-order.js +175 -0
- package/dist/src/duck/jsonb.js +10 -8
- package/dist/src/duck/kept-route.js +82 -2
- package/dist/src/duck/kept.js +211 -44
- package/dist/src/duck/pgtree.js +40 -0
- package/dist/src/duck/plan.js +120 -30
- package/dist/src/duck/publication.js +6 -6
- package/dist/src/duck/requests.js +265 -93
- package/dist/src/duck/rewrite.js +1193 -73
- package/dist/src/duck/row-filter.js +649 -0
- package/dist/src/duck/store.js +271 -159
- package/dist/src/duck/stream.js +10 -5
- package/dist/src/duck/sync.js +907 -275
- package/dist/src/duck/types.js +6 -5
- package/dist/src/duck/usage.js +324 -0
- package/dist/src/duck/users.js +64 -15
- package/dist/src/duck/verified.js +9 -0
- package/dist/src/duck/views.js +2 -3
- package/dist/src/lib/database-name.js +62 -0
- package/dist/src/lib/dotenv.js +17 -4
- package/dist/src/lib/log-buffer.js +68 -0
- package/dist/src/lib/pg-numeric.js +5 -2
- package/dist/src/lib/project-version.js +23 -23
- package/dist/src/lib/table-sync.js +267 -0
- package/dist/src/lib/timezone.js +3 -2
- package/dist/src/web/app.css +706 -55
- package/dist/src/web/app.js +330 -258
- package/dist/src/web/charts.js +229 -0
- package/dist/src/web/index.html +84 -15
- package/dist/src/web/pages/queries.js +192 -0
- package/dist/src/web/pages/status.js +446 -0
- package/dist/src/web/pages/syncs.js +155 -0
- package/dist/src/web/pages/tables.js +434 -0
- package/dist/src/web/pages/usage.js +397 -0
- package/dist/src/web/pages/users.js +218 -0
- package/dist/src/web/shared.js +568 -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 +10 -3
package/dist/src/cli/doctor.js
CHANGED
|
@@ -14,6 +14,7 @@ import { theRun } from "./runs.js";
|
|
|
14
14
|
import { projectSource } from "./project.js";
|
|
15
15
|
import { DEFAULT_PG_PORT } from "../api/postgres/listener.js";
|
|
16
16
|
import { defaultMemoryLimit, machineMemoryBytes, MEMORY_LIMIT_RE } from "../duck/store.js";
|
|
17
|
+
import { probeExtensions } from "../duck/extensions.js";
|
|
17
18
|
const out = (m) => { process.stdout.write(`${m}\n`); };
|
|
18
19
|
/**
|
|
19
20
|
* THE NODE THE PACKAGE NEEDS, major.minor: cli/package.json's `engines`, this check, `mason --help` and the README read
|
|
@@ -25,10 +26,12 @@ const out = (m) => { process.stdout.write(`${m}\n`); };
|
|
|
25
26
|
export const NODE_MIN = "22.13";
|
|
26
27
|
export const DOCTOR_USAGE = `usage: mason doctor [--json]
|
|
27
28
|
|
|
28
|
-
Says whether this machine is ready for Mason — Node ${NODE_MIN} or later, memory (
|
|
29
|
-
default; MASON_DUCKDB_MEMORY=4GiB sets it),
|
|
29
|
+
Says whether this machine is ready for Mason — Node ${NODE_MIN} or later, memory (the engine uses up to half of it by
|
|
30
|
+
default; MASON_DUCKDB_MEMORY=4GiB sets it), Mason's postgres and inet extensions (installed with Mason for this
|
|
31
|
+
platform, so nothing is downloaded; else the first sync downloads them), the ports Mason listens on — with the way
|
|
32
|
+
out for anything missing. In a
|
|
30
33
|
project's folder it checks the project's database too; with Mason running for it, the run in a few lines instead: its
|
|
31
|
-
database, whether every added table is
|
|
34
|
+
database, whether every added table is synced, the statements of the last day it refused or answered slowly.
|
|
32
35
|
|
|
33
36
|
mason doctor
|
|
34
37
|
|
|
@@ -65,10 +68,16 @@ export function memoryCheck(totalBytes = machineMemoryBytes(), env = process.env
|
|
|
65
68
|
const low = totalBytes < 2 * 1024 ** 3;
|
|
66
69
|
return {
|
|
67
70
|
name: "memory", ok: !low,
|
|
68
|
-
detail: `${gib} GiB;
|
|
69
|
-
(low ? ". Under 2 GiB, a large table's
|
|
71
|
+
detail: `${gib} GiB; the engine uses up to ${share.trim()}${set !== undefined ? " (MASON_DUCKDB_MEMORY)" : " (half, the default; MASON_DUCKDB_MEMORY sets it)"}` +
|
|
72
|
+
(low ? ". Under 2 GiB, a large table's first sync and a query starve each other: expect statements refused for memory" : ""),
|
|
70
73
|
};
|
|
71
74
|
}
|
|
75
|
+
/** The copy's extensions (src/duck/extensions.ts): shipped with Mason and loaded by an engine of their own as the run's
|
|
76
|
+
* will load them, or not shipped, and why. `probe` is a test's. */
|
|
77
|
+
export async function extensionsCheck(probe = () => probeExtensions()) {
|
|
78
|
+
const p = await probe();
|
|
79
|
+
return { name: "extensions", ok: p.ok, detail: p.detail };
|
|
80
|
+
}
|
|
72
81
|
/**
|
|
73
82
|
* Mason's ports in one line, decided by the probe mason start uses (`portHolder`, src/cli/ports.ts: every address, so
|
|
74
83
|
* doctor and start agree on a port another program holds on the wildcard). A taken command port only moves Mason to
|
|
@@ -111,11 +120,11 @@ function portOf(url) {
|
|
|
111
120
|
} // not a URL: no port to compare with
|
|
112
121
|
}
|
|
113
122
|
/** The database a URL names: reachable, its version, what this user can read. */
|
|
114
|
-
export async function databaseCheck(url) {
|
|
123
|
+
export async function databaseCheck(url, o = {}) {
|
|
115
124
|
const parsed = parseSource(url);
|
|
116
125
|
if ("problem" in parsed)
|
|
117
126
|
return { name: "database", ok: false, detail: parsed.problem };
|
|
118
|
-
const connected = await connectSource(parsed.source);
|
|
127
|
+
const connected = await connectSource(parsed.source, o);
|
|
119
128
|
if ("failure" in connected)
|
|
120
129
|
return { name: "database", ok: false, detail: `${maskedUrl(parsed.source.url)}: ${connected.failure.message}${connected.failure.hints.map((h) => `\n - ${h}`).join("")}` };
|
|
121
130
|
let facts;
|
|
@@ -153,7 +162,7 @@ export async function runChecks(run) {
|
|
|
153
162
|
else if (ready.waiting.length)
|
|
154
163
|
checks.push({ name: "project", ok: null, detail: `still at work: ${ready.waiting.join("; ")} (mason wait)` });
|
|
155
164
|
else
|
|
156
|
-
checks.push({ name: "project", ok: true, detail: picked ? `${picked} table(s)
|
|
165
|
+
checks.push({ name: "project", ok: true, detail: picked ? `${picked} table(s) synced and kept current` : "no table added yet (mason tables, then mason add <table>)" });
|
|
157
166
|
const slow = await callRun(run, "GET", "/api/slow"), refused = await callRun(run, "GET", "/api/refused");
|
|
158
167
|
const slowN = slow.status === 200 ? slow.body.slow.length : null, refusedN = refused.status === 200 ? refused.body.refused.length : null;
|
|
159
168
|
checks.push(slowN === null || refusedN === null
|
|
@@ -168,11 +177,14 @@ export function nextSteps(checks, run, project) {
|
|
|
168
177
|
return [["mason doctor", `again, once Node ${NODE_MIN} or later is installed`]];
|
|
169
178
|
if (bad("project") && run === null)
|
|
170
179
|
return [["mason doctor", "again, once .mason/config.json is repaired or moved aside (the detail above says how)"]];
|
|
180
|
+
// the demo database is hosted by the run itself (src/cli/demo.ts): down until mason start makes it
|
|
181
|
+
if (bad("database") && project?.config.demo === true)
|
|
182
|
+
return [["mason start", "hosts the demo database, then syncs its tables"]];
|
|
171
183
|
if (bad("database"))
|
|
172
184
|
return [["mason start", `once the connection works (the detail above says how; the URL is in ${project ? `${project.dir}/config.json` : ".mason/config.json"})`]];
|
|
173
185
|
if (project === null)
|
|
174
186
|
return [
|
|
175
|
-
["mason init", "in your app's folder: the database from DATABASE_URL in .env.local, .env or the shell"],
|
|
187
|
+
["mason init", "in your app's folder: the database from MASON_DATABASE_URL or DATABASE_URL in .env.local, .env or the shell"],
|
|
176
188
|
["mason init postgres://user:password@localhost:5432/your_app", "or name it"],
|
|
177
189
|
];
|
|
178
190
|
if (run === null)
|
|
@@ -182,7 +194,7 @@ export function nextSteps(checks, run, project) {
|
|
|
182
194
|
if (bad("project"))
|
|
183
195
|
return [["mason wait", "each problem, with the way out"]];
|
|
184
196
|
if (checks.some((c) => c.name === "project" && /no table added/.test(c.detail)))
|
|
185
|
-
return [["mason tables", "the database's tables and their columns"], ["mason add <table>", "
|
|
197
|
+
return [["mason tables", "the database's tables and their columns"], ["mason add <table>", "sync a table into Mason"]];
|
|
186
198
|
if (bad("queries"))
|
|
187
199
|
return [["mason status", "the refused and slow statements, each with what to do"]];
|
|
188
200
|
return [["mason user add <name>", "a user for a BI tool or your app: its password and URL, printed once"]];
|
|
@@ -219,12 +231,13 @@ export async function doctor(argv, deps = {}) {
|
|
|
219
231
|
if (run === null) {
|
|
220
232
|
checks.push(nodeCheck());
|
|
221
233
|
checks.push(memoryCheck());
|
|
234
|
+
checks.push(await extensionsCheck());
|
|
222
235
|
checks.push(await portsCheck([], portHolder, process.env, project?.config.pgPort ?? null));
|
|
223
236
|
if (project) {
|
|
224
237
|
// the database the project's sources file names (src/cli/sources.ts): a file that cannot be read is the failing line
|
|
225
238
|
const src = projectSource(project);
|
|
226
239
|
checks.push("message" in src ? { name: "database", ok: false, detail: `${src.message}${src.why ? `: ${src.why.replace(/\n/g, "; ")}` : ""}. ${src.fix?.split("\n")[0] ?? ""}`.trim() }
|
|
227
|
-
: await databaseCheck(src.resolved));
|
|
240
|
+
: await databaseCheck(src.resolved, { demo: project.config.demo === true }));
|
|
228
241
|
}
|
|
229
242
|
// a project here whose config cannot be read: init would refuse, so "Ready: mason init" would send a person nowhere
|
|
230
243
|
if ("code" in which && which.code === "bad_config")
|
|
@@ -250,7 +263,7 @@ export async function doctor(argv, deps = {}) {
|
|
|
250
263
|
out(` ${mark} ${c.name.padEnd(w)} ${wrap(c.detail, s.width, 2 + 3 + 1 + w + 2)}`);
|
|
251
264
|
}
|
|
252
265
|
if (run !== null)
|
|
253
|
-
out(s.dim(` its
|
|
266
|
+
out(s.dim(` its synced data and its state in ${run.home}. mason status for the details`));
|
|
254
267
|
if (run === null || !ready)
|
|
255
268
|
out(nextBlock(next));
|
|
256
269
|
return ready ? 0 : 1;
|
package/dist/src/cli/guides.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* A guide's name is never a command's (tests/cli-help.test.ts): one word never means two pages.
|
|
9
9
|
*/
|
|
10
|
+
import { DEMO_ADDED_TABLES, DEMO_TABLES, DEMO_TABLE_TO_ADD } from "../demo/seed.js";
|
|
10
11
|
/** The guides, by the name `mason help <guide>` takes. */
|
|
11
12
|
export const GUIDES = {
|
|
12
13
|
agent: "driving Mason from a coding agent: the commands in order, --json and its shapes, exit codes, failures",
|
|
@@ -29,22 +30,31 @@ async function agentGuide() {
|
|
|
29
30
|
// imported here, not at the top: the registry imports this file for the HELP usage
|
|
30
31
|
const { STEPS } = await import("./registry.js");
|
|
31
32
|
const { FAILURE_CODES } = await import("./command.js");
|
|
32
|
-
const noJson = new Set(["help"]);
|
|
33
33
|
const w = Math.max(...STEPS.flatMap(([, cs]) => cs.map((c) => c.name.length))) + "mason --json".length;
|
|
34
|
-
const commands = STEPS.map(([step, cs]) => ` ${step}\n${cs.map((c) => ` ${`mason ${c.name}${
|
|
34
|
+
const commands = STEPS.map(([step, cs]) => ` ${step}\n${cs.map((c) => ` ${`mason ${c.name}${Object.hasOwn(c.options, "json") ? " --json" : ""}`.padEnd(w)} ${c.summary}`).join("\n")}`).join("\n");
|
|
35
35
|
const codes = Object.entries(FAILURE_CODES).map(([code, what]) => ` ${code.padEnd(12)} ${what}`).join("\n");
|
|
36
36
|
return `DRIVING MASON FROM A CODING AGENT — the same commands a person runs, answered as data
|
|
37
37
|
|
|
38
38
|
Every command prints for a person, or with --json one JSON object for a program. One prints text alone: mason help
|
|
39
|
-
(this reference); mason
|
|
39
|
+
(this reference); mason mcp's stdout is the MCP protocol's, so it takes no --json and says a failure to start on
|
|
40
|
+
stderr; mason logs --json is the log's entries ({ at, text }), or with --requests the request log, --events the sync's
|
|
41
|
+
events, --running the statements running now. Nothing a command prints
|
|
40
42
|
refers to a file this package does not ship.
|
|
41
43
|
|
|
44
|
+
AN AGENT WITH MCP (Claude Code, Claude Desktop, any MCP client) adds Mason as an MCP server first, then asks with
|
|
45
|
+
its tools rather than through a shell: tables (each synced table's rows and how current it is), describe, query (rows with
|
|
46
|
+
their Postgres types, or the refusal with what to write instead) and explain. In the project's folder, Mason running:
|
|
47
|
+
mason user add claude --json its url, printed once
|
|
48
|
+
claude mcp add mason --env MASON_URL="<url>" -- mason mcp
|
|
49
|
+
(the server's name before --env, which takes every value after it). mason help mcp says the rest: Claude Desktop's
|
|
50
|
+
config, a mason serve elsewhere.
|
|
51
|
+
|
|
42
52
|
THE COMMANDS, in the order a project takes them (mason --help lists the same):
|
|
43
53
|
${commands}
|
|
44
54
|
|
|
45
55
|
EVERY ANSWER is an object with next: [{ command, what }] — the commands to run next, the same a person reads under
|
|
46
56
|
Next. The other keys are each command's; its help names them (mason help <command>, the --json line). Lists are wrapped
|
|
47
|
-
({ tables: […], next }), never bare. mason wait --json blocks until every added table is
|
|
57
|
+
({ tables: […], next }), never bare. mason wait --json blocks until every added table is synced and kept current, then
|
|
48
58
|
answers once (exit 0), or with error beside the state (exit 1 failed, 124 timed out: nothing stopped, run it again).
|
|
49
59
|
|
|
50
60
|
A FAILURE is { "error": { "code", "message", "why", "fix" }, "next": […] } on stdout ("why" and "fix" where there is
|
|
@@ -63,69 +73,131 @@ ${codes}
|
|
|
63
73
|
Where the Postgres API listens is in mason status --json (postgres.url, host, port); never read .mason/config.json
|
|
64
74
|
for it. A failure that is a bug in mason says so; MASON_DEBUG=1 adds where it happened.
|
|
65
75
|
|
|
66
|
-
THE SOURCES FILE, mason/sources/<name>.yml (mason init writes it; one per project for now), is what Mason
|
|
76
|
+
THE SOURCES FILE, mason/sources/<name>.yml (mason init writes it; one per project for now), is what Mason syncs:
|
|
67
77
|
kind: postgres
|
|
68
78
|
url: \${DATABASE_URL} a variable from .env.local, .env or the shell; a literal URL is allowed for local dev
|
|
79
|
+
database: production optional: the database name a tool connects with (none: mason), to keep the name it has
|
|
80
|
+
sync: off how each table is kept current after its first sync: off (the default; mason sync
|
|
81
|
+
<table> reads it again), poll <interval> (poll 5s, poll 5m, poll 1h; 1s to 24h) or
|
|
82
|
+
replication (the change log: a primary key, wal_level = logical)
|
|
83
|
+
publication: \${PG_PUBLICATION} optional: the publication replication reads, its owner's, never altered
|
|
84
|
+
(publication: null: Mason's own, past one an earlier start recorded)
|
|
69
85
|
tables:
|
|
70
86
|
orders: a bare name is in public; billing.invoices names its schema
|
|
71
|
-
columns: [id, status] these columns (the primary key is always
|
|
87
|
+
columns: [id, status] these columns (the primary key is always synced)
|
|
88
|
+
sync: poll 30s this table's own: polled every 30 seconds (mason add orders --sync "poll 30s")
|
|
72
89
|
customers: {} every column
|
|
73
90
|
events:
|
|
74
91
|
exclude: [raw_body] every column but these
|
|
75
92
|
sessions:
|
|
76
|
-
key: [tenant_id, id] only for a table with no primary key: the unique columns to
|
|
77
|
-
mason add and mason remove edit one entry of it
|
|
78
|
-
|
|
93
|
+
key: [tenant_id, id] only for a table with no primary key: the unique columns to key it by (polling only)
|
|
94
|
+
mason add and mason remove edit one entry of it and keep the rest as it was, Mason running or not: with it running
|
|
95
|
+
they take effect at once, else at mason start. An edit by hand is read at the next mason start (or the next add or
|
|
96
|
+
remove). Column types are read from the database at every start. A change of a table's sync: keeps its synced data and
|
|
97
|
+
catches it up by one poll. mason status --json says each table's kept: { sync, syncedAt, nextAt } — how old its
|
|
98
|
+
synced data is; a table synced days ago answers today's count(*) as of then.
|
|
79
99
|
|
|
80
100
|
A FIRST SESSION, as a shell runs it:
|
|
81
|
-
mason doctor --json && mason init --json
|
|
101
|
+
mason doctor --json && mason init --json
|
|
82
102
|
mason tables --json choose the tables (rows, key, whether one can be added)
|
|
83
|
-
mason add orders customers --json
|
|
103
|
+
mason add orders customers --json added to the sources file, checked against the database
|
|
104
|
+
mason start --json && mason wait --json Mason in the background; until every added table is synced
|
|
105
|
+
mason sync orders --json read what changed in it since (a table whose sync is off: only so)
|
|
84
106
|
mason user add my-tool --json the URL and password a tool connects with
|
|
85
|
-
mason status --json each table
|
|
107
|
+
mason status --json each synced table, and the last day's slow, refused and unsupported statements
|
|
108
|
+
WATCHING IT, once it runs (every one with --json; --since and --until take an age, 30m or 2h or 7d, or an ISO 8601 time WITH
|
|
109
|
+
its zone):
|
|
110
|
+
mason status --json recentErrors: the sync's latest failures and the statements latest refused, failed or cancelled
|
|
111
|
+
mason logs --requests --failed --json every statement a tool sent that was not answered, with its SQLSTATE and message
|
|
112
|
+
mason logs --events --failed --json the sync's failures: a first sync, a poll, the stream, the database out of reach, a restart
|
|
113
|
+
mason logs --running --json the statements running now, one per session
|
|
114
|
+
mason cancel <session> --json stops that session's statement, as a client's cancel does (its client gets a "canceling statement" error)
|
|
115
|
+
mason usage --since 7d --step 1h --json the statements, the synced data's size and age, the memory over time, a sample a minute
|
|
116
|
+
With no database to hand, in a new folder: mason init demo --json && mason start --json — a demo database Mason
|
|
117
|
+
hosts itself (${DEMO_TABLES.join(", ")}), changed every few seconds, polled every 5 s (its sync: poll 5s). Init adds
|
|
118
|
+
${DEMO_ADDED_TABLES.join(", ")}; ${DEMO_TABLE_TO_ADD} is left to add: mason add ${DEMO_TABLE_TO_ADD} --json && mason wait --json, after mason start.
|
|
86
119
|
`;
|
|
87
120
|
}
|
|
88
121
|
const REPO = "https://github.com/embeddable-hq/mason-data/blob/main";
|
|
89
122
|
/**
|
|
90
123
|
* `mason help production` (the journey from running it locally to deploying it, on the mason-data image): the
|
|
91
|
-
* README's "
|
|
124
|
+
* README's "Going to production", offline and short — what travels, what the server needs, how it runs, how it is upgraded.
|
|
92
125
|
*/
|
|
93
126
|
export function productionGuide() {
|
|
94
127
|
return `GOING TO PRODUCTION — the project folder (mason/sources/, mason/mason.yml) is all that travels: its sources file
|
|
95
|
-
names the tables the server
|
|
128
|
+
names the tables the server syncs, and its url: names the variable that holds the database's URL. Nothing in .mason/ goes with it; the server makes its own synced data and state on its volume.
|
|
96
129
|
|
|
97
|
-
1. PREPARE THE DATABASE. Mason only reads it
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
130
|
+
1. PREPARE THE DATABASE. Mason only reads it, as a user that can SELECT the tables. How each table is kept current
|
|
131
|
+
after its first sync is the sources file's sync:, committed with it, so the server runs what you ran — at the top
|
|
132
|
+
for every table, and on an entry for that one:
|
|
133
|
+
sync: off the file's default for every table
|
|
134
|
+
tables:
|
|
135
|
+
orders: { sync: replication } this one from the change log, within seconds
|
|
136
|
+
customers: { sync: poll 5m } this one polled every 5 minutes
|
|
137
|
+
events: {} off, as the top says
|
|
138
|
+
off (the default mason init writes): after the first sync Mason reads the table again only when you run mason sync
|
|
139
|
+
<table> (before a report, from cron, at the end of a deploy) — nothing Mason does to your database is a hidden
|
|
140
|
+
default. poll <interval> (1s to 24h): a SELECT that scans the table whole, at that interval, stretched to four
|
|
141
|
+
times the last poll's own time, so the polls never take more than a fifth of the database's time; it works on a
|
|
142
|
+
read replica. replication: the change log, through one slot and one publication for the project; it needs
|
|
143
|
+
wal_level = logical, a user with REPLICATION (or rds_replication), a primary key (a key: table is polled), and
|
|
144
|
+
either a publication you name (publication: \${PG_PUBLICATION} at the top of the sources file, read as it is, never
|
|
145
|
+
altered: it must carry every replicated table; a read replica needs one) or the right to create Mason's own, which
|
|
146
|
+
carries the replicated tables alone. The slot holds the database's WAL while Mason is down, so cap it on the
|
|
147
|
+
database with max_slot_wal_keep_size (Postgres 13+); it is dropped, verified gone, when no table's sync is
|
|
148
|
+
replication any more, and at mason destroy. The flags --replication, --poll and --publication of mason start, and
|
|
149
|
+
MASON_CHANGES, are gone: each answers with the line to write in the sources file. A project that streams, under a
|
|
150
|
+
file with no sync: anywhere, is refused rather than dropped (sync: replication keeps the slot; sync: off or
|
|
151
|
+
sync: poll <interval> confirms the drop), and so is one with a recorded publication that nothing names now
|
|
152
|
+
(publication: <name>, or publication: null for Mason's own, with PG_PUBLICATION unset). Try it locally first:
|
|
153
|
+
mason add orders --sync replication that table from the change log, as the server reads it
|
|
154
|
+
mason sync customers any table read again now, whatever its sync:
|
|
104
155
|
|
|
105
156
|
2. RUN IT: one container, ghcr.io/embeddable-hq/mason-data:<version> (the version mason.yml names), running mason serve:
|
|
106
|
-
DATABASE_URL the database to
|
|
157
|
+
DATABASE_URL the database to sync from (the variable the sources file's url: names)
|
|
107
158
|
MASON_API_KEY what the users' passwords are made with (openssl rand -hex 24)
|
|
108
159
|
MASON_PROJECT your mason/ folder, mounted (default ./mason)
|
|
109
|
-
MASON_DATA the volume whose .mason/ holds the
|
|
160
|
+
MASON_DATA the volume whose .mason/ holds the synced data and the state (default .)
|
|
110
161
|
MASON_PG_HOST=0.0.0.0 and TLS: MASON_PG_TLS_CERT / MASON_PG_TLS_KEY, or MASON_PG_SELF_SIGNED_TLS=1, or
|
|
111
162
|
MASON_PG_PLAIN=1 behind a TLS proxy of your own
|
|
112
|
-
Docker Compose: ${REPO}/deploy/mason-data/README.md. One container per volume, never two:
|
|
163
|
+
Docker Compose: ${REPO}/deploy/mason-data/README.md. One container per volume, never two: the synced data lets one process
|
|
113
164
|
write its file, so run one replica, with no autoscaling, and stop the old container before the new one starts.
|
|
114
165
|
GET /healthz on MASON_HEALTH_PORT (4140) answers 503 while it starts and 200 once it serves; the first start takes
|
|
115
|
-
about 15 seconds before that, then
|
|
166
|
+
about 15 seconds before that, then syncs the tables. GET /metrics on the same port is for Prometheus: numbers only
|
|
167
|
+
(tables by phase, lag, ages, sizes, statements), never a table's or a user's name, so it needs no secret.
|
|
168
|
+
MEMORY: the engine may use half the container's memory limit by default (else half the machine's RAM); the other
|
|
169
|
+
half is the page cache, Node and the catalog (~820 MB). Give the container enough that the page cache holds the
|
|
170
|
+
columns your dashboards read: a query over a column it does not hold runs at the disk's speed. The benchmark's synced data
|
|
171
|
+
was about a fifth of its tables in Postgres (10.1 GiB to 1.8 GiB, on a 15 GiB machine); mason status says yours.
|
|
172
|
+
To set it, MASON_DUCKDB_MEMORY=4GB with the container's limit about twice that. A statement past it fails alone
|
|
173
|
+
(53200) and the run goes on; a value near the container's limit is an OOM kill.
|
|
174
|
+
CPU: memory first, then cores. The benchmark's machine (4 vCPU Graviton3, 15 GiB) answered 32 clients at 10 M rows,
|
|
175
|
+
12 statements a second at a p95 of 3.5 s, as many a second as at 8 clients: more clients waited longer. A first sync's
|
|
176
|
+
readers take about a core each while it runs (MASON_COPY_PARALLEL, default 3). The image is arm64 (the measured
|
|
177
|
+
one) and amd64. Which settings to decide: ${REPO}/deploy/mason-data/README.md#settings
|
|
116
178
|
|
|
117
179
|
3. CONNECT THE TOOLS: beside the server (docker compose exec mason …), a user is a password a tool keeps:
|
|
118
180
|
mason user add metabase its URL and password, printed once
|
|
181
|
+
mason user add agent --role agent a user held to a role of mason/roles.yml: the tables and columns it reads
|
|
182
|
+
mason user add acme --role tenant --set tenant=42 and the rows: a role whose where: reads :tenant
|
|
119
183
|
mason user list
|
|
120
184
|
mason user remove metabase ends it at once, without a restart
|
|
121
|
-
The tool connects to the Postgres API (5433) as to a read replica, each added table a table
|
|
122
|
-
|
|
185
|
+
The tool connects to the Postgres API (5433) as to a read replica, each added table a table, in the database
|
|
186
|
+
mason: a tool moving from your Postgres changes its host, port and database name, or database: <name> in the sources
|
|
187
|
+
file keeps its name (MASON_PG_DATABASE on a server whose sources file has no such line). Changing
|
|
188
|
+
MASON_API_KEY removes every user. mason/roles.yml ships with mason/ (mason help user shows the file): a user with
|
|
189
|
+
a role is refused anything past it as Postgres refuses it (42501), and information_schema and mason mcp list it only
|
|
190
|
+
what it may read; a user added without --role reads as the file's default: (mason init writes default:
|
|
191
|
+
everything, tables: all — every synced table), and nothing when the file names no default. A role's where: and
|
|
192
|
+
through: filter its tables' rows by the values each user carries (--set), every read of them filtered; a user
|
|
193
|
+
missing a value is refused at every statement, never answered with no rows. A roles file from before this release
|
|
194
|
+
names no default: write default: <role> at its top before upgrading, or its roleless users read nothing.
|
|
123
195
|
|
|
124
|
-
4. CHANGE IT: ship mason/ with your release and restart the container; a table added or removed is
|
|
196
|
+
4. CHANGE IT: ship mason/ with your release and restart the container; a table added or removed is synced or dropped
|
|
125
197
|
at the next start, or beside the server:
|
|
126
|
-
mason add public.orders
|
|
127
|
-
mason remove orders taken out of the sources file, its
|
|
128
|
-
mason status each table
|
|
198
|
+
mason add public.orders synced at once, added to the mounted mason/sources/
|
|
199
|
+
mason remove orders taken out of the sources file, its synced data dropped
|
|
200
|
+
mason status each synced table, the statements, the recent errors
|
|
129
201
|
Upgrade: read the release notes, then move mason.yml and the image together. Removing Mason for good in replication mode:
|
|
130
202
|
SELECT pg_drop_replication_slot('mason_dev_<name>_<source>') — the slot holds your WAL (mason status names it).
|
|
131
203
|
|