@mason-data/cli 0.7.0 → 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.
Files changed (111) hide show
  1. package/README.md +90 -71
  2. package/dist/src/api/error.js +3 -0
  3. package/dist/src/api/postgres/catalog.js +222 -13
  4. package/dist/src/api/postgres/encode.js +3 -1
  5. package/dist/src/api/postgres/explain.js +3 -2
  6. package/dist/src/api/postgres/protocol.js +39 -23
  7. package/dist/src/api/postgres/server.js +1173 -207
  8. package/dist/src/api/postgres/translate.js +81 -9
  9. package/dist/src/api/request-log.js +18 -5
  10. package/dist/src/cdc/poll.js +5 -5
  11. package/dist/src/cli/background.js +29 -6
  12. package/dist/src/cli/cancel.js +75 -0
  13. package/dist/src/cli/command.js +1 -0
  14. package/dist/src/cli/commands.js +622 -183
  15. package/dist/src/cli/control.js +59 -0
  16. package/dist/src/cli/demo.js +19 -10
  17. package/dist/src/cli/dev.js +351 -111
  18. package/dist/src/cli/doctor.js +20 -10
  19. package/dist/src/cli/guides.js +79 -31
  20. package/dist/src/cli/init.js +119 -58
  21. package/dist/src/cli/machine.js +12 -11
  22. package/dist/src/cli/main.js +3 -3
  23. package/dist/src/cli/mcp.js +29 -20
  24. package/dist/src/cli/metrics.js +79 -0
  25. package/dist/src/cli/pg-tls.js +13 -3
  26. package/dist/src/cli/pg-url.js +4 -0
  27. package/dist/src/cli/pin.js +13 -12
  28. package/dist/src/cli/preflight.js +8 -7
  29. package/dist/src/cli/project.js +43 -4
  30. package/dist/src/cli/readiness.js +38 -4
  31. package/dist/src/cli/registry.js +53 -34
  32. package/dist/src/cli/replication-checks.js +7 -5
  33. package/dist/src/cli/replication-mode.js +105 -26
  34. package/dist/src/cli/request-filter.js +134 -0
  35. package/dist/src/cli/roles.js +584 -0
  36. package/dist/src/cli/run-server.js +483 -41
  37. package/dist/src/cli/runs.js +1 -1
  38. package/dist/src/cli/security.js +10 -8
  39. package/dist/src/cli/serve.js +144 -42
  40. package/dist/src/cli/share-run.js +286 -0
  41. package/dist/src/cli/share.js +286 -0
  42. package/dist/src/cli/source-edit.js +65 -13
  43. package/dist/src/cli/sources.js +287 -38
  44. package/dist/src/cli/supervise.js +12 -1
  45. package/dist/src/cli/sync.js +55 -25
  46. package/dist/src/cli/tunnel.js +200 -0
  47. package/dist/src/cli/ui.js +9 -6
  48. package/dist/src/cli/usage.js +128 -0
  49. package/dist/src/cli/user.js +496 -40
  50. package/dist/src/cli/version.js +9 -7
  51. package/dist/src/demo/seed.js +3 -0
  52. package/dist/src/duck/cancel.js +10 -1
  53. package/dist/src/duck/events.js +134 -0
  54. package/dist/src/duck/executor-worker.js +18 -9
  55. package/dist/src/duck/executor.js +43 -30
  56. package/dist/src/duck/explain.js +56 -46
  57. package/dist/src/duck/extensions.js +160 -0
  58. package/dist/src/duck/functions.js +13 -14
  59. package/dist/src/duck/gaps.js +17 -8
  60. package/dist/src/duck/grants.js +176 -0
  61. package/dist/src/duck/history.js +123 -0
  62. package/dist/src/duck/kept.js +6 -6
  63. package/dist/src/duck/plan.js +102 -25
  64. package/dist/src/duck/publication.js +6 -6
  65. package/dist/src/duck/requests.js +265 -93
  66. package/dist/src/duck/rewrite.js +25 -5
  67. package/dist/src/duck/row-filter.js +649 -0
  68. package/dist/src/duck/store.js +156 -120
  69. package/dist/src/duck/stream.js +10 -5
  70. package/dist/src/duck/sync.js +835 -268
  71. package/dist/src/duck/types.js +6 -5
  72. package/dist/src/duck/usage.js +324 -0
  73. package/dist/src/duck/users.js +64 -15
  74. package/dist/src/duck/verified.js +3 -0
  75. package/dist/src/duck/views.js +2 -3
  76. package/dist/src/lib/database-name.js +62 -0
  77. package/dist/src/lib/dotenv.js +17 -4
  78. package/dist/src/lib/log-buffer.js +68 -0
  79. package/dist/src/lib/pg-numeric.js +5 -2
  80. package/dist/src/lib/project-version.js +23 -23
  81. package/dist/src/lib/table-sync.js +267 -0
  82. package/dist/src/web/app.css +706 -66
  83. package/dist/src/web/app.js +327 -410
  84. package/dist/src/web/charts.js +229 -0
  85. package/dist/src/web/index.html +82 -18
  86. package/dist/src/web/pages/queries.js +192 -0
  87. package/dist/src/web/pages/status.js +446 -0
  88. package/dist/src/web/pages/syncs.js +155 -0
  89. package/dist/src/web/pages/tables.js +434 -0
  90. package/dist/src/web/pages/usage.js +397 -0
  91. package/dist/src/web/pages/users.js +218 -0
  92. package/dist/src/web/shared.js +568 -0
  93. package/dist/src/web/vendor/README.md +25 -0
  94. package/dist/src/web/vendor/geist-1.7.2/Geist-Variable.woff2 +0 -0
  95. package/dist/src/web/vendor/geist-1.7.2/GeistMono-Variable.woff2 +0 -0
  96. package/dist/src/web/vendor/geist-1.7.2/LICENSE.txt +92 -0
  97. package/dist/src/web/vendor/lucide-1.56.0/LICENSE.txt +43 -0
  98. package/dist/src/web/vendor/lucide-1.56.0/chart-column.mjs +29 -0
  99. package/dist/src/web/vendor/lucide-1.56.0/layout-dashboard.mjs +29 -0
  100. package/dist/src/web/vendor/lucide-1.56.0/logs.mjs +34 -0
  101. package/dist/src/web/vendor/lucide-1.56.0/menu.mjs +28 -0
  102. package/dist/src/web/vendor/lucide-1.56.0/square-terminal.mjs +28 -0
  103. package/dist/src/web/vendor/lucide-1.56.0/table.mjs +29 -0
  104. package/dist/src/web/vendor/lucide-1.56.0/users.mjs +29 -0
  105. package/dist/src/web/vendor/lucide-1.56.0/x.mjs +27 -0
  106. package/dist/src/web/vendor/uplot-1.6.32/LICENSE.txt +21 -0
  107. package/dist/src/web/vendor/uplot-1.6.32/uPlot.esm.js +6140 -0
  108. package/dist/src/web/vendor/uplot-1.6.32/uPlot.min.css +1 -0
  109. package/dist/src/web/vendor/zilla-slab-5.3.0/LICENSE.txt +93 -0
  110. package/dist/src/web/vendor/zilla-slab-5.3.0/zilla-slab-latin-700-normal.woff2 +0 -0
  111. package/package.json +10 -3
