@mason-data/cli 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.md +101 -73
  2. package/dist/src/api/error.js +3 -0
  3. package/dist/src/api/postgres/catalog-worker.js +49 -2
  4. package/dist/src/api/postgres/catalog.js +368 -25
  5. package/dist/src/api/postgres/encode.js +3 -1
  6. package/dist/src/api/postgres/explain.js +3 -2
  7. package/dist/src/api/postgres/listener.js +11 -3
  8. package/dist/src/api/postgres/protocol.js +39 -23
  9. package/dist/src/api/postgres/proxy.js +283 -0
  10. package/dist/src/api/postgres/schema.js +6 -0
  11. package/dist/src/api/postgres/server.js +1617 -245
  12. package/dist/src/api/postgres/translate.js +127 -10
  13. package/dist/src/api/request-log.js +18 -5
  14. package/dist/src/cdc/poll.js +5 -5
  15. package/dist/src/cli/background.js +47 -25
  16. package/dist/src/cli/bridge-stdin.js +101 -0
  17. package/dist/src/cli/cancel.js +75 -0
  18. package/dist/src/cli/command.js +170 -11
  19. package/dist/src/cli/commands.js +804 -209
  20. package/dist/src/cli/control.js +59 -0
  21. package/dist/src/cli/credentials.js +182 -0
  22. package/dist/src/cli/demo.js +20 -11
  23. package/dist/src/cli/deploy.js +574 -0
  24. package/dist/src/cli/dev.js +491 -130
  25. package/dist/src/cli/doctor.js +131 -38
  26. package/dist/src/cli/generated-files.js +143 -0
  27. package/dist/src/cli/guides.js +175 -36
  28. package/dist/src/cli/hosted.js +850 -0
  29. package/dist/src/cli/init.js +162 -65
  30. package/dist/src/cli/login.js +289 -0
  31. package/dist/src/cli/machine.js +13 -12
  32. package/dist/src/cli/main.js +11 -5
  33. package/dist/src/cli/mason.cjs +17 -0
  34. package/dist/src/cli/mcp.js +31 -22
  35. package/dist/src/cli/metrics.js +79 -0
  36. package/dist/src/cli/output-gate.js +176 -0
  37. package/dist/src/cli/page-share.js +96 -0
  38. package/dist/src/cli/pg-tls.js +14 -4
  39. package/dist/src/cli/pg-url.js +4 -0
  40. package/dist/src/cli/pin.js +19 -14
  41. package/dist/src/cli/pool-size.cjs +85 -0
  42. package/dist/src/cli/ports.js +2 -0
  43. package/dist/src/cli/preflight.js +138 -46
  44. package/dist/src/cli/project.js +142 -4
  45. package/dist/src/cli/readiness.js +76 -6
  46. package/dist/src/cli/registry.js +65 -38
  47. package/dist/src/cli/replica-hosted.js +665 -0
  48. package/dist/src/cli/replica.js +76 -0
  49. package/dist/src/cli/replication-checks.js +7 -5
  50. package/dist/src/cli/replication-mode.js +105 -26
  51. package/dist/src/cli/request-filter.js +134 -0
  52. package/dist/src/cli/roles.js +585 -0
  53. package/dist/src/cli/run-server.js +637 -48
  54. package/dist/src/cli/runs.js +1 -1
  55. package/dist/src/cli/security.js +67 -18
  56. package/dist/src/cli/serve.js +204 -46
  57. package/dist/src/cli/share-run.js +329 -0
  58. package/dist/src/cli/share.js +286 -0
  59. package/dist/src/cli/source-edit.js +75 -17
  60. package/dist/src/cli/source-role.js +344 -0
  61. package/dist/src/cli/sources.js +306 -47
  62. package/dist/src/cli/style.js +2 -1
  63. package/dist/src/cli/supervise.js +27 -24
  64. package/dist/src/cli/sync.js +55 -25
  65. package/dist/src/cli/token-hosted.js +337 -0
  66. package/dist/src/cli/tunnel.js +200 -0
  67. package/dist/src/cli/ui-ssh.js +460 -0
  68. package/dist/src/cli/ui.js +124 -17
  69. package/dist/src/cli/usage.js +128 -0
  70. package/dist/src/cli/user-hosted.js +299 -0
  71. package/dist/src/cli/user.js +573 -65
  72. package/dist/src/cli/version.js +9 -7
  73. package/dist/src/demo/seed.js +29 -0
  74. package/dist/src/duck/analyze.js +36 -20
  75. package/dist/src/duck/cancel.js +10 -1
  76. package/dist/src/duck/collation.js +266 -0
  77. package/dist/src/duck/constant-key.js +94 -0
  78. package/dist/src/duck/distinct-key.js +283 -26
  79. package/dist/src/duck/events.js +165 -0
  80. package/dist/src/duck/executor-worker.js +20 -11
  81. package/dist/src/duck/executor.js +47 -33
  82. package/dist/src/duck/explain.js +60 -46
  83. package/dist/src/duck/extensions.js +160 -0
  84. package/dist/src/duck/functions.js +461 -63
  85. package/dist/src/duck/gaps.js +48 -21
  86. package/dist/src/duck/generated-plan.js +1070 -0
  87. package/dist/src/duck/generated.js +906 -0
  88. package/dist/src/duck/grants.js +176 -0
  89. package/dist/src/duck/held-types.js +64 -0
  90. package/dist/src/duck/history.js +127 -0
  91. package/dist/src/duck/jsonb-order.js +9 -12
  92. package/dist/src/duck/kept-route.js +6 -5
  93. package/dist/src/duck/kept.js +15 -13
  94. package/dist/src/duck/late.js +1469 -40
  95. package/dist/src/duck/libc-bound.js +341 -0
  96. package/dist/src/duck/libc-order.js +892 -126
  97. package/dist/src/duck/pg-eval-worker.js +7 -1
  98. package/dist/src/duck/pg-eval.js +6 -4
  99. package/dist/src/duck/pgtree.js +774 -23
  100. package/dist/src/duck/plan.js +369 -47
  101. package/dist/src/duck/publication.js +20 -6
  102. package/dist/src/duck/requests.js +269 -93
  103. package/dist/src/duck/resolved.js +117 -2
  104. package/dist/src/duck/rewrite.js +4584 -580
  105. package/dist/src/duck/row-filter.js +655 -0
  106. package/dist/src/duck/statement.js +19 -0
  107. package/dist/src/duck/store.js +439 -177
  108. package/dist/src/duck/stream.js +19 -10
  109. package/dist/src/duck/sync.js +1411 -368
  110. package/dist/src/duck/token.js +4 -1
  111. package/dist/src/duck/types.js +69 -12
  112. package/dist/src/duck/usage.js +326 -0
  113. package/dist/src/duck/users.js +69 -15
  114. package/dist/src/duck/verified.js +161 -1
  115. package/dist/src/duck/views.js +5 -5
  116. package/dist/src/lib/database-name.js +62 -0
  117. package/dist/src/lib/database-url.js +55 -10
  118. package/dist/src/lib/dotenv.js +17 -4
  119. package/dist/src/lib/instant.js +50 -0
  120. package/dist/src/lib/log-buffer.js +77 -0
  121. package/dist/src/lib/log-read.js +196 -0
  122. package/dist/src/lib/pg-numeric.js +390 -49
  123. package/dist/src/lib/project-version.js +23 -23
  124. package/dist/src/lib/secret-mask.js +142 -0
  125. package/dist/src/lib/table-sync.js +282 -0
  126. package/dist/src/web/app.css +706 -66
  127. package/dist/src/web/app.js +331 -409
  128. package/dist/src/web/charts.js +229 -0
  129. package/dist/src/web/index.html +82 -18
  130. package/dist/src/web/pages/queries.js +192 -0
  131. package/dist/src/web/pages/status.js +611 -0
  132. package/dist/src/web/pages/syncs.js +155 -0
  133. package/dist/src/web/pages/tables.js +439 -0
  134. package/dist/src/web/pages/usage.js +397 -0
  135. package/dist/src/web/pages/users.js +219 -0
  136. package/dist/src/web/shared.js +584 -0
  137. package/dist/src/web/vendor/README.md +25 -0
  138. package/dist/src/web/vendor/geist-1.7.2/Geist-Variable.woff2 +0 -0
  139. package/dist/src/web/vendor/geist-1.7.2/GeistMono-Variable.woff2 +0 -0
  140. package/dist/src/web/vendor/geist-1.7.2/LICENSE.txt +92 -0
  141. package/dist/src/web/vendor/lucide-1.56.0/LICENSE.txt +43 -0
  142. package/dist/src/web/vendor/lucide-1.56.0/chart-column.mjs +29 -0
  143. package/dist/src/web/vendor/lucide-1.56.0/layout-dashboard.mjs +29 -0
  144. package/dist/src/web/vendor/lucide-1.56.0/logs.mjs +34 -0
  145. package/dist/src/web/vendor/lucide-1.56.0/menu.mjs +28 -0
  146. package/dist/src/web/vendor/lucide-1.56.0/square-terminal.mjs +28 -0
  147. package/dist/src/web/vendor/lucide-1.56.0/table.mjs +29 -0
  148. package/dist/src/web/vendor/lucide-1.56.0/users.mjs +29 -0
  149. package/dist/src/web/vendor/lucide-1.56.0/x.mjs +27 -0
  150. package/dist/src/web/vendor/uplot-1.6.32/LICENSE.txt +21 -0
  151. package/dist/src/web/vendor/uplot-1.6.32/uPlot.esm.js +6140 -0
  152. package/dist/src/web/vendor/uplot-1.6.32/uPlot.min.css +1 -0
  153. package/dist/src/web/vendor/zilla-slab-5.3.0/LICENSE.txt +93 -0
  154. package/dist/src/web/vendor/zilla-slab-5.3.0/zilla-slab-latin-700-normal.woff2 +0 -0
  155. package/package.json +12 -4
@@ -7,18 +7,99 @@
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
+ import { DEFAULT_MEMORY_RULE, memorySize, MEMORY_FLOOR_BYTES, OUTSIDE_RESERVE_BYTES } from "../duck/store.js";
11
+ import { DEMO_ADDED_TABLES, DEMO_GENERATED, DEMO_TABLES, DEMO_TABLE_TO_ADD } from "../demo/seed.js";
11
12
  /** The guides, by the name `mason help <guide>` takes. */
12
13
  export const GUIDES = {
13
14
  agent: "driving Mason from a coding agent: the commands in order, --json and its shapes, exit codes, failures",
14
15
  production: "going to production: the mason-data image, its settings, Docker Compose, logical replication, upgrades",
16
+ generated: "generated tables: a table declared in SQL, kept current by Mason, read by name",
15
17
  };
16
18
  /** A guide's text: built at run time from the registry's and the commands' own tables. */
17
19
  export async function guideText(guide) {
18
20
  if (guide === "agent")
19
21
  return agentGuide();
22
+ if (guide === "generated")
23
+ return generatedGuide();
20
24
  return productionGuide();
21
25
  }
26
+ /** THE GENERATED TABLES' PAGE: its keys are the reader's own table (src/cli/generated-files.ts `GENERATED_KEYS`), so it
27
+ * cannot name a key the file does not take. */
28
+ async function generatedGuide() {
29
+ const { GENERATED_DIR, GENERATED_KEYS } = await import("./generated-files.js");
30
+ const w = Math.max(...Object.keys(GENERATED_KEYS).map((k) => k.length));
31
+ const keys = Object.entries(GENERATED_KEYS).map(([k, what]) => ` ${k.padEnd(w)} ${what}`).join("\n");
32
+ return `GENERATED TABLES — a table declared in SQL, kept current by Mason, read by name
33
+
34
+ A statement that repeats expensive work on every read can read a table that holds its answer instead. One file per
35
+ table in mason/${GENERATED_DIR}/, committed beside the sources file. The file's name is the table's: requests.yml is
36
+ public.requests, reports.daily.yml is reports.daily.
37
+
38
+ kind: scheduled
39
+ target_lag: 5m
40
+ sql: |
41
+ SELECT tenant_id, date_trunc('day', created_at, 'UTC') AS day, count(*) AS orders
42
+ FROM orders
43
+ GROUP BY 1, 2
44
+ order_by: day, tenant_id
45
+
46
+ THE KEYS
47
+ ${keys}
48
+
49
+ HOW IT STAYS CURRENT
50
+ A scheduled table runs its whole SQL over one snapshot, into a table beside the old one, and swaps it in. The old
51
+ table serves until then. It runs again once a base has changed and target_lag comes due; nothing runs over idle
52
+ bases. A run shares the queue that first syncs use, one at a time, and reads nothing from your database.
53
+ A run that fails leaves the table serving what it held, and mason status says why.
54
+ A table may read another generated table. Parents are built first; a base's change re-runs the whole chain above
55
+ it over one snapshot, and the chain swaps in at once. A table that reads itself, through others or not, is refused.
56
+ A parent whose run failed, or that was refused before it was ever built, holds back its children, and mason status
57
+ says why; a parent already built whose file is then refused keeps being re-run from what it was built from, and
58
+ carries its children. A table's lag is measured against the base, across the chain: the oldest change below it not
59
+ yet in it. So a child's target_lag can't be shorter than its parent's, and is refused if it is.
60
+ mason wait <table> waits until a table holds every change synced when the wait began.
61
+
62
+ WHAT IS REFUSED, WITH THE WAY OUT
63
+ A file with a mistake, or SQL Mason cannot store, is refused by file, key and reason. Nothing else stops, and a
64
+ table already built keeps serving and being re-run from what it was built with. A table has no session, so SQL that
65
+ reads a time zone or a setting is refused: name the zone (t AT TIME ZONE 'UTC', date_trunc('day', t, 'UTC')).
66
+ A daily table in one zone doesn't serve a dashboard in another.
67
+ A numeric column is stored exactly, with Postgres's digits, wherever its types fix the scale: one passed through,
68
+ its min or max, a sum (of a bigint too), and + - * of those, negated, abs, mod, round(x, n) and trunc(x, n) (n
69
+ below 0 too), floor, ceil, a cast to numeric(p,s) (of a JSON number's text too), and a CASE, GREATEST, LEAST or
70
+ COALESCE whose values have one scale, up to 18 decimals; a sign is stored too. Refused, each with its reason and
71
+ the way out: a division (avg is one: store it as a double, or divide where you read), a variance or stddev, which
72
+ Postgres computes exactly at decimals it picks per value, rounded, cast to numeric(p,s) or not (store it as a
73
+ double), a sqrt, exp, ln, log or power, whose decimals Postgres picks per value, rounded, cast to numeric(p,s) or
74
+ not (store it as a double), whatever the decimals of what it is computed from, a JSON number's text
75
+ cast to numeric with no scale (cast it to numeric(p,s)), more than 18 decimals, at the end or on the way
76
+ (round it, or give the values fewer decimals first), a sum whose decimals the rows decide (cast the values to one
77
+ scale first), a CASE, GREATEST, LEAST or COALESCE of values with differing decimals (cast it to numeric(p,s)), and a
78
+ numeric cast from a double (store the double it is). Each keeps its reason read back from its text (::text,
79
+ ::varchar, || '', a jsonb's ->>, a WITH column), cast to numeric(p,s) or not; only a stored column's or a JSON
80
+ number's text is a text's. Each is said as Postgres types the value (sqrt or avg of a
81
+ double is a double, so cast to numeric it is a cast from a double) wherever it sits in the column (under an
82
+ operator, a coalesce, a round, a cast or an aggregate, or in a scalar subquery, a WITH query, a subquery in FROM or
83
+ a UNION's arm), its way out your own SQL with the cast moved, the aggregate kept: sum(sqrt(x)::double precision),
84
+ or the WITH query, subquery or UNION the cast belongs in, given whole. A run that fails (a value
85
+ past 38 digits, bigint out of range, a division by zero) says Postgres's error and SQLSTATE, and is tried again.
86
+ An enum and a citext are stored as themselves:
87
+ an enum sorts in its declared order, a citext compares without case.
88
+ Not available yet: kind: live, and a domain or a composite type's column.
89
+
90
+ WHO READS IT
91
+ A role with tables: all reads it. A role with a list of tables reads it once it names it, as mason/roles.yml names
92
+ any table. It inherits no limits from the tables it reads: carry a tenant column and filter it in the role's where:.
93
+
94
+ SEEING IT
95
+ mason start reads the folder, says what is refused, builds what is new, and keeps looking
96
+ mason status each table's kind, method, state and lag
97
+ mason status --json the same as generatedTables, with each refusal's key, reason and options
98
+ mason logs each run: its rows and seconds, or why it failed
99
+
100
+ EXPLAIN says how a generated table is kept current on its first line, as it does for any table.
101
+ `;
102
+ }
22
103
  /**
23
104
  * THE AGENT'S PAGE (two readers: a person wants feedback and short, scannable responses; an agent wants clear,
24
105
  * unambiguous context and machine-readable formats; both want the next steps): what a
@@ -30,6 +111,7 @@ async function agentGuide() {
30
111
  // imported here, not at the top: the registry imports this file for the HELP usage
31
112
  const { STEPS } = await import("./registry.js");
32
113
  const { FAILURE_CODES } = await import("./command.js");
114
+ const { GENERATED_DIR } = await import("./generated-files.js");
33
115
  const w = Math.max(...STEPS.flatMap(([, cs]) => cs.map((c) => c.name.length))) + "mason --json".length;
34
116
  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
117
  const codes = Object.entries(FAILURE_CODES).map(([code, what]) => ` ${code.padEnd(12)} ${what}`).join("\n");
@@ -37,11 +119,12 @@ async function agentGuide() {
37
119
 
38
120
  Every command prints for a person, or with --json one JSON object for a program. One prints text alone: mason help
39
121
  (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
122
+ stderr; mason logs --json is the log's entries ({ at, text }), or with --requests the request log, --events the sync's
123
+ events, --running the statements running now. Nothing a command prints
41
124
  refers to a file this package does not ship.
42
125
 
43
126
  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
127
+ its tools rather than through a shell: tables (each synced table's rows and how current it is), describe, query (rows with
45
128
  their Postgres types, or the refusal with what to write instead) and explain. In the project's folder, Mason running:
46
129
  mason user add claude --json its url, printed once
47
130
  claude mcp add mason --env MASON_URL="<url>" -- mason mcp
@@ -53,7 +136,7 @@ ${commands}
53
136
 
54
137
  EVERY ANSWER is an object with next: [{ command, what }] — the commands to run next, the same a person reads under
55
138
  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
139
+ ({ tables: […], next }), never bare. mason wait --json blocks until every added table is synced and kept current, then
57
140
  answers once (exit 0), or with error beside the state (exit 1 failed, 124 timed out: nothing stopped, run it again).
58
141
 
59
142
  A FAILURE is { "error": { "code", "message", "why", "fix" }, "next": […] } on stdout ("why" and "fix" where there is
@@ -61,7 +144,7 @@ ${commands}
61
144
  0 done
62
145
  1 failed, with the reason
63
146
  2 the command line is wrong (without --json the usage follows)
64
- 124 ran out of time: mason wait
147
+ 124 ran out of time: mason wait, mason deploy, mason replica create (nothing stopped)
65
148
  The codes:
66
149
  ${codes}
67
150
 
@@ -72,31 +155,51 @@ ${codes}
72
155
  Where the Postgres API listens is in mason status --json (postgres.url, host, port); never read .mason/config.json
73
156
  for it. A failure that is a bug in mason says so; MASON_DEBUG=1 adds where it happened.
74
157
 
75
- THE SOURCES FILE, mason/sources/<name>.yml (mason init writes it; one per project for now), is what Mason copies:
158
+ THE SOURCES FILE, mason/sources/<name>.yml (mason init writes it; one per project for now), is what Mason syncs:
76
159
  kind: postgres
77
160
  url: \${DATABASE_URL} a variable from .env.local, .env or the shell; a literal URL is allowed for local dev
161
+ database: production optional: the database name a tool connects with (none: mason), to keep the name it has
162
+ sync: off how each table is kept current after its first sync: off (the default; mason sync
163
+ <table> reads it again), poll <interval> (poll 5s, poll 5m, poll 1h; 1s to 24h) or
164
+ replication (the change log: a primary key, wal_level = logical)
165
+ publication: \${PG_PUBLICATION} optional: the publication replication reads, its owner's, never altered
166
+ (publication: null: Mason's own, past one an earlier start recorded)
78
167
  tables:
79
168
  orders: a bare name is in public; billing.invoices names its schema
80
- columns: [id, status] these columns (the primary key is always copied)
169
+ columns: [id, status] these columns (the primary key is always synced)
170
+ sync: poll 30s this table's own: polled every 30 seconds (mason add orders --sync "poll 30s")
81
171
  customers: {} every column
82
172
  events:
83
173
  exclude: [raw_body] every column but these
84
174
  sessions:
85
- key: [tenant_id, id] only for a table with no primary key: the unique columns to copy it by (polling only)
175
+ key: [tenant_id, id] only for a table with no primary key: the unique columns to key it by (polling only)
86
176
  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
177
  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.
178
+ remove). Column types are read from the database at every start. A change of a table's sync: keeps its synced data and
179
+ catches it up by one poll. mason status --json says each table's kept: { sync, syncedAt, nextAt } — how old its
180
+ synced data is; a table synced days ago answers today's count(*) as of then.
89
181
 
90
182
  A FIRST SESSION, as a shell runs it:
91
183
  mason doctor --json && mason init --json
92
184
  mason tables --json choose the tables (rows, key, whether one can be added)
93
185
  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
186
+ mason start --json && mason wait --json Mason in the background; until every added table is synced
187
+ mason sync orders --json read what changed in it since (a table whose sync is off: only so)
95
188
  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
189
+ mason status --json each synced table, and the last day's slow, refused and unsupported statements
190
+ 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
191
+ its zone):
192
+ mason status --json recentErrors: the sync's latest failures and the statements latest refused, failed or cancelled
193
+ mason logs --requests --failed --json every statement a tool sent that was not answered, with its SQLSTATE and message
194
+ mason logs --events --failed --json the sync's failures: a first sync, a poll, the stream, the database out of reach, a restart
195
+ mason logs --running --json the statements running now, one per session
196
+ mason cancel <session> --json stops that session's statement, as a client's cancel does (its client gets a "canceling statement" error)
197
+ 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
198
  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
199
+ hosts itself (${DEMO_TABLES.join(", ")}), changed every few seconds, polled every 5 s (its sync: poll 5s). Init adds
99
200
  ${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.
201
+ It also writes a generated table, ${DEMO_GENERATED.table} (mason/${GENERATED_DIR}/${DEMO_GENERATED.file}: orders and revenue per
202
+ day and status, over the synced orders): mason wait ${DEMO_GENERATED.table} --json, after mason start.
100
203
  `;
101
204
  }
102
205
  const REPO = "https://github.com/embeddable-hq/mason-data/blob/main";
@@ -106,50 +209,86 @@ const REPO = "https://github.com/embeddable-hq/mason-data/blob/main";
106
209
  */
107
210
  export function productionGuide() {
108
211
  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.
212
+ 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
213
 
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
214
+ 1. PREPARE THE DATABASE. Mason only reads it, as a user that can SELECT the tables. How each table is kept current
215
+ after its first sync is the sources file's sync:, committed with it, so the server runs what you ran — at the top
216
+ for every table, and on an entry for that one:
217
+ sync: off the file's default for every table
218
+ tables:
219
+ orders: { sync: replication } this one from the change log, within seconds
220
+ customers: { sync: poll 5m } this one polled every 5 minutes
221
+ events: {} off, as the top says
222
+ off (the default mason init writes): after the first sync Mason reads the table again only when you run mason sync
223
+ <table> (before a report, from cron, at the end of a deploy) — nothing Mason does to your database is a hidden
224
+ default. poll <interval> (1s to 24h): a SELECT that scans the table whole, at that interval, stretched to four
225
+ 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
226
+ read replica. replication: the change log, through one slot and one publication for the project; it needs
227
+ wal_level = logical, a user with REPLICATION (or rds_replication), a primary key (a key: table is polled), and
228
+ either a publication you name (publication: \${PG_PUBLICATION} at the top of the sources file, read as it is, never
229
+ altered: it must carry every replicated table; a read replica needs one) or the right to create Mason's own, which
230
+ carries the replicated tables alone. The slot holds the database's WAL while Mason is down, so cap it on the
231
+ database with max_slot_wal_keep_size (Postgres 13+); it is dropped, verified gone, when no table's sync is
232
+ replication any more, and at mason destroy. The flags --replication, --poll and --publication of mason start, and
233
+ MASON_CHANGES, are gone: each answers with the line to write in the sources file. A project that streams, under a
234
+ file with no sync: anywhere, is refused rather than dropped (sync: replication keeps the slot; sync: off or
235
+ sync: poll <interval> confirms the drop), and so is one with a recorded publication that nothing names now
236
+ (publication: <name>, or publication: null for Mason's own, with PG_PUBLICATION unset). Try it locally first:
237
+ mason add orders --sync replication that table from the change log, as the server reads it
238
+ mason sync customers any table read again now, whatever its sync:
118
239
 
119
240
  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)
241
+ DATABASE_URL the database to sync from (the variable the sources file's url: names)
121
242
  MASON_API_KEY what the users' passwords are made with (openssl rand -hex 24)
122
243
  MASON_PROJECT your mason/ folder, mounted (default ./mason)
123
- MASON_DATA the volume whose .mason/ holds the copy and the state (default .)
244
+ MASON_PROJECT_READONLY=1 when that folder is deployed as committed: every edit beside the server is refused
245
+ MASON_DATA the volume whose .mason/ holds the synced data and the state (default .)
124
246
  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
247
  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
248
+ Docker Compose: ${REPO}/deploy/mason-data/README.md. One container per volume, never two: the synced data lets one process
127
249
  write its file, so run one replica, with no autoscaling, and stop the old container before the new one starts.
250
+ THE WEB PAGE stays on the server's loopback, so publish nothing for it: mason ui --ssh <user@server> on your machine
251
+ opens it through your own ssh (--mason "docker exec -i <container> mason" for a container), until Ctrl-C.
128
252
  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
131
- 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
253
+ about 15 seconds before that, then syncs the tables. GET /metrics on the same port is for Prometheus: numbers only
254
+ (tables by phase, lag, ages, sizes, statements), never a table's or a user's name, so it needs no secret.
255
+ MEMORY: by default the engine may use ${DEFAULT_MEMORY_RULE};
256
+ the rest is the page cache, Node and the catalog (~820 MB).
257
+ Give it ${memorySize(MEMORY_FLOOR_BYTES)} or more (Mason itself was measured using up to 2.3 GiB beside the engine, and the kernel
258
+ killed it in less): under it, with MASON_DUCKDB_MEMORY unset, a start goes on and warns, in the log, mason status and
259
+ mason doctor, that the system may stop it. Set, it is your word at any size, and mason doctor says what it leaves. Give the container enough that the page cache holds the
260
+ 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
261
  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
- To set it, MASON_DUCKDB_MEMORY=4GB with the container's limit about twice that. A statement past it fails alone
135
- (53200) and the run goes on; a value near the container's limit is an OOM kill.
262
+ To set it, MASON_DUCKDB_MEMORY=4GB with the container's limit about twice that, never under it plus ${memorySize(OUTSIDE_RESERVE_BYTES)}.
263
+ A statement past it fails alone (53200) and the run goes on; a value near the container's limit is an OOM kill.
264
+ What spills past memory takes at most half of what is free on the disk (MASON_SPILL_LIMIT sets it); a statement past that fails alone (53400).
136
265
  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
266
+ 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
267
  readers take about a core each while it runs (MASON_COPY_PARALLEL, default 3). The image is arm64 (the measured
139
268
  one) and amd64. Which settings to decide: ${REPO}/deploy/mason-data/README.md#settings
140
269
 
141
270
  3. CONNECT THE TOOLS: beside the server (docker compose exec mason …), a user is a password a tool keeps:
142
271
  mason user add metabase its URL and password, printed once
272
+ mason user add agent --role agent a user held to a role of mason/roles.yml: the tables and columns it reads
273
+ mason user add acme --role tenant --set tenant=42 and the rows: a role whose where: reads :tenant
143
274
  mason user list
144
275
  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.
276
+ The tool connects to the Postgres API (5433) as to a read replica, each added table a table, in the database
277
+ mason: a tool moving from your Postgres changes its host, port and database name, or database: <name> in the sources
278
+ file keeps its name (MASON_PG_DATABASE on a server whose sources file has no such line). Changing
279
+ MASON_API_KEY removes every user. mason/roles.yml ships with mason/ (mason help user shows the file): a user with
280
+ a role is refused anything past it as Postgres refuses it (42501), and information_schema and mason mcp list it only
281
+ what it may read; a user added without --role reads as the file's default: (mason init writes default:
282
+ everything, tables: all — every synced table), and nothing when the file names no default. A role's where: and
283
+ through: filter its tables' rows by the values each user carries (--set), every read of them filtered; a user
284
+ missing a value is refused at every statement, never answered with no rows. A roles file from before this release
285
+ names no default: write default: <role> at its top before upgrading, or its roleless users read nothing.
147
286
 
148
- 4. CHANGE IT: ship mason/ with your release and restart the container; a table added or removed is copied or dropped
287
+ 4. CHANGE IT: ship mason/ with your release and restart the container; a table added or removed is synced or dropped
149
288
  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
289
+ mason add public.orders synced at once, added to the mounted mason/sources/
290
+ mason remove orders taken out of the sources file, its synced data dropped
291
+ mason status each synced table, the statements, the recent errors
153
292
  Upgrade: read the release notes, then move mason.yml and the image together. Removing Mason for good in replication mode:
154
293
  SELECT pg_drop_replication_slot('mason_dev_<name>_<source>') — the slot holds your WAL (mason status names it).
155
294