@mason-data/cli 0.0.0-stage → 0.7.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 (109) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +268 -2
  3. package/dist/src/api/error.js +18 -0
  4. package/dist/src/api/postgres/catalog-worker.js +92 -0
  5. package/dist/src/api/postgres/catalog.js +477 -0
  6. package/dist/src/api/postgres/encode.js +541 -0
  7. package/dist/src/api/postgres/explain.js +143 -0
  8. package/dist/src/api/postgres/listener.js +60 -0
  9. package/dist/src/api/postgres/parser-worker.js +58 -0
  10. package/dist/src/api/postgres/parser.js +121 -0
  11. package/dist/src/api/postgres/plain.js +33 -0
  12. package/dist/src/api/postgres/protocol.js +404 -0
  13. package/dist/src/api/postgres/schema.js +54 -0
  14. package/dist/src/api/postgres/server.js +1771 -0
  15. package/dist/src/api/postgres/translate.js +248 -0
  16. package/dist/src/api/request-log.js +50 -0
  17. package/dist/src/cdc/pg-wire.js +588 -0
  18. package/dist/src/cdc/pgoutput.js +107 -0
  19. package/dist/src/cdc/poll.js +67 -0
  20. package/dist/src/cdc/source.js +65 -0
  21. package/dist/src/cli/background.js +198 -0
  22. package/dist/src/cli/command.js +173 -0
  23. package/dist/src/cli/commands.js +1277 -0
  24. package/dist/src/cli/control.js +36 -0
  25. package/dist/src/cli/demo.js +149 -0
  26. package/dist/src/cli/dev.js +669 -0
  27. package/dist/src/cli/doctor.js +260 -0
  28. package/dist/src/cli/guides.js +158 -0
  29. package/dist/src/cli/init.js +366 -0
  30. package/dist/src/cli/machine.js +162 -0
  31. package/dist/src/cli/main.js +83 -0
  32. package/dist/src/cli/mcp.js +726 -0
  33. package/dist/src/cli/pg-tls.js +100 -0
  34. package/dist/src/cli/pg-url.js +52 -0
  35. package/dist/src/cli/pin.js +36 -0
  36. package/dist/src/cli/ports.js +118 -0
  37. package/dist/src/cli/preflight.js +347 -0
  38. package/dist/src/cli/project.js +129 -0
  39. package/dist/src/cli/readiness.js +76 -0
  40. package/dist/src/cli/registry.js +220 -0
  41. package/dist/src/cli/replication-checks.js +120 -0
  42. package/dist/src/cli/replication-mode.js +148 -0
  43. package/dist/src/cli/run-server.js +311 -0
  44. package/dist/src/cli/runs.js +120 -0
  45. package/dist/src/cli/security.js +86 -0
  46. package/dist/src/cli/self-signed-cert.js +64 -0
  47. package/dist/src/cli/serve.js +301 -0
  48. package/dist/src/cli/source-edit.js +116 -0
  49. package/dist/src/cli/sources.js +564 -0
  50. package/dist/src/cli/state.js +58 -0
  51. package/dist/src/cli/style.js +187 -0
  52. package/dist/src/cli/supervise.js +248 -0
  53. package/dist/src/cli/sync.js +94 -0
  54. package/dist/src/cli/ui.js +107 -0
  55. package/dist/src/cli/user.js +229 -0
  56. package/dist/src/cli/version.js +23 -0
  57. package/dist/src/demo/seed.js +237 -0
  58. package/dist/src/demo/source.js +655 -0
  59. package/dist/src/duck/analyze.js +113 -0
  60. package/dist/src/duck/cancel.js +49 -0
  61. package/dist/src/duck/distinct-key.js +228 -0
  62. package/dist/src/duck/executor-worker.js +268 -0
  63. package/dist/src/duck/executor.js +567 -0
  64. package/dist/src/duck/explain.js +238 -0
  65. package/dist/src/duck/functions.js +819 -0
  66. package/dist/src/duck/gaps.js +94 -0
  67. package/dist/src/duck/jsonb-order.js +175 -0
  68. package/dist/src/duck/jsonb.js +188 -0
  69. package/dist/src/duck/kept-route.js +299 -0
  70. package/dist/src/duck/kept.js +964 -0
  71. package/dist/src/duck/late.js +221 -0
  72. package/dist/src/duck/libc-order.js +190 -0
  73. package/dist/src/duck/pg-eval-worker.js +34 -0
  74. package/dist/src/duck/pg-eval.js +59 -0
  75. package/dist/src/duck/pgtree.js +378 -0
  76. package/dist/src/duck/plan.js +259 -0
  77. package/dist/src/duck/publication.js +207 -0
  78. package/dist/src/duck/requests.js +209 -0
  79. package/dist/src/duck/resolved.js +248 -0
  80. package/dist/src/duck/rewrite.js +5101 -0
  81. package/dist/src/duck/store.js +907 -0
  82. package/dist/src/duck/stream.js +518 -0
  83. package/dist/src/duck/sync.js +2929 -0
  84. package/dist/src/duck/to-char.js +482 -0
  85. package/dist/src/duck/token.js +31 -0
  86. package/dist/src/duck/types.js +427 -0
  87. package/dist/src/duck/users.js +231 -0
  88. package/dist/src/duck/verified.js +1001 -0
  89. package/dist/src/duck/views.js +150 -0
  90. package/dist/src/lib/database-url.js +74 -0
  91. package/dist/src/lib/did-you-mean.js +43 -0
  92. package/dist/src/lib/dotenv.js +72 -0
  93. package/dist/src/lib/exit-when-written.js +20 -0
  94. package/dist/src/lib/float-equal.js +69 -0
  95. package/dist/src/lib/http-json.js +22 -0
  96. package/dist/src/lib/log.js +161 -0
  97. package/dist/src/lib/pg-numeric.js +489 -0
  98. package/dist/src/lib/port.js +22 -0
  99. package/dist/src/lib/project-file.js +49 -0
  100. package/dist/src/lib/project-files.js +43 -0
  101. package/dist/src/lib/project-version.js +76 -0
  102. package/dist/src/lib/runnable.js +14 -0
  103. package/dist/src/lib/shell-word.js +4 -0
  104. package/dist/src/lib/timezone.js +237 -0
  105. package/dist/src/lib/zone-names.js +114 -0
  106. package/dist/src/web/app.css +76 -0
  107. package/dist/src/web/app.js +491 -0
  108. package/dist/src/web/index.html +44 -0
  109. package/package.json +22 -4
@@ -0,0 +1,238 @@
1
+ /** a node of DuckDB's JSON, either form: `name` (EXPLAIN) or `type` with `intermediate_rows` and `timing` (the profile).
2
+ * The profile's RESULT_COLLECTOR and EXECUTE are the result's plumbing, not the plan's: their child stands for them */
3
+ export function planNodes(json) {
4
+ const list = Array.isArray(json) ? json : [json];
5
+ return list.flatMap((n) => {
6
+ const b = n;
7
+ const type = String(b.name ?? b.type ?? "");
8
+ const children = planNodes((b.children ?? []));
9
+ if (type === "RESULT_COLLECTOR" || type === "EXECUTE" || type === "EXPLAIN_ANALYZE")
10
+ return children;
11
+ const extra = { ...(b.extra_info ?? {}) };
12
+ const est = extra["Estimated Cardinality"];
13
+ delete extra["Estimated Cardinality"];
14
+ const node = { type: type === "SEQ_SCAN" ? "TABLE_SCAN" : type, extra, estimate: typeof est === "string" && /^\d+$/.test(est) ? Number(est) : null, children };
15
+ if (typeof b.intermediate_rows === "number") {
16
+ node.actualRows = b.intermediate_rows;
17
+ node.threadMs = Number(b.timing ?? 0) * 1000 + children.reduce((s, c) => s + (c.threadMs ?? 0), 0);
18
+ }
19
+ return [node];
20
+ });
21
+ }
22
+ /** a profile DuckDB's profiler wrote (`enable_profiling = 'json'`): the statement it timed, as it names it (`query.sql`,
23
+ * the text it was given), and its plan. The name is what holds a profile to its statement (src/duck/store.ts
24
+ * `explained`): the file is the last profiled statement's on the connection, whichever that was */
25
+ export function profiled(doc) {
26
+ const d = doc;
27
+ return { sql: typeof d.query?.sql === "string" ? d.query.sql : null, plan: planNodes(d.operator ?? []) };
28
+ }
29
+ /** what EXPLAIN ANALYZE says of a statement DuckDB's profiler timed no operator of */
30
+ export const UNPROFILED = "DuckDB's profiler recorded no operator of this statement: DuckDB answered it while planning it (as it answers a count(*) of a whole table from the table's row count), so the plan is its EXPLAIN, with the rows it answered";
31
+ /** DuckDB's operator as DuckDB's own plan prints it (`HASH_GROUP_BY` is `Hash Group By`), a scan `Table Scan` as its
32
+ * profiler names it: DuckDB's EXPLAIN says SEQ_SCAN for the same operator, and Postgres's `Seq Scan` says otherwise */
33
+ export const operatorName = (type) => type.toLowerCase().split("_").map((w) => w === "n" ? "N" : w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
34
+ /** a name as Postgres writes one in a statement: bare when it is lower case letters, digits and `_`, else quoted */
35
+ const quotedName = (s) => /^[a-z_][a-z0-9_$]*$/.test(s) ? s : `"${s.replaceAll('"', '""')}"`;
36
+ /**
37
+ * A TABLE AS POSTGRES WRITES IT, each part quoted where it must be (`public.orders`, `public."Clients"`,
38
+ * `public."x; y"`): the ONE way EXPLAIN names a copy (its line, FORMAT JSON's `Mason.Copies[].Table`), which a statement
39
+ * can read back as it is (`mason mcp`'s tables and describe name tables by it too).
40
+ */
41
+ export const qualifiedName = (t) => `${quotedName(t.schema)}.${quotedName(t.name)}`;
42
+ /** DuckDB's `<file>.<schema>.<table>`, each part quoted where DuckDB quotes it (`memory.main."x; drop.t"`), as its last
43
+ * two parts: a `.` inside a quoted part is the name's */
44
+ export function duckTableName(text) {
45
+ const parts = [];
46
+ let at = 0;
47
+ while (at <= text.length) {
48
+ if (text[at] === '"') {
49
+ let out = "", end = at + 1;
50
+ for (; end < text.length; end++) {
51
+ if (text[end] === '"') {
52
+ if (text[end + 1] === '"') {
53
+ out += '"';
54
+ end++;
55
+ continue;
56
+ }
57
+ break;
58
+ }
59
+ out += text[end];
60
+ }
61
+ parts.push(out);
62
+ at = end + 2; // past the closing quote and the dot
63
+ }
64
+ else {
65
+ const dot = text.indexOf(".", at);
66
+ parts.push(text.slice(at, dot < 0 ? undefined : dot));
67
+ at = dot < 0 ? text.length + 1 : dot + 1;
68
+ }
69
+ }
70
+ return parts.length >= 2 ? { schema: parts[parts.length - 2], name: parts[parts.length - 1] } : null;
71
+ }
72
+ /** the copy a scan reads (DuckDB's `<file>.<schema>.<table>`) */
73
+ export const scannedTable = (n) => {
74
+ const t = n.extra.Table;
75
+ return typeof t === "string" ? duckTableName(t) : null;
76
+ };
77
+ export function scannedTables(nodes) {
78
+ const out = [];
79
+ const walk = (n) => {
80
+ const t = scannedTable(n);
81
+ if (t !== null && !out.some((o) => o.schema === t.schema && o.name === t.name))
82
+ out.push(t);
83
+ n.children.forEach(walk);
84
+ };
85
+ nodes.forEach(walk);
86
+ return out;
87
+ }
88
+ /** the sync's states as EXPLAIN takes them across to the executor's thread (src/api/postgres/server.ts `duckExplain`):
89
+ * its facts alone, read at the EXPLAIN */
90
+ export const copyFacts = (states) => states.map((s) => ({ source: s.source, phase: s.phase, error: s.error, ...(s.rowsSoFar !== undefined ? { rowsSoFar: s.rowsSoFar } : {}), ...(s.kept !== undefined ? { kept: s.kept } : {}) }));
91
+ /** an ISO instant as EXPLAIN says one: `2026-10-27 19:58:39 UTC` */
92
+ const utc = (iso) => `${iso.slice(0, 19).replace("T", " ")} UTC`;
93
+ const UNKNOWN = "how it is kept current is unknown: the sync has no record of it";
94
+ /**
95
+ * HOW A COPY IS KEPT CURRENT, in EXPLAIN's words, from the sync's one derivation (src/duck/sync.ts `KeptCurrent`, which
96
+ * `mason status` renders too): a copy not yet copied or whose copy failed says so first; then polled with its last
97
+ * poll, served as copied and skipped by the passes (to be copied again, caught up first, over the poll's row limit),
98
+ * copied again whole, or, in replication mode, its part in the stream, how far the stream has applied and why it is
99
+ * not running. "kept current" is said only of a copy that is (`keptCurrentBy`).
100
+ */
101
+ export function keptText(f) {
102
+ if (f === null)
103
+ return UNKNOWN;
104
+ if (f.phase === "waiting")
105
+ return "waiting for its copy to start";
106
+ if (f.phase === "copying")
107
+ return `being copied (${count(f.rowsSoFar ?? 0)} rows so far): kept current once copied`;
108
+ if (f.phase === "failed")
109
+ return `its last copy failed: ${(f.error ?? "mason logs says why").split("\n")[0]}`;
110
+ const k = f.kept;
111
+ if (k === undefined)
112
+ return UNKNOWN;
113
+ if (k.by === "poll") {
114
+ const asOf = k.polledAt === null ? "no poll recorded yet" : `as of ${utc(k.polledAt)}`;
115
+ if (k.pending !== undefined)
116
+ return `NOT kept current (${asOf}): ${k.pending}`;
117
+ if (k.unsynced !== undefined)
118
+ return `NOT kept current (${k.unsynced}; ${asOf}): mason sync ${f.source} polls it now`;
119
+ return `kept current by polling, ${k.polledAt === null ? "no poll recorded yet" : `last poll ${utc(k.polledAt)}`}`;
120
+ }
121
+ if (k.by === "recopy")
122
+ return `kept current by copying it again whole, ${k.copiedAt === null ? "no copy recorded yet" : `last copied ${utc(k.copiedAt)}`}`;
123
+ const stream = `logical replication (slot ${k.slot})`;
124
+ const where = `applied through ${k.position}, ${k.lastCommit === null ? "no change applied this run" : `last change committed ${utc(k.lastCommit)}`}`;
125
+ if (k.state === "streamed")
126
+ return k.notRunning === null ? `kept current by ${stream}, ${where}`
127
+ : `streamed by ${stream}, ${where}; NOT kept current now, the stream is not running: ${k.notRunning}`;
128
+ return k.state === "left-out" ? `NOT kept current: left out of ${stream}: ${k.why ?? ""}`
129
+ : `${stream}, not streamed yet: ${k.why ?? ""}`;
130
+ }
131
+ /** what keeps a copy current NOW, as FORMAT JSON's `Kept Current By` says it: null for a copy nothing does (not copied
132
+ * yet, skipped by the passes, over the poll's row limit, not streamed, the stream down), so a reader keying on it is
133
+ * never told of a mode that is not at work on it */
134
+ export function keptCurrentBy(f) {
135
+ const k = f?.phase === "copied" ? f.kept : undefined;
136
+ if (k === undefined)
137
+ return null;
138
+ if (k.by === "poll")
139
+ return k.pending === undefined && k.unsynced === undefined ? "polling" : null;
140
+ if (k.by === "recopy")
141
+ return "copying again";
142
+ return k.state === "streamed" && k.notRunning === null ? "logical replication" : null;
143
+ }
144
+ /** how a copy is kept current, as FORMAT JSON's keys say it (`keptText`'s facts) */
145
+ function keptJson(f) {
146
+ const k = f?.phase === "copied" ? f.kept : undefined;
147
+ return {
148
+ "Kept Current By": keptCurrentBy(f), "Kept Current": keptText(f),
149
+ ...(k?.by === "poll" ? { "Last Poll": k.polledAt } : {}),
150
+ ...(k?.by === "replication" ? { Slot: k.slot, "Stream State": k.state, "Applied Through": k.position, "Last Change Committed": k.lastCommit, "Stream Not Running": k.notRunning } : {}),
151
+ };
152
+ }
153
+ const ms = (x) => x.toFixed(3);
154
+ const count = (n) => n.toLocaleString("en-US");
155
+ const detail = (v) => Array.isArray(v) ? v.join(", ") : v;
156
+ /** the details a plan line carries without VERBOSE: everything but the projections and filters, which VERBOSE adds */
157
+ const VERBOSE_ONLY = new Set(["Projections", "Filters", "Table", "Type"]);
158
+ function copyLine(c) {
159
+ const rows = c.rows === null ? "rows unknown" : `about ${count(c.rows)} rows`;
160
+ return `${c.table} (${rows}; ${keptText(c.facts)})`;
161
+ }
162
+ /** EXPLAIN as Postgres's text format: one line a row */
163
+ export function explainText(e, o) {
164
+ const lines = [];
165
+ lines.push(e.copies.length === 0 ? "Mason: answered by DuckDB, reading none of its copies"
166
+ : e.copies.length === 1 ? `Mason: answered by DuckDB from its copy of ${copyLine(e.copies[0])}` : "Mason: answered by DuckDB from its copies of");
167
+ if (e.copies.length > 1)
168
+ for (const c of e.copies)
169
+ lines.push(` ${copyLine(c)}`);
170
+ lines.push(` Sent to DuckDB: ${e.sql}`);
171
+ lines.push(` Computed by Mason after DuckDB: ${e.computed.length === 0 ? "nothing" : e.computed.join("; ")}`);
172
+ if (e.functions.length > 0)
173
+ lines.push(` Mason's functions DuckDB calls: ${e.functions.join(", ")}`);
174
+ for (const name of o.ignored)
175
+ lines.push(` Note: ${name} has no DuckDB meaning: ignored`);
176
+ if (e.analyzed?.profiled === false)
177
+ lines.push(` Note: ${UNPROFILED}`);
178
+ else if (e.analyzed !== undefined && o.timing)
179
+ lines.push(" Note: an operator's thread time is DuckDB's, summed over the threads that ran it and the operators under it: a parallel one's exceeds the Execution Time");
180
+ const walk = (n, depth) => {
181
+ const lead = depth === 0 ? "" : `${" ".repeat(6 * depth - 4)}-> `;
182
+ const on = scannedTable(n);
183
+ const est = o.costs && n.estimate !== null ? ` (rows=${n.estimate})` : "";
184
+ const actual = n.actualRows === undefined ? "" : ` (actual rows=${n.actualRows}${o.timing && n.threadMs !== undefined ? `, thread time=${ms(n.threadMs)} ms` : ""})`;
185
+ lines.push(`${lead}${operatorName(n.type)}${on === null ? "" : ` on ${qualifiedName(on)}`}${est}${actual}`);
186
+ const pad = " ".repeat(depth === 0 ? 2 : 6 * depth + 2);
187
+ for (const [k, v] of Object.entries(n.extra))
188
+ if (o.verbose ? k !== "Table" : !VERBOSE_ONLY.has(k))
189
+ lines.push(`${pad}${k}: ${detail(v)}`);
190
+ for (const c of n.children)
191
+ walk(c, depth + 1);
192
+ };
193
+ e.plan.forEach((n) => walk(n, 0));
194
+ if (e.analyzed !== undefined && o.summary) {
195
+ const p = e.analyzed.planning, x = e.analyzed.execution;
196
+ lines.push(`Planning Time: ${ms(p.analysisMs + p.rewriteMs + p.duckdbMs)} ms (Postgres's analysis ${ms(p.analysisMs)}, Mason's rewrite ${ms(p.rewriteMs)}, DuckDB's planning ${ms(p.duckdbMs)})`);
197
+ lines.push(`Execution Time: ${ms(x.duckdbMs + x.masonMs)} ms (DuckDB ${ms(x.duckdbMs)}, Mason's conversion and exact numerics ${ms(x.masonMs)})`);
198
+ }
199
+ return lines;
200
+ }
201
+ /** EXPLAIN as Postgres's JSON format: `[{ "Plan": { "Node Type", "Plans", "Plan Rows", "Actual Rows" … } }]` where the
202
+ * meaning is Postgres's, DuckDB's operator names, and what is DuckDB's own in keys of their own — its thread time is
203
+ * `DuckDB Thread Time`, never `Actual Total Time`, which a plan viewer draws as elapsed */
204
+ export function explainJson(e, o) {
205
+ const node = (n) => {
206
+ const on = scannedTable(n);
207
+ return {
208
+ "Node Type": operatorName(n.type),
209
+ ...(on === null ? {} : { "Relation Name": on.name, Schema: on.schema }),
210
+ ...(o.costs && n.estimate !== null ? { "Plan Rows": n.estimate } : {}),
211
+ ...(n.actualRows === undefined ? {} : { "Actual Rows": n.actualRows, "Actual Loops": 1, ...(o.timing && n.threadMs !== undefined ? { "DuckDB Thread Time": Number(ms(n.threadMs)) } : {}) }),
212
+ ...Object.fromEntries(Object.entries(n.extra).filter(([k]) => o.verbose ? k !== "Table" : !VERBOSE_ONLY.has(k)).map(([k, v]) => [`DuckDB ${k}`, v])),
213
+ ...(n.children.length === 0 ? {} : { Plans: n.children.map(node) }),
214
+ };
215
+ };
216
+ // Postgres's plan has one root; DuckDB's has one too (a statement's), kept as a list should it ever have more
217
+ const roots = e.plan.map(node);
218
+ const top = {
219
+ Plan: roots.length === 1 ? roots[0] : { "Node Type": "DuckDB Plans", Plans: roots },
220
+ Mason: {
221
+ "Answered By": "DuckDB",
222
+ Copies: e.copies.map((c) => ({ Table: c.table, "Estimated Rows": c.rows, ...keptJson(c.facts) })),
223
+ "Sent To DuckDB": e.sql,
224
+ "Computed After DuckDB": e.computed,
225
+ "Mason Functions DuckDB Calls": e.functions,
226
+ ...(o.ignored.length === 0 ? {} : { Ignored: o.ignored.map((n) => `${n} has no DuckDB meaning`) }),
227
+ ...(e.analyzed?.profiled === false ? { Note: UNPROFILED } : {}),
228
+ },
229
+ };
230
+ if (e.analyzed !== undefined && o.summary) {
231
+ const p = e.analyzed.planning, x = e.analyzed.execution;
232
+ top["Planning Time"] = Number(ms(p.analysisMs + p.rewriteMs + p.duckdbMs));
233
+ top["Execution Time"] = Number(ms(x.duckdbMs + x.masonMs));
234
+ top.Mason["Planning"] = { "Postgres Analysis": Number(ms(p.analysisMs)), "Mason Rewrite": Number(ms(p.rewriteMs)), "DuckDB Planning": Number(ms(p.duckdbMs)) };
235
+ top.Mason["Execution"] = { DuckDB: Number(ms(x.duckdbMs)), "Mason Conversion": Number(ms(x.masonMs)) };
236
+ }
237
+ return JSON.stringify([top], null, 2);
238
+ }