@@ -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 (Mason's copy uses up to half of it by
29
- default; MASON_DUCKDB_MEMORY=4GiB sets it), the ports Mason listens on — with the way out for anything missing. In a
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 copied, the statements of the last day it refused or answered slowly.
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; Mason's copy uses up to ${share.trim()}${set !== undefined ? " (MASON_DUCKDB_MEMORY)" : " (half, the default; MASON_DUCKDB_MEMORY sets it)"}` +
69
- (low ? ". Under 2 GiB, a large table's copy and a query starve each other: expect statements refused for memory" : ""),
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
@@ -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) copied and kept current` : "no table added yet (mason tables, then mason add <table>)" });
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
@@ -170,12 +179,12 @@ export function nextSteps(checks, run, project) {
170
179
  return [["mason doctor", "again, once .mason/config.json is repaired or moved aside (the detail above says how)"]];
171
180
  // the demo database is hosted by the run itself (src/cli/demo.ts): down until mason start makes it
172
181
  if (bad("database") && project?.config.demo === true)
173
- return [["mason start", "hosts the demo database, then copies its tables"]];
182
+ return [["mason start", "hosts the demo database, then syncs its tables"]];
174
183
  if (bad("database"))
175
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"})`]];
176
185
  if (project === null)
177
186
  return [
178
- ["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"],
179
188
  ["mason init postgres://user:password@localhost:5432/your_app", "or name it"],
180
189
  ];
181
190
  if (run === null)
@@ -185,7 +194,7 @@ export function nextSteps(checks, run, project) {
185
194
  if (bad("project"))
186
195
  return [["mason wait", "each problem, with the way out"]];
187
196
  if (checks.some((c) => c.name === "project" && /no table added/.test(c.detail)))
188
- return [["mason tables", "the database's tables and their columns"], ["mason add <table>", "copy a table into Mason"]];
197
+ return [["mason tables", "the database's tables and their columns"], ["mason add <table>", "sync a table into Mason"]];
189
198
  if (bad("queries"))
190
199
  return [["mason status", "the refused and slow statements, each with what to do"]];
191
200
  return [["mason user add <name>", "a user for a BI tool or your app: its password and URL, printed once"]];
@@ -222,6 +231,7 @@ export async function doctor(argv, deps = {}) {
222
231
  if (run === null) {
223
232
  checks.push(nodeCheck());
224
233
  checks.push(memoryCheck());
234
+ checks.push(await extensionsCheck());
225
235
  checks.push(await portsCheck([], portHolder, process.env, project?.config.pgPort ?? null));
226
236
  if (project) {
227
237
  // the database the project's sources file names (src/cli/sources.ts): a file that cannot be read is the failing line
@@ -253,7 +263,7 @@ export async function doctor(argv, deps = {}) {
253
263
  out(` ${mark} ${c.name.padEnd(w)} ${wrap(c.detail, s.width, 2 + 3 + 1 + w + 2)}`);
254
264
  }
255
265
  if (run !== null)
256
- out(s.dim(` its copy of the data and its state in ${run.home}. mason status for the details`));
266
+ out(s.dim(` its synced data and its state in ${run.home}. mason status for the details`));
257
267
  if (run === null || !ready)
258
268
  out(nextBlock(next));
259
269
  return ready ? 0 : 1;
@@ -37,11 +37,12 @@ async function agentGuide() {
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
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 lines, or with --requests the request log. Nothing a command prints
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
41
42
  refers to a file this package does not ship.
42
43
 
43
44
  AN AGENT WITH MCP (Claude Code, Claude Desktop, any MCP client) adds Mason as an MCP server first, then asks with
44
- its tools rather than through a shell: tables (each copy's rows and how current it is), describe, query (rows with
45
+ its tools rather than through a shell: tables (each synced table's rows and how current it is), describe, query (rows with
45
46
  their Postgres types, or the refusal with what to write instead) and explain. In the project's folder, Mason running:
46
47
  mason user add claude --json its url, printed once
47
48
  claude mcp add mason --env MASON_URL="<url>" -- mason mcp
@@ -53,7 +54,7 @@ ${commands}
53
54
 
54
55
  EVERY ANSWER is an object with next: [{ command, what }] — the commands to run next, the same a person reads under
55
56
  Next. The other keys are each command's; its help names them (mason help <command>, the --json line). Lists are wrapped
56
- ({ tables: […], next }), never bare. mason wait --json blocks until every added table is copied and kept current, then
57
+ ({ tables: […], next }), never bare. mason wait --json blocks until every added table is synced and kept current, then
57
58
  answers once (exit 0), or with error beside the state (exit 1 failed, 124 timed out: nothing stopped, run it again).
58
59
 
59
60
  A FAILURE is { "error": { "code", "message", "why", "fix" }, "next": […] } on stdout ("why" and "fix" where there is
@@ -72,30 +73,48 @@ ${codes}
72
73
  Where the Postgres API listens is in mason status --json (postgres.url, host, port); never read .mason/config.json
73
74
  for it. A failure that is a bug in mason says so; MASON_DEBUG=1 adds where it happened.
74
75
 
75
- THE SOURCES FILE, mason/sources/<name>.yml (mason init writes it; one per project for now), is what Mason copies:
76
+ THE SOURCES FILE, mason/sources/<name>.yml (mason init writes it; one per project for now), is what Mason syncs:
76
77
  kind: postgres
77
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)
78
85
  tables:
79
86
  orders: a bare name is in public; billing.invoices names its schema
80
- columns: [id, status] these columns (the primary key is always copied)
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")
81
89
  customers: {} every column
82
90
  events:
83
91
  exclude: [raw_body] every column but these
84
92
  sessions:
85
- key: [tenant_id, id] only for a table with no primary key: the unique columns to copy it by (polling only)
93
+ key: [tenant_id, id] only for a table with no primary key: the unique columns to key it by (polling only)
86
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
87
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
88
- remove). Column types are read from the database at every start.
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.
89
99
 
90
100
  A FIRST SESSION, as a shell runs it:
91
101
  mason doctor --json && mason init --json
92
102
  mason tables --json choose the tables (rows, key, whether one can be added)
93
103
  mason add orders customers --json added to the sources file, checked against the database
94
- mason start --json && mason wait --json Mason in the background; until every added table is copied
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)
95
106
  mason user add my-tool --json the URL and password a tool connects with
96
- mason status --json each table's copy, and the last day's slow and refused statements
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
97
116
  With no database to hand, in a new folder: mason init demo --json && mason start --json — a demo database Mason
98
- hosts itself (${DEMO_TABLES.join(", ")}), changed every few seconds, read by polling only. Init adds
117
+ hosts itself (${DEMO_TABLES.join(", ")}), changed every few seconds, polled every 5 s (its sync: poll 5s). Init adds
99
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.
100
119
  `;
101
120
  }
@@ -106,50 +125,79 @@ const REPO = "https://github.com/embeddable-hq/mason-data/blob/main";
106
125
  */
107
126
  export function productionGuide() {
108
127
  return `GOING TO PRODUCTION — the project folder (mason/sources/, mason/mason.yml) is all that travels: its sources file
109
- names the tables the server copies, and its url: names the variable that holds the database's URL. Nothing in .mason/ goes with it; the server makes its own copy and state on its volume.
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.
110
129
 
111
- 1. PREPARE THE DATABASE. Mason only reads it. Polling (the default) needs a user that can SELECT the tables, and costs
112
- the database a scan of each table per poll; it works on a read replica. Logical replication (MASON_CHANGES=replication)
113
- needs wal_level = logical, a user with REPLICATION, and either a publication you name (PG_PUBLICATION, read as it is,
114
- never altered) or the right to create one; the slot holds the database's WAL while Mason is down, so cap it with
115
- max_slot_wal_keep_size. Try it locally first:
116
- mason start --replication the change log, as the server reads it
117
- mason start --replication --publication app_pub through a publication the database's owner made
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:
118
155
 
119
156
  2. RUN IT: one container, ghcr.io/embeddable-hq/mason-data:<version> (the version mason.yml names), running mason serve:
120
- DATABASE_URL the database to copy from (the variable the sources file's url: names)
157
+ DATABASE_URL the database to sync from (the variable the sources file's url: names)
121
158
  MASON_API_KEY what the users' passwords are made with (openssl rand -hex 24)
122
159
  MASON_PROJECT your mason/ folder, mounted (default ./mason)
123
- MASON_DATA the volume whose .mason/ holds the copy and the state (default .)
160
+ MASON_DATA the volume whose .mason/ holds the synced data and the state (default .)
124
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
125
162
  MASON_PG_PLAIN=1 behind a TLS proxy of your own
126
- Docker Compose: ${REPO}/deploy/mason-data/README.md. One container per volume, never two: DuckDB lets one process
163
+ Docker Compose: ${REPO}/deploy/mason-data/README.md. One container per volume, never two: the synced data lets one process
127
164
  write its file, so run one replica, with no autoscaling, and stop the old container before the new one starts.
128
165
  GET /healthz on MASON_HEALTH_PORT (4140) answers 503 while it starts and 200 once it serves; the first start takes
129
- about 15 seconds before that, then copies the tables.
130
- MEMORY: the copy may use half the container's memory limit by default (else half the machine's RAM); the other
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
131
169
  half is the page cache, Node and the catalog (~820 MB). Give the container enough that the page cache holds the
132
- columns your dashboards read: a query over a column it does not hold runs at the disk's speed. The benchmark's copy
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
133
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.
134
172
  To set it, MASON_DUCKDB_MEMORY=4GB with the container's limit about twice that. A statement past it fails alone
135
173
  (53200) and the run goes on; a value near the container's limit is an OOM kill.
136
174
  CPU: memory first, then cores. The benchmark's machine (4 vCPU Graviton3, 15 GiB) answered 32 clients at 10 M rows,
137
- 12 statements a second at a p95 of 3.5 s, as many a second as at 8 clients: more clients waited longer. A copy's
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
138
176
  readers take about a core each while it runs (MASON_COPY_PARALLEL, default 3). The image is arm64 (the measured
139
177
  one) and amd64. Which settings to decide: ${REPO}/deploy/mason-data/README.md#settings
140
178
 
141
179
  3. CONNECT THE TOOLS: beside the server (docker compose exec mason …), a user is a password a tool keeps:
142
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
143
183
  mason user list
144
184
  mason user remove metabase ends it at once, without a restart
145
- The tool connects to the Postgres API (5433) as to a read replica, each added table a table. Changing
146
- MASON_API_KEY removes every user.
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.
147
195
 
148
- 4. CHANGE IT: ship mason/ with your release and restart the container; a table added or removed is copied or dropped
196
+ 4. CHANGE IT: ship mason/ with your release and restart the container; a table added or removed is synced or dropped
149
197
  at the next start, or beside the server:
150
- mason add public.orders copied at once, added to the mounted mason/sources/
151
- mason remove orders taken out of the sources file, its copy dropped
152
- mason status each table's copy, the slow and refused statements
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
153
201
  Upgrade: read the release notes, then move mason.yml and the image together. Removing Mason for good in replication mode:
154
202
  SELECT pg_drop_replication_slot('mason_dev_<name>_<source>') — the slot holds your WAL (mason status names it).
155
203