@mason-data/cli 0.8.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 (109) hide show
  1. package/README.md +13 -4
  2. package/dist/src/api/postgres/catalog-worker.js +49 -2
  3. package/dist/src/api/postgres/catalog.js +166 -32
  4. package/dist/src/api/postgres/listener.js +11 -3
  5. package/dist/src/api/postgres/proxy.js +283 -0
  6. package/dist/src/api/postgres/schema.js +6 -0
  7. package/dist/src/api/postgres/server.js +505 -99
  8. package/dist/src/api/postgres/translate.js +47 -2
  9. package/dist/src/cli/background.js +18 -19
  10. package/dist/src/cli/bridge-stdin.js +101 -0
  11. package/dist/src/cli/command.js +169 -11
  12. package/dist/src/cli/commands.js +207 -51
  13. package/dist/src/cli/credentials.js +182 -0
  14. package/dist/src/cli/demo.js +2 -2
  15. package/dist/src/cli/deploy.js +574 -0
  16. package/dist/src/cli/dev.js +148 -27
  17. package/dist/src/cli/doctor.js +116 -33
  18. package/dist/src/cli/generated-files.js +143 -0
  19. package/dist/src/cli/guides.js +97 -6
  20. package/dist/src/cli/hosted.js +850 -0
  21. package/dist/src/cli/init.js +47 -11
  22. package/dist/src/cli/login.js +289 -0
  23. package/dist/src/cli/machine.js +1 -1
  24. package/dist/src/cli/main.js +8 -2
  25. package/dist/src/cli/mason.cjs +17 -0
  26. package/dist/src/cli/mcp.js +2 -2
  27. package/dist/src/cli/output-gate.js +176 -0
  28. package/dist/src/cli/page-share.js +96 -0
  29. package/dist/src/cli/pg-tls.js +1 -1
  30. package/dist/src/cli/pin.js +6 -2
  31. package/dist/src/cli/pool-size.cjs +85 -0
  32. package/dist/src/cli/ports.js +2 -0
  33. package/dist/src/cli/preflight.js +130 -39
  34. package/dist/src/cli/project.js +99 -0
  35. package/dist/src/cli/readiness.js +42 -6
  36. package/dist/src/cli/registry.js +12 -4
  37. package/dist/src/cli/replica-hosted.js +665 -0
  38. package/dist/src/cli/replica.js +76 -0
  39. package/dist/src/cli/roles.js +2 -1
  40. package/dist/src/cli/run-server.js +154 -7
  41. package/dist/src/cli/security.js +57 -10
  42. package/dist/src/cli/serve.js +63 -7
  43. package/dist/src/cli/share-run.js +51 -8
  44. package/dist/src/cli/share.js +3 -3
  45. package/dist/src/cli/source-edit.js +10 -4
  46. package/dist/src/cli/source-role.js +344 -0
  47. package/dist/src/cli/sources.js +30 -20
  48. package/dist/src/cli/style.js +2 -1
  49. package/dist/src/cli/supervise.js +15 -23
  50. package/dist/src/cli/token-hosted.js +337 -0
  51. package/dist/src/cli/ui-ssh.js +460 -0
  52. package/dist/src/cli/ui.js +115 -11
  53. package/dist/src/cli/user-hosted.js +299 -0
  54. package/dist/src/cli/user.js +126 -74
  55. package/dist/src/demo/seed.js +26 -0
  56. package/dist/src/duck/analyze.js +36 -20
  57. package/dist/src/duck/collation.js +266 -0
  58. package/dist/src/duck/constant-key.js +94 -0
  59. package/dist/src/duck/distinct-key.js +283 -26
  60. package/dist/src/duck/events.js +39 -8
  61. package/dist/src/duck/executor-worker.js +2 -2
  62. package/dist/src/duck/executor.js +6 -5
  63. package/dist/src/duck/explain.js +4 -0
  64. package/dist/src/duck/functions.js +448 -49
  65. package/dist/src/duck/gaps.js +34 -16
  66. package/dist/src/duck/generated-plan.js +1070 -0
  67. package/dist/src/duck/generated.js +906 -0
  68. package/dist/src/duck/grants.js +1 -1
  69. package/dist/src/duck/held-types.js +64 -0
  70. package/dist/src/duck/history.js +4 -0
  71. package/dist/src/duck/jsonb-order.js +9 -12
  72. package/dist/src/duck/kept-route.js +6 -5
  73. package/dist/src/duck/kept.js +9 -7
  74. package/dist/src/duck/late.js +1469 -40
  75. package/dist/src/duck/libc-bound.js +341 -0
  76. package/dist/src/duck/libc-order.js +892 -126
  77. package/dist/src/duck/pg-eval-worker.js +7 -1
  78. package/dist/src/duck/pg-eval.js +6 -4
  79. package/dist/src/duck/pgtree.js +774 -23
  80. package/dist/src/duck/plan.js +275 -30
  81. package/dist/src/duck/publication.js +14 -0
  82. package/dist/src/duck/requests.js +9 -5
  83. package/dist/src/duck/resolved.js +117 -2
  84. package/dist/src/duck/rewrite.js +4567 -583
  85. package/dist/src/duck/row-filter.js +12 -6
  86. package/dist/src/duck/statement.js +19 -0
  87. package/dist/src/duck/store.js +294 -68
  88. package/dist/src/duck/stream.js +9 -5
  89. package/dist/src/duck/sync.js +602 -126
  90. package/dist/src/duck/token.js +4 -1
  91. package/dist/src/duck/types.js +63 -7
  92. package/dist/src/duck/usage.js +6 -4
  93. package/dist/src/duck/users.js +5 -0
  94. package/dist/src/duck/verified.js +159 -2
  95. package/dist/src/duck/views.js +4 -3
  96. package/dist/src/lib/database-url.js +55 -10
  97. package/dist/src/lib/instant.js +50 -0
  98. package/dist/src/lib/log-buffer.js +13 -4
  99. package/dist/src/lib/log-read.js +196 -0
  100. package/dist/src/lib/pg-numeric.js +387 -49
  101. package/dist/src/lib/secret-mask.js +142 -0
  102. package/dist/src/lib/table-sync.js +24 -9
  103. package/dist/src/web/app.js +9 -4
  104. package/dist/src/web/pages/status.js +178 -13
  105. package/dist/src/web/pages/syncs.js +1 -1
  106. package/dist/src/web/pages/tables.js +8 -3
  107. package/dist/src/web/pages/users.js +4 -3
  108. package/dist/src/web/shared.js +19 -3
  109. package/package.json +8 -7
@@ -14,11 +14,14 @@ export function mintToken(apiKey, seconds, now = Date.now()) {
14
14
  const expiry = Math.floor(now / 1000) + seconds;
15
15
  return { token: `${PREFIX}_${expiry}_${mac(apiKey, expiry)}`, expiresAt: new Date(expiry * 1000).toISOString() };
16
16
  }
17
+ const TOKEN_RE = /^mason_(\d{1,12})_([A-Za-z0-9_-]{43})$/;
18
+ /** Whether a password is a token by its shape, good or not. */
19
+ export const isToken = (password) => TOKEN_RE.test(password);
17
20
  /** null when the token is good; else why not, in a sentence a person reads after "password authentication failed" */
18
21
  export function tokenProblem(apiKey, token, now = Date.now()) {
19
22
  if (token === apiKey)
20
23
  return "the API key is not a password here: mason user add <name> makes a user whose password does not expire";
21
- const m = /^mason_(\d{1,12})_([A-Za-z0-9_-]{43})$/.exec(token);
24
+ const m = TOKEN_RE.exec(token);
22
25
  if (m === null)
23
26
  return "not a Mason user's password or token: mason user add <name> makes a user whose password does not expire";
24
27
  const expiry = Number(m[1]);
@@ -11,9 +11,11 @@
11
11
  * UNION as its text. Each column that diverges carries the reason, and the runner logs it once per statement.
12
12
  */
13
13
  import { Buffer } from "node:buffer";
14
- import { DuckDBTypeId, DuckDBDateValue, DuckDBTimestampValue, DuckDBTimestampTZValue, DuckDBDecimalValue, DuckDBUUIDValue, DuckDBListValue, DuckDBArrayValue, DuckDBIntervalValue, DuckDBBlobValue, DuckDBTimeValue, DuckDBListType, DuckDBArrayType, DuckDBTimestampSecondsValue, DuckDBTimestampMillisecondsValue, DuckDBTimestampNanosecondsValue, } from "@duckdb/node-api";
14
+ import { DuckDBTypeId, DuckDBDateValue, DuckDBTimestampValue, DuckDBTimestampTZValue, DuckDBDecimalValue, DuckDBUUIDValue, DuckDBListValue, DuckDBArrayValue, DuckDBIntervalValue, DuckDBBlobValue, DuckDBTimeValue, DuckDBListType, DuckDBArrayType, DuckDBTimeTZValue, DuckDBTimestampSecondsValue, DuckDBTimestampMillisecondsValue, DuckDBTimestampNanosecondsValue, } from "@duckdb/node-api";
15
15
  import { jsonbText } from "./jsonb.js";
16
16
  import { gapMark } from "./gaps.js";
17
+ import { heldType, ZONED_HELD } from "./held-types.js";
18
+ import { pgEvalSync } from "./pg-eval.js";
17
19
  import { EncodeError, encodeValue, OID, TIMESTAMP } from "../api/postgres/encode.js";
18
20
  const MICROS_PER_DAY = 86400000000n;
19
21
  const DATE_INFINITY = 2_147_483_647, DATE_NEG_INFINITY = -2_147_483_647;
@@ -137,6 +139,12 @@ function timeText(micros) {
137
139
  const secs = micros / 1000000n, frac = micros % 1000000n;
138
140
  return `${pad(secs / 3600n)}:${pad((secs % 3600n) / 60n)}:${pad(secs % 60n)}${frac === 0n ? "" : `.${pad(frac, 6).replace(/0+$/, "")}`}`;
139
141
  }
142
+ /** A timetz as timetz_out writes it: the time, then its offset as `+HH`, `+HH:MM` or `+HH:MM:SS` (DuckDB's offset, as
143
+ * Postgres writes it, is east of UTC; its stored zone is west) */
144
+ export function timetzText(v) {
145
+ const abs = Math.abs(v.offset), s = abs % 60, m = Math.trunc(abs / 60) % 60;
146
+ return `${timeText(v.micros)}${v.offset < 0 ? "-" : "+"}${pad(Math.trunc(abs / 3600))}${m !== 0 || s !== 0 ? `:${pad(m)}` : ""}${s !== 0 ? `:${pad(s)}` : ""}`;
147
+ }
140
148
  /** An interval as Postgres's `interval_out` writes it under IntervalStyle postgres: `1 year 2 mons 3 days 04:05:06.7` */
141
149
  export function intervalText(months, days, micros) {
142
150
  const parts = [];
@@ -213,7 +221,7 @@ function arrayElement(text) {
213
221
  /** Postgres's array OIDs of the element OIDs this file sends */
214
222
  const ARRAY_OID = {
215
223
  16: 1000, 17: 1001, 20: 1016, 21: 1005, 23: 1007, 25: 1009, 700: 1021, 701: 1022, 1082: 1182, 1083: 1183,
216
- 1114: 1115, 1184: 1185, 1186: 1187, 1700: 1231, 2950: 2951, 3802: 3807,
224
+ 1114: 1115, 1184: 1185, 1186: 1187, 1266: 1270, 1700: 1231, 2950: 2951, 3802: 3807,
217
225
  };
218
226
  function custom(name, oid, size, text, binary, divergence = null) {
219
227
  return {
@@ -285,6 +293,8 @@ export function wireColumn(name, t) {
285
293
  case DuckDBTypeId.TIMESTAMP_TZ: return timestampColumn(name, true);
286
294
  case DuckDBTypeId.TIME:
287
295
  return custom(name, 1083, 8, (v) => timeText(v.micros), (v) => { const b = Buffer.alloc(8); b.writeBigInt64BE(v.micros); return b; });
296
+ case DuckDBTypeId.TIME_TZ:
297
+ return custom(name, 1266, 12, (v) => timetzText(v), (v) => { const t = v; const b = Buffer.alloc(12); b.writeBigInt64BE(t.micros, 0); b.writeInt32BE(-t.offset, 8); return b; });
288
298
  case DuckDBTypeId.INTERVAL:
289
299
  return custom(name, 1186, 16, (v) => { const i = v; return intervalText(i.months, i.days, i.micros); }, (v) => { const i = v; const b = Buffer.alloc(16); b.writeBigInt64BE(i.micros, 0); b.writeInt32BE(i.days, 8); b.writeInt32BE(i.months, 12); return b; });
290
300
  case DuckDBTypeId.BLOB:
@@ -313,7 +323,7 @@ export function wireColumn(name, t) {
313
323
  // int4, its DECIMAL as a numeric, its JSON as jsonb's canonical text. Where DuckDB's value cannot be written as
314
324
  // Postgres's type (a timestamp where Postgres says date), DuckDB's own encoding stands and the column says why.
315
325
  /** pg_type.typlen of the OIDs a result column may have */
316
- const TYPLEN = { 16: 1, 18: 1, 19: 64, 20: 8, 21: 2, 23: 4, 26: 4, 700: 4, 701: 8, 1082: 4, 1083: 8, 1114: 8, 1184: 8, 1186: 16, 2950: 16 };
326
+ const TYPLEN = { 16: 1, 18: 1, 19: 64, 20: 8, 21: 2, 23: 4, 26: 4, 700: 4, 701: 8, 1082: 4, 1083: 8, 1114: 8, 1184: 8, 1186: 16, 1266: 12, 2950: 16 };
317
327
  const PG_TEXT = new Set([25, 1043, 1042, 19, 18]);
318
328
  const PG_INT = { 21: "int2", 23: "int4", 20: "int8" };
319
329
  const DUCK_EXACT = new Set([DuckDBTypeId.TINYINT, DuckDBTypeId.SMALLINT, DuckDBTypeId.INTEGER, DuckDBTypeId.BIGINT, DuckDBTypeId.HUGEINT,
@@ -349,6 +359,45 @@ export function floatAsNumeric(x, digits = 15) {
349
359
  }
350
360
  return s === "-0" ? "0" : s;
351
361
  }
362
+ /** the binary forms of the text-held types a driver asks for by default (pgjdbc: point, box): each a float8's bytes */
363
+ const HELD_BINARY = {
364
+ 600: floats, 603: floats,
365
+ };
366
+ function floats(text) {
367
+ const parts = text.match(/-?(?:Infinity|NaN|[\d.]+(?:e[-+]?\d+)?)/g) ?? [];
368
+ const b = Buffer.alloc(parts.length * 8);
369
+ parts.forEach((p, i) => b.writeDoubleBE(Number(p), i * 8));
370
+ return b;
371
+ }
372
+ /** a time range's text in the session's zone: Postgres's own output, a value at a time, the last few remembered */
373
+ const zonedTexts = new Map();
374
+ function inZone(type, text, zone) {
375
+ const key = `${zone}\u0000${type}\u0000${text}`;
376
+ let written = zonedTexts.get(key);
377
+ if (written === undefined) {
378
+ written = pgEvalSync(`SELECT $1::${type}::text`, [text], zone)[0]?.[0] ?? text;
379
+ if (zonedTexts.size >= 1024)
380
+ zonedTexts.clear();
381
+ zonedTexts.set(key, written);
382
+ }
383
+ return written;
384
+ }
385
+ /** a column of a type held as its text. A value's text only: Mason writes the binary form of few of them */
386
+ function heldColumn(name, oid, own, size) {
387
+ const binary = HELD_BINARY[oid], zoned = ZONED_HELD.get(oid);
388
+ return {
389
+ name, oid, size, divergence: null,
390
+ encode: (v, format, s) => {
391
+ if (v === null)
392
+ return null;
393
+ if (format === 0)
394
+ return zoned !== undefined ? Buffer.from(inZone(zoned, str(v), s.timeZone), "utf8") : own.encode(v, 0, s);
395
+ if (binary !== undefined)
396
+ return binary(str(v));
397
+ throw new EncodeError(`column ${JSON.stringify(name)} (type OID ${oid}) is held as its text and cannot be sent in binary by Mason: ask for text (${gapMark("binary-held-type")})`, "0A000", "binary-held-type");
398
+ },
399
+ };
400
+ }
352
401
  /** The wire column of a result column whose Postgres type the analysis gave, holding DuckDB values of type `t`. */
353
402
  export function pgTypedColumn(name, oid, t, typmod = -1) {
354
403
  const own = wireColumn(name, t);
@@ -360,6 +409,16 @@ export function pgTypedColumn(name, oid, t, typmod = -1) {
360
409
  return retyped(viaApi(name, "numeric", (v) => padDecimals(exactText(v), (typmod - 4) & 0xffff)));
361
410
  if (own.oid === oid)
362
411
  return retyped(own);
412
+ // a type the copy holds as its text, or an array of one (src/duck/held-types.ts): the text, under Postgres's OID
413
+ const held = heldType(oid);
414
+ if (held !== undefined && (own.oid === 25 || own.oid === 1009))
415
+ return heldColumn(name, oid, own, held.oid === oid ? held.size : -1);
416
+ // an oid element of an oid[], held as its digits
417
+ if (oid === 26 && own.oid === 25)
418
+ return custom(name, 26, 4, str, (v) => { const b = Buffer.alloc(4); b.writeUInt32BE(Number(str(v))); return b; });
419
+ // an oid: DuckDB's UINTEGER
420
+ if (oid === 26 && DUCK_EXACT.has(t.typeId))
421
+ return custom(name, 26, 4, (v) => exactText(v), (v) => { const b = Buffer.alloc(4); b.writeUInt32BE(Number(exactText(v))); return b; });
363
422
  // an inet: stored as Postgres's text with its mask (`10.3.6.45/32`), sent as inet_out writes it (a full mask dropped)
364
423
  if (oid === 869 && own.oid === 25)
365
424
  return custom(name, 869, -1, (v) => inetOut(str(v)), (v) => inetBytes(str(v)));
@@ -404,12 +463,9 @@ export function pgTypedColumn(name, oid, t, typmod = -1) {
404
463
  }
405
464
  return { ...own, divergence: `Postgres says type OID ${oid}; the engine computed ${t.toString()}, sent as its own type` };
406
465
  }
407
- /** How a cell of an exact numeric (src/duck/rewrite.ts ExactColumn) reads as a decimal text, by DuckDB's type: an exact
408
- * number's digits, a float as Postgres's cast to numeric writes it — or null where the cell needs digits DuckDB did not
409
- * keep (a double where the program wants exact ones). */
410
466
  export function cellText(t, conv) {
411
467
  // a numeric's text as Postgres's numeric_out wrote it (a text read by numeric_in, src/duck/pg-eval.ts)
412
- if (conv === "text")
468
+ if (conv === "text" || conv === "parsed")
413
469
  return t.typeId === DuckDBTypeId.VARCHAR ? (v) => String(v) : null;
414
470
  if (DUCK_EXACT.has(t.typeId))
415
471
  return exactText;
@@ -1,5 +1,5 @@
1
1
  import { MASON_SCHEMA } from "./store.js";
2
- import { History, Streak } from "./history.js";
2
+ import { History, Streak, cutoff } from "./history.js";
3
3
  import { outcomeOf } from "../api/request-log.js";
4
4
  export const USAGE_TABLE = `${MASON_SCHEMA}.usage`;
5
5
  export const SIZES_TABLE = `${MASON_SCHEMA}.copy_sizes`;
@@ -265,10 +265,12 @@ export class UsageSampler {
265
265
  this.log(`usage: measured ${rows.length} synced tables' own size in ${Math.round(performance.now() - t0)} ms (every ${Math.round((this.options.sizeEveryMs ?? SIZE_EVERY_MS) / 60_000)} min)`);
266
266
  }
267
267
  }
268
- /** Deletes the samples and sizes older than the retention. */
268
+ /** Deletes the samples and sizes older than the retention, counted back from the module's ONE clock (`now`, which
269
+ * stamps each sample), never the engine's `now()` (src/duck/events.ts `trim` says why). */
269
270
  async trim() {
270
- await this.history.writer.run(`DELETE FROM ${USAGE_TABLE} WHERE sampled_at < now() - INTERVAL (${this.days}) DAY`);
271
- await this.history.writer.run(`DELETE FROM ${SIZES_TABLE} WHERE sampled_at < now() - INTERVAL (${this.days}) DAY`);
271
+ const before = cutoff(this.now(), this.days);
272
+ await this.history.writer.run(`DELETE FROM ${USAGE_TABLE} WHERE sampled_at < $1::TIMESTAMPTZ`, [before]);
273
+ await this.history.writer.run(`DELETE FROM ${SIZES_TABLE} WHERE sampled_at < $1::TIMESTAMPTZ`, [before]);
272
274
  }
273
275
  /**
274
276
  * The samples from `since` until `until`, oldest first; with `stepSeconds`, re-bucketed in ONE query: buckets of that
@@ -59,6 +59,8 @@ export const newUserId = () => crypto.randomBytes(8).toString("hex");
59
59
  export const userPassword = (apiKey, name, id) => `mason_${id}_${mac(apiKey, name, id)}`;
60
60
  /** Whether a password is a user's by its shape (else it is read as a token, src/duck/token.ts). */
61
61
  export const isUserPassword = (password) => PASSWORD_RE.test(password);
62
+ /** What every `mason share` password begins with (src/cli/share-run.ts `newSharePassword`): no user's has it, `share` being no hex. */
63
+ export const SHARE_PASSWORD_PREFIX = "mason_share_";
62
64
  /** Whether a user was added under this API key (one rotated since is no longer live). */
63
65
  export const underKey = (u, apiKey) => u.key === keyFingerprint(apiKey);
64
66
  /** The users the file lists; none when there is no file; why not, when it cannot be read — never an empty list then. */
@@ -263,12 +265,15 @@ export const disabledMessage = (name) => `role "${name}" is not permitted to log
263
265
  * HOW THE DUCKDB RUN'S LISTENER TAKES A PASSWORD (src/cli/dev.ts; the tests wire the same): a user's password (its shape)
264
266
  * is the users file's to judge, and its session is asked again before every statement (a stat); anything else is read as a
265
267
  * token, the kind `mason connect` minted before `mason user` (src/duck/token.ts: the API key used as a password is refused by name).
268
+ * A `mason share` password is refused by name too: it was never this port's, and making a user is not the way out.
266
269
  */
267
270
  export function listenerLogins(users, apiKey) {
268
271
  return {
269
272
  authenticate: (password, user) => {
270
273
  if (isUserPassword(password))
271
274
  return users.login(user, password);
275
+ if (password.startsWith(SHARE_PASSWORD_PREFIX))
276
+ throw new Error("a mason share password logs in only at the address mason share printed, never on this port");
272
277
  const problem = tokenProblem(apiKey, password);
273
278
  if (problem !== null)
274
279
  throw new Error(problem);