@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
@@ -1,28 +1,15 @@
1
- import { outcomeOf } from "../api/request-log.js";
1
+ import { outcomeOf, outcomeSql, statusOf } from "../api/request-log.js";
2
2
  import { MASON_SCHEMA } from "./store.js";
3
+ import { Batches, History, Streak } from "./history.js";
4
+ import { GAPS } from "./gaps.js";
5
+ import { quantileContOf } from "./usage.js";
3
6
  export const REQUESTS_TABLE = `${MASON_SCHEMA}.requests`;
4
- /** How many days of requests are kept unless MASON_REQUEST_LOG_DAYS says. */
5
- export const DEFAULT_REQUEST_LOG_DAYS = 7;
6
7
  const FLUSH_MS = 1000;
7
8
  const BATCH_ROWS = 1000;
8
- /** how often the rows past the retention are deleted (and once at open) */
9
- const TRIM_EVERY_MS = 60 * 60 * 1000;
10
- /**
11
- * The days MASON_REQUEST_LOG_DAYS keeps, or why the text is not a number of days. Unset or empty: the default.
12
- */
13
- export function requestLogDays(text) {
14
- if (text === undefined || text.trim() === "")
15
- return DEFAULT_REQUEST_LOG_DAYS;
16
- const days = Number(text);
17
- if (!Number.isInteger(days) || days < 1) {
18
- return { problem: `MASON_REQUEST_LOG_DAYS=${JSON.stringify(text)} is not a number of days: write a whole number, 1 or more (unset, ${DEFAULT_REQUEST_LOG_DAYS})` };
19
- }
20
- return days;
21
- }
22
9
  /** A request's outcome in a word, from its SQLSTATE (src/api/request-log.ts: the listener's refusal line reads it too). */
23
10
  export { outcomeOf };
24
11
  const COLUMNS = ["request_id", "started_at", "session", "application_name", "token_name", "user_name", "method", "sqlstate", "message", "detail", "hint",
25
- "rows", "duration_ms", "query", "query_truncated", "seq", "json_reads"];
12
+ "rows", "duration_ms", "query", "query_truncated", "seq", "json_reads", "bytes_sent", "gap", "role", "attributes"];
26
13
  /** the ledger's DDL: one row per request, named for a developer who knows pg_stat_activity */
27
14
  /** a ledger made before `mason user` gains its column; a column added last is named in every INSERT */
28
15
  export const REQUESTS_USER_DDL = `ALTER TABLE ${REQUESTS_TABLE} ADD COLUMN IF NOT EXISTS user_name VARCHAR`;
@@ -40,68 +27,114 @@ export const REQUESTS_SEQ_DDL = `ALTER TABLE ${REQUESTS_TABLE} ADD COLUMN IF NOT
40
27
  * `review`: read by several statements, or by a slow one, within the log's retention). A ledger made before gains it.
41
28
  */
42
29
  export const REQUESTS_JSON_DDL = `ALTER TABLE ${REQUESTS_TABLE} ADD COLUMN IF NOT EXISTS json_reads VARCHAR`;
30
+ /** THE ROLE a session read as (src/duck/grants.ts): beside `user_name`, so a refusal (42501) says who was held to what;
31
+ * a ledger made before roles gains the column, its old rows NULL (read as '') */
32
+ export const REQUESTS_ROLE_DDL = `ALTER TABLE ${REQUESTS_TABLE} ADD COLUMN IF NOT EXISTS role VARCHAR`;
33
+ /** THE USER'S ATTRIBUTES beside its role (src/duck/row-filter.ts: the values its role's row filters read), as JSON text,
34
+ * so a statement's rows can be told apart by whose values filtered them; NULL for none, and in a ledger made before */
35
+ export const REQUESTS_ATTRIBUTES_DDL = `ALTER TABLE ${REQUESTS_TABLE} ADD COLUMN IF NOT EXISTS attributes VARCHAR`;
36
+ /** THE BYTES SENT for each statement (ApiLogRow.bytesSent): what the answers cost to send. A ledger made before gains it;
37
+ * its old rows are NULL, never 0. */
38
+ export const REQUESTS_BYTES_DDL = `ALTER TABLE ${REQUESTS_TABLE} ADD COLUMN IF NOT EXISTS bytes_sent BIGINT`;
39
+ /** THE REGISTRY ENTRY THAT REFUSED EACH STATEMENT (ApiLogRow.gap, src/duck/gaps.ts): NULL for an answer and for any
40
+ * other refusal; what `unsupported` groups by. A ledger made before gains it; its old rows are NULL. */
41
+ export const REQUESTS_GAP_DDL = `ALTER TABLE ${REQUESTS_TABLE} ADD COLUMN IF NOT EXISTS gap VARCHAR`;
43
42
  export const REQUESTS_DDL = `CREATE TABLE IF NOT EXISTS ${REQUESTS_TABLE} (request_id VARCHAR, started_at TIMESTAMPTZ, session INTEGER, ` +
44
43
  `application_name VARCHAR, token_name VARCHAR, user_name VARCHAR, method VARCHAR, sqlstate VARCHAR, message VARCHAR, detail VARCHAR, hint VARCHAR, ` +
45
- `"rows" BIGINT, duration_ms INTEGER, query VARCHAR, query_truncated BOOLEAN, seq BIGINT, json_reads VARCHAR)`;
44
+ `"rows" BIGINT, duration_ms INTEGER, query VARCHAR, query_truncated BOOLEAN, seq BIGINT, json_reads VARCHAR, bytes_sent BIGINT, gap VARCHAR, role VARCHAR, attributes VARCHAR)`;
46
45
  /** an ApiLogRow's values in COLUMNS' order */
47
46
  function valuesOf({ row: r, seq }) {
48
47
  const app = (r.client ?? "").replace(/^postgres:/, "");
49
48
  return [r.requestId, r.at.toISOString(), r.session ?? null, app === "unnamed" ? "" : app, r.tokenName ?? "", r.user ?? "", r.method,
50
49
  r.sqlstate ?? null, r.error === undefined || r.error === "" ? null : r.error, r.detail ?? null, r.hint ?? null,
51
50
  r.rows === undefined ? null : r.rows, Math.round(r.durationMs), r.payload ?? "", r.payloadTruncated === true, seq,
52
- r.jsonReads === undefined || r.jsonReads.length === 0 ? null : JSON.stringify(r.jsonReads)];
51
+ r.jsonReads === undefined || r.jsonReads.length === 0 ? null : JSON.stringify(r.jsonReads), r.bytesSent === undefined ? null : r.bytesSent, r.gap ?? null, r.role ?? "",
52
+ r.attributes === undefined || Object.keys(r.attributes).length === 0 ? null : JSON.stringify(r.attributes)];
53
53
  }
54
54
  /** A ledger row as the commands read it. */
55
55
  function requestOf(o) {
56
56
  const text = (v) => v === null || v === undefined ? null : String(v);
57
57
  const sqlstate = text(o.sqlstate);
58
+ const outcome = outcomeOf(sqlstate);
59
+ const startedAt = new Date(String(o.started_at));
58
60
  return {
59
- requestId: String(o.request_id), startedAt: new Date(String(o.started_at)).toISOString(), session: o.session === null ? null : Number(o.session),
60
- applicationName: String(o.application_name ?? ""), tokenName: String(o.token_name ?? ""), user: String(o.user_name ?? ""), method: String(o.method),
61
- outcome: outcomeOf(sqlstate), sqlstate, message: text(o.message), detail: text(o.detail), hint: text(o.hint),
62
- rows: o.rows === null ? null : Number(o.rows), durationMs: Number(o.duration_ms), query: String(o.query ?? ""), queryTruncated: o.query_truncated === true,
61
+ requestId: String(o.request_id), startedAt: startedAt.toISOString(), session: o.session === null ? null : Number(o.session),
62
+ applicationName: String(o.application_name ?? ""), tokenName: String(o.token_name ?? ""), user: String(o.user_name ?? ""), role: String(o.role ?? ""),
63
+ attributes: typeof o.attributes === "string" && o.attributes !== "" ? JSON.parse(o.attributes) : null, method: String(o.method),
64
+ outcome, status: statusOf(outcome), sqlstate, message: text(o.message), detail: text(o.detail), hint: text(o.hint),
65
+ rows: o.rows === null ? null : Number(o.rows), durationMs: Number(o.duration_ms),
66
+ endedAt: new Date(startedAt.getTime() + Number(o.duration_ms)).toISOString(), bytesSent: o.bytes_sent === null || o.bytes_sent === undefined ? null : Number(o.bytes_sent),
67
+ query: String(o.query ?? ""), queryTruncated: o.query_truncated === true,
63
68
  };
64
69
  }
70
+ /**
71
+ * A position in the request log's order, as a page hands it on: the row's start, ISO 8601, and its number (`seq`; -1
72
+ * for a row recorded before rows were numbered) — `2026-10-28T10:00:00.123Z~42`. A reader passes it back as it came.
73
+ */
74
+ export const requestCursor = (at, seq) => `${at}~${seq}`;
75
+ /** A cursor read back (requestCursor), or null when the text is not one. */
76
+ export function parseRequestCursor(text) {
77
+ const m = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)~(-?\d+)$/.exec(text);
78
+ if (m === null || Number.isNaN(Date.parse(m[1])))
79
+ return null;
80
+ return { at: m[1], seq: Number(m[2]) };
81
+ }
82
+ /** how much of a statement `unsupported` shows as its example */
83
+ export const UNSUPPORTED_EXAMPLE_CHARS = 1000;
84
+ /**
85
+ * A WINDOW FOLDED FROM THE GROUPS of a scan by SQLSTATE and a completed one's duration (`summary`, `window`): each
86
+ * outcome by `outcomeOf` (the one rule), the average from the durations' sum, the p50 / p95 from the durations'
87
+ * histogram as quantile_cont interpolates them (`quantileContOf`). ONE place adds a window up.
88
+ */
89
+ function foldWindow(groups, days, keptDays) {
90
+ const w = { days, covered: Math.min(days, keptDays), statements: 0, completed: 0, refused: 0, failed: 0, cancelled: 0, rows: 0, bytesSent: 0, durationMs: null };
91
+ const histogram = [];
92
+ let duration = 0;
93
+ for (const g of groups) {
94
+ const n = Number(g.n);
95
+ w.statements += n;
96
+ w.rows += Number(g.sent_rows);
97
+ w.bytesSent += Number(g.sent_bytes);
98
+ const outcome = outcomeOf(g.sqlstate === null ? null : String(g.sqlstate));
99
+ if (outcome === "ok") {
100
+ w.completed += n;
101
+ duration += Number(g.duration);
102
+ histogram.push([Number(g.ms), n]);
103
+ }
104
+ else
105
+ w[outcome] += n;
106
+ }
107
+ histogram.sort((a, b) => a[0] - b[0]);
108
+ const p50 = quantileContOf(histogram, 0.5), p95 = quantileContOf(histogram, 0.95);
109
+ if (w.completed > 0 && p50 !== null && p95 !== null)
110
+ w.durationMs = { avg: Math.round(duration / w.completed), p50, p95 };
111
+ return w;
112
+ }
65
113
  export class RequestLedger {
66
- buffer = [];
67
114
  /** the next row's number (REQUESTS_SEQ_DDL): one past the ledger's last at open, then one per request recorded */
68
115
  next = 0;
69
- timer = null;
70
- trimTimer = null;
71
- /** the flush in flight: one at a time, so batches commit in the order they were taken */
72
- flushing = Promise.resolve();
73
116
  /** rows lost since the last flush that succeeded: said once when the streak begins, and when it ends */
74
- lost = 0;
117
+ lost = new Streak();
75
118
  closed = false;
76
- writer;
77
- reader;
119
+ history;
120
+ batches;
78
121
  log;
79
122
  options;
80
123
  /** days kept */
81
124
  days;
82
- constructor(writer, reader, log, options) {
83
- this.writer = writer;
84
- this.reader = reader;
125
+ constructor(history, log, options) {
126
+ this.history = history;
85
127
  this.log = log;
86
128
  this.options = options;
87
129
  this.days = options.days;
130
+ this.batches = new Batches({ rows: BATCH_ROWS, ms: options.flushMs ?? FLUSH_MS, write: (rows) => this.write(rows) });
88
131
  }
89
132
  /** The ledger of a store: its table made, the rows past the retention deleted, and a trim every hour. */
90
133
  static async open(store, log, options) {
91
- const writer = await store.instance.connect();
92
- await writer.run(`CREATE SCHEMA IF NOT EXISTS ${MASON_SCHEMA}`);
93
- await writer.run(REQUESTS_DDL);
94
- await writer.run(REQUESTS_USER_DDL);
95
- await writer.run(REQUESTS_SEQ_DDL);
96
- await writer.run(REQUESTS_JSON_DDL);
97
- const reader = await store.instance.connect();
98
- const ledger = new RequestLedger(writer, reader, log, options);
99
- ledger.next = Number((await reader.runAndReadAll(`SELECT coalesce(max(seq), -1) + 1 FROM ${REQUESTS_TABLE}`)).getRowsJS()[0][0]);
100
- await ledger.trim();
101
- ledger.trimTimer = setInterval(() => {
102
- ledger.trim().catch((e) => log(`request log: deleting the requests older than ${ledger.days} days failed: ${e.message}`));
103
- }, TRIM_EVERY_MS);
104
- ledger.trimTimer.unref();
134
+ const history = await History.open(store, [REQUESTS_DDL, REQUESTS_USER_DDL, REQUESTS_SEQ_DDL, REQUESTS_JSON_DDL, REQUESTS_BYTES_DDL, REQUESTS_GAP_DDL, REQUESTS_ROLE_DDL, REQUESTS_ATTRIBUTES_DDL]);
135
+ const ledger = new RequestLedger(history, log, options);
136
+ ledger.next = Number((await history.reader.runAndReadAll(`SELECT coalesce(max(seq), -1) + 1 FROM ${REQUESTS_TABLE}`)).getRowsJS()[0][0]);
137
+ await history.keepTrimmed(() => ledger.trim(), (e) => log(`request log: deleting the requests older than ${ledger.days} days failed: ${e.message}`));
105
138
  return ledger;
106
139
  }
107
140
  /** One request, written with the next batch. Never throws: the statement it records was already answered. */
@@ -111,26 +144,10 @@ export class RequestLedger {
111
144
  this.log(`request log: a request after the log closed, not recorded (${row.requestId})`);
112
145
  return;
113
146
  }
114
- this.buffer.push({ row, seq: this.next++ });
115
- if (this.buffer.length >= BATCH_ROWS) {
116
- void this.flush();
117
- return;
118
- }
119
- this.timer ??= setTimeout(() => { void this.flush(); }, this.options.flushMs ?? FLUSH_MS);
147
+ this.batches.push({ row, seq: this.next++ });
120
148
  }
121
149
  /** Writes what is waiting, after any flush in flight. Resolves whether it was written or not (a failure is said). */
122
- flush() {
123
- if (this.timer !== null) {
124
- clearTimeout(this.timer);
125
- this.timer = null;
126
- }
127
- const rows = this.buffer;
128
- this.buffer = [];
129
- if (rows.length === 0)
130
- return this.flushing;
131
- this.flushing = this.flushing.then(() => this.write(rows));
132
- return this.flushing;
133
- }
150
+ flush() { return this.batches.flush(); }
134
151
  async write(rows) {
135
152
  const width = COLUMNS.length;
136
153
  const sql = `INSERT INTO ${REQUESTS_TABLE} (${COLUMNS.map((c) => c === "rows" ? `"rows"` : c).join(", ")}) VALUES ` +
@@ -140,45 +157,162 @@ export class RequestLedger {
140
157
  if (this.options.insert !== undefined)
141
158
  await this.options.insert(sql, values);
142
159
  else
143
- await this.writer.run(sql, values);
160
+ await this.history.writer.run(sql, values);
144
161
  }
145
162
  catch (e) {
146
163
  // Tolerated, narrowly, as the ClickHouse writer tolerates it: the answers are sent, so there is no statement left
147
164
  // to fail, and a ledger that cannot write must not take the listener down. SAID, once per streak with the rows it
148
165
  // costs, so a ledger that stopped recording never reads as a quiet listener.
149
- if (this.lost === 0)
150
- this.log(`request log: could not record ${rows.length} request(s) in ${REQUESTS_TABLE} (${rows[0].row.requestId}` +
151
- `${rows.length > 1 ? ` … ${rows[rows.length - 1].row.requestId}` : ""}): ${e.message}; said once until a write succeeds`);
152
- this.lost += rows.length;
166
+ this.lost.failed(rows.length, () => this.log(`request log: could not record ${rows.length} request(s) in ${REQUESTS_TABLE} (${rows[0].row.requestId}` +
167
+ `${rows.length > 1 ? ` … ${rows[rows.length - 1].row.requestId}` : ""}): ${e.message}; said once until a write succeeds`));
153
168
  return;
154
169
  }
155
- if (this.lost > 0) {
156
- this.log(`request log: recording again; ${this.lost} request(s) were not recorded`);
157
- this.lost = 0;
158
- }
170
+ this.lost.succeeded((n) => this.log(`request log: recording again; ${n} request(s) were not recorded`));
159
171
  }
160
172
  /** Deletes the requests older than the retention; the count is said when there were any. */
161
173
  async trim() {
162
- const r = await this.writer.runAndReadAll(`DELETE FROM ${REQUESTS_TABLE} WHERE started_at < now() - INTERVAL (${this.days}) DAY`);
174
+ const r = await this.history.writer.runAndReadAll(`DELETE FROM ${REQUESTS_TABLE} WHERE started_at < now() - INTERVAL (${this.days}) DAY`);
163
175
  const n = Number(r.getRowsJS()[0]?.[0] ?? 0);
164
176
  if (n > 0)
165
- this.log(`request log: deleted ${n} request(s) older than ${this.days} days (MASON_REQUEST_LOG_DAYS)`);
177
+ this.log(`request log: deleted ${n} request(s) older than ${this.days} days (MASON_HISTORY_DAYS)`);
166
178
  return n;
167
179
  }
168
- /** The newest `limit` requests, newest LAST; `failed`: the refused and failed ones only. Committed rows only. */
169
- async recent(o) {
170
- const rows = (await this.reader.runAndReadAll(`SELECT * FROM (SELECT * FROM ${REQUESTS_TABLE}${o.failed ? " WHERE sqlstate IS NOT NULL" : ""} ` +
171
- `ORDER BY started_at DESC, seq DESC LIMIT $1) ORDER BY started_at, seq`, [o.limit])).getRowObjectsJson();
180
+ /**
181
+ * The newest `limit` requests that pass the filters, newest LAST. `failed`: the ones not answered (refused, failed or
182
+ * cancelled); `outcomes`, `since` / `until` (started at or after / before), `user`, `client` (application_name) and
183
+ * `before` (strictly older than that position: a page's `before`) narrow it further. Committed rows only.
184
+ */
185
+ async recent(q) {
186
+ return (await this.rows(q)).map((r) => requestOf(r));
187
+ }
188
+ /**
189
+ * ONE PAGE of the request log (`/api/requests`, `mason logs --requests`): `recent`, and `before`, the cursor of the
190
+ * page's oldest row when an older one passes the filters (null: this is the last page). Pages follow the ledger's
191
+ * order (started_at, then seq), so the page asked with `before` holds the rows older than the page before it, whatever
192
+ * was recorded since — but a statement still running when a page was read is recorded at its START, so it may land in
193
+ * a page already read.
194
+ */
195
+ async page(q) {
196
+ const rows = await this.rows({ ...q, limit: q.limit + 1 });
197
+ const more = rows.length > q.limit;
198
+ const shown = more ? rows.slice(1) : rows;
199
+ const first = shown[0];
200
+ return { requests: shown.map((r) => requestOf(r)),
201
+ before: more && first !== undefined ? requestCursor(new Date(String(first.started_at)).toISOString(), first.seq === null || first.seq === undefined ? -1 : Number(first.seq)) : null };
202
+ }
203
+ async rows(q) {
204
+ const where = [];
205
+ const values = [];
206
+ const arg = (v, cast = "") => { values.push(v); return `$${values.length}${cast}`; };
207
+ if (q.failed === true)
208
+ where.push("sqlstate IS NOT NULL");
209
+ if (q.outcomes !== undefined && q.outcomes.length > 0)
210
+ where.push(`(${outcomeSql("sqlstate")}) IN (${q.outcomes.map((o) => arg(o)).join(", ")})`);
211
+ if (q.since !== undefined)
212
+ where.push(`started_at >= ${arg(q.since, "::TIMESTAMPTZ")}`);
213
+ if (q.until !== undefined)
214
+ where.push(`started_at < ${arg(q.until, "::TIMESTAMPTZ")}`);
215
+ if (q.user !== undefined)
216
+ where.push(`user_name = ${arg(q.user)}`);
217
+ if (q.client !== undefined)
218
+ where.push(`application_name = ${arg(q.client)}`);
219
+ if (q.minMs !== undefined)
220
+ where.push(`duration_ms >= ${arg(q.minMs)}`);
221
+ // a substring, any case: contains() takes the text as it is (no LIKE pattern to escape); a scan of the kept days, as every filter is
222
+ if (q.search !== undefined)
223
+ where.push(`contains(lower(coalesce(query, '')), lower(${arg(q.search)}))`);
224
+ if (q.before !== undefined) {
225
+ const at = arg(q.before.at, "::TIMESTAMPTZ");
226
+ where.push(`(started_at < ${at} OR (started_at = ${at} AND coalesce(seq, -1) < ${arg(q.before.seq)}))`);
227
+ }
228
+ const limit = arg(q.limit);
229
+ const rows = (await this.history.reader.runAndReadAll(`SELECT * FROM (SELECT * FROM ${REQUESTS_TABLE}${where.length ? ` WHERE ${where.join(" AND ")}` : ""} ` +
230
+ `ORDER BY started_at DESC, coalesce(seq, -1) DESC LIMIT ${limit}) ORDER BY started_at, coalesce(seq, -1)`, values)).getRowObjectsJson();
231
+ return rows;
232
+ }
233
+ /**
234
+ * THE QUERY SUMMARY of `mason status` (`/api/queries`): the statements of the last day and of the last 7 days — how
235
+ * many, how many completed, refused, failed and cancelled, how long the completed ones took (average, p50, p95), the
236
+ * rows and the bytes sent — in ONE scan of the ledger. A refused login (CONNECT) is not a statement. The log keeps
237
+ * `days` days, so a window longer than that is `covered` only as far as it keeps (said, never a short week passed off
238
+ * as a whole one).
239
+ *
240
+ * The scan is ONE GROUP BY — by the window (the last day or before it), the SQLSTATE, and a completed one's duration
241
+ * (whole ms) — whose few groups are added up here: each outcome by `outcomeOf` (the one rule), the average from the
242
+ * durations' sum, and the p50 / p95 from the durations' histogram as quantile_cont interpolates them
243
+ * (`quantileContOf`), exact. Measured over a week of 50 statements a second (30 M rows): four quantile_cont and
244
+ * sixteen FILTERed aggregates over the raw rows took 0.71–0.77 s, this ~0.12 s — and the Overview asks it every 30 s
245
+ * (docs/OBSERVABILITY.md "Architecture review").
246
+ */
247
+ async summary() {
248
+ const groups = (await this.history.reader.runAndReadAll(`SELECT started_at > now() - INTERVAL 1 DAY AS in_day, sqlstate, ` +
249
+ `CASE WHEN ${outcomeSql("sqlstate")} = 'ok' THEN duration_ms END AS ms, count(*) AS n, sum(duration_ms) AS duration, ` +
250
+ `coalesce(sum("rows"), 0) AS sent_rows, coalesce(sum(bytes_sent), 0) AS sent_bytes ` +
251
+ `FROM ${REQUESTS_TABLE} WHERE started_at > now() - INTERVAL 7 DAY AND method <> 'CONNECT' GROUP BY ALL`)).getRowObjectsJson();
252
+ const windowOf = (days) => foldWindow(days === 1 ? groups.filter((g) => g.in_day === true) : groups, days, this.days);
253
+ return { keptDays: this.days, day: windowOf(1), week: windowOf(7) };
254
+ }
255
+ /**
256
+ * ONE WINDOW OF THE REQUEST LOG from `since` (ISO 8601) to now — what the Usage page's cards say for its time range:
257
+ * the statements, each outcome, the rows and bytes sent and the completed ones' durations, as `summary` folds a day,
258
+ * by the same GROUP BY (`foldWindow`). `days` is the window's length in days (a fraction under one), `covered` as far
259
+ * as the log keeps it.
260
+ */
261
+ async window(since, now = Date.now()) {
262
+ const groups = (await this.history.reader.runAndReadAll(`SELECT sqlstate, CASE WHEN ${outcomeSql("sqlstate")} = 'ok' THEN duration_ms END AS ms, count(*) AS n, ` +
263
+ `sum(duration_ms) AS duration, coalesce(sum("rows"), 0) AS sent_rows, coalesce(sum(bytes_sent), 0) AS sent_bytes ` +
264
+ `FROM ${REQUESTS_TABLE} WHERE started_at >= $1::TIMESTAMPTZ AND method <> 'CONNECT' GROUP BY ALL`, [since])).getRowObjectsJson();
265
+ return foldWindow(groups, Math.max(0, (now - Date.parse(since)) / 86_400_000), this.days);
266
+ }
267
+ /**
268
+ * WHO CONNECTED from `since` on, by the statements they sent: a row per user and client (application_name) — its
269
+ * statements, the failed ones (refused or failed, as the Failed card counts them: a cancel is not), the sessions they came from and when the latest started —
270
+ * the most statements first, `limit` of them, and the totals over every one (`total`: how many clients, how many
271
+ * sessions — a card says them, so they are never the first `limit`'s). A session that sent only what the log does not list (select 1, SET, a
272
+ * catalog query) is not here; the live sessions are `/api/state`'s. Committed rows only.
273
+ */
274
+ async clients(since, limit = 50) {
275
+ // the totals over EVERY client, before the LIMIT (a window over the groups): a card says them, never the first `limit`'s
276
+ const rows = (await this.history.reader.runAndReadAll(`SELECT user_name, token_name, application_name, count(*) AS statements, ` +
277
+ `count(*) OVER () AS total_clients, sum(count(DISTINCT session)) OVER () AS total_sessions, ` +
278
+ `count(*) FILTER (WHERE (${outcomeSql("sqlstate")}) IN ('refused', 'failed')) AS failed, count(DISTINCT session) AS sessions, max(started_at) AS last ` +
279
+ `FROM ${REQUESTS_TABLE} WHERE started_at >= $1::TIMESTAMPTZ AND method <> 'CONNECT' GROUP BY ALL ORDER BY statements DESC, last DESC LIMIT $2`, [since, limit])).getRowObjectsJson();
280
+ const first = rows[0];
281
+ return { total: { clients: Number(first?.total_clients ?? 0), sessions: Number(first?.total_sessions ?? 0) },
282
+ clients: rows.map((r) => ({ user: String(r.user_name ?? ""), tokenName: String(r.token_name ?? ""), client: String(r.application_name ?? ""),
283
+ statements: Number(r.statements), failed: Number(r.failed), sessions: Number(r.sessions), last: new Date(String(r.last)).toISOString() })) };
284
+ }
285
+ /**
286
+ * THE COMPLETED STATEMENTS' p50 AND p95 IN BUCKETS of `stepSeconds` from `since` until `until`, by when each started
287
+ * (quantile_cont, as `summary`'s): what a chart of more than a minute a point plots. A bucket's p95 is the p95 of its
288
+ * statements, never the highest of its minutes' p95s — one slow minute in an hour was the hour's "p95". By bucket
289
+ * start, epoch ms; a bucket with no completed statement is absent. Committed rows only.
290
+ */
291
+ async durationsBy(since, until, stepSeconds) {
292
+ const rows = (await this.history.reader.runAndReadAll(`SELECT epoch_ms(time_bucket(to_seconds($3::BIGINT), started_at, $1::TIMESTAMPTZ)) AS at, ` +
293
+ `quantile_cont(duration_ms, 0.5) AS p50, quantile_cont(duration_ms, 0.95) AS p95 FROM ${REQUESTS_TABLE} ` +
294
+ `WHERE started_at >= $1::TIMESTAMPTZ AND started_at < $2::TIMESTAMPTZ AND method <> 'CONNECT' AND (${outcomeSql("sqlstate")}) = 'ok' GROUP BY 1`, [since, until, stepSeconds])).getRowsJS();
295
+ // rounded as the summary's and a sample's are (src/duck/usage.ts quantileCont): one ms, one definition
296
+ return new Map(rows.map(([at, p50, p95]) => [Number(at), { p50: Math.round(Number(p50)), p95: Math.round(Number(p95)) }]));
297
+ }
298
+ /** The slowest `limit` completed statements started from `since` on, slowest first: what ran longest in a time range
299
+ * that is past (the Usage page's analogue of what runs now). Committed rows only. */
300
+ async slowest(since, limit = 10) {
301
+ const rows = (await this.history.reader.runAndReadAll(`SELECT * FROM ${REQUESTS_TABLE} WHERE started_at >= $1::TIMESTAMPTZ AND sqlstate IS NULL AND method <> 'CONNECT' ` +
302
+ `ORDER BY duration_ms DESC, started_at DESC LIMIT $2`, [since, limit])).getRowObjectsJson();
172
303
  return rows.map((r) => requestOf(r));
173
304
  }
174
305
  /**
175
306
  * The last day's slow statements in `mason status`'s shape (src/cli/run-server.ts SlowQuery): each distinct statement
176
- * whose LATEST answer still took `slowMs` or more, slowest first.
307
+ * whose LATEST answer still took `slowMs` or more, slowest first. Only a statement that took it at least once can,
308
+ * so those are found first and only their runs are grouped (a day of 50 statements a second, 4.3 M rows: 0.21 s
309
+ * grouping every statement's text, 0.07 s this way).
177
310
  */
178
311
  async slow(slowMs, limit = 5) {
179
- const rows = (await this.reader.runAndReadAll(`SELECT query, count(*) AS runs, CAST(quantile_disc(duration_ms, 0.5) AS INTEGER) AS median_ms, ` +
312
+ const day = `sqlstate IS NULL AND started_at > now() - INTERVAL 1 DAY AND query <> '' AND NOT query_truncated`;
313
+ const rows = (await this.history.reader.runAndReadAll(`SELECT query, count(*) AS runs, CAST(quantile_disc(duration_ms, 0.5) AS INTEGER) AS median_ms, ` +
180
314
  `max(duration_ms) AS max_ms, arg_max(duration_ms, started_at) AS latest_ms, max(started_at) AS last FROM ${REQUESTS_TABLE} ` +
181
- `WHERE sqlstate IS NULL AND started_at > now() - INTERVAL 1 DAY AND query <> '' AND NOT query_truncated ` +
315
+ `WHERE ${day} AND query IN (SELECT query FROM ${REQUESTS_TABLE} WHERE ${day} AND duration_ms >= $1) ` +
182
316
  `GROUP BY query HAVING arg_max(duration_ms, started_at) >= $1 ORDER BY latest_ms DESC LIMIT $2`, [slowMs, limit])).getRowObjectsJson();
183
317
  return rows.map((r) => ({ route: "postgres", payload: String(r.query), runs: Number(r.runs), medianMs: Number(r.median_ms), maxMs: Number(r.max_ms),
184
318
  latestMs: Number(r.latest_ms), last: new Date(String(r.last)).toISOString() }));
@@ -189,21 +323,59 @@ export class RequestLedger {
189
323
  */
190
324
  async refused(limit = 8) {
191
325
  const day = `sqlstate IS NOT NULL AND started_at > now() - INTERVAL 1 DAY`;
192
- const rows = (await this.reader.runAndReadAll(`SELECT query, count(*) AS runs, max(started_at) AS last, arg_max(sqlstate, started_at) AS code, arg_max(message, started_at) AS message ` +
326
+ const rows = (await this.history.reader.runAndReadAll(`SELECT query, count(*) AS runs, max(started_at) AS last, arg_max(sqlstate, started_at) AS code, arg_max(message, started_at) AS message ` +
193
327
  `FROM ${REQUESTS_TABLE} WHERE ${day} GROUP BY query ORDER BY last DESC LIMIT $1`, [limit])).getRowObjectsJson();
194
- const total = Number((await this.reader.runAndReadAll(`SELECT count(*) FROM ${REQUESTS_TABLE} WHERE ${day}`)).getRowsJS()[0]?.[0] ?? 0);
328
+ const total = Number((await this.history.reader.runAndReadAll(`SELECT count(*) FROM ${REQUESTS_TABLE} WHERE ${day}`)).getRowsJS()[0]?.[0] ?? 0);
195
329
  return { total, refused: rows.map((r) => ({ route: "postgres", payload: String(r.query), runs: Number(r.runs), last: new Date(String(r.last)).toISOString(),
196
330
  code: String(r.code), message: String(r.message ?? "") })) };
197
331
  }
332
+ /**
333
+ * THE REFUSED STATEMENTS GROUPED BY THE UNSUPPORTED SQL THEY USED (`/api/unsupported`, `mason status`): for each
334
+ * registry entry that refused a statement started at or after `since`, how many it refused, by whom (users and
335
+ * clients), when last, and the latest of them — the most refused first, `limit` entries, in ONE query. A statement
336
+ * refused for any other reason (a missing table, a syntax error) has no entry, so it is not here. Committed rows only.
337
+ */
338
+ async unsupported(q) {
339
+ const rows = (await this.history.reader.runAndReadAll(`SELECT gap, count(*) AS runs, ` +
340
+ `coalesce(list_sort(list(DISTINCT user_name) FILTER (WHERE user_name <> '')), []) AS users, ` +
341
+ `coalesce(list_sort(list(DISTINCT application_name) FILTER (WHERE application_name <> '')), []) AS clients, ` +
342
+ `max(started_at) AS last, arg_max(query, started_at) AS example ` +
343
+ `FROM ${REQUESTS_TABLE} WHERE gap IS NOT NULL AND started_at >= $1::TIMESTAMPTZ GROUP BY gap ORDER BY runs DESC, last DESC, gap LIMIT $2`, [q.since, q.limit])).getRowObjectsJson();
344
+ return rows.map((r) => {
345
+ const gap = String(r.gap);
346
+ const query = String(r.example ?? "");
347
+ return { gap, title: Object.hasOwn(GAPS, gap) ? GAPS[gap].what : null, runs: Number(r.runs),
348
+ users: r.users.map(String), clients: r.clients.map(String), last: new Date(String(r.last)).toISOString(),
349
+ example: query.length > UNSUPPORTED_EXAMPLE_CHARS ? `${query.slice(0, UNSUPPORTED_EXAMPLE_CHARS)}…` : query };
350
+ });
351
+ }
198
352
  /** Writes what is waiting and closes the ledger's connections (mason stop). */
199
353
  async close() {
200
- if (this.trimTimer !== null) {
201
- clearInterval(this.trimTimer);
202
- this.trimTimer = null;
203
- }
354
+ this.history.stopTrimming();
204
355
  await this.flush();
205
356
  this.closed = true;
206
- this.writer.closeSync();
207
- this.reader.closeSync();
357
+ this.history.closeConnections();
358
+ }
359
+ /**
360
+ * When each `mason user` last ran a statement, within the log's retention: for each of `users` (a name and the window
361
+ * it was a user in: `since` its addedAt, `until` its removedAt or null), the time of its newest request in that
362
+ * window, ISO 8601, or null when the log has none. The window tells a user from an earlier one of the same name. A
363
+ * refused login (28P01, 57P03) or an ended session (28000) is no use, so it does not count. Committed rows only.
364
+ * ONE scan of the log for every user, JOINED to the users' windows (a hash join on the name, the window a residual),
365
+ * never one per user — nor a `max … FILTER` per user, which evaluated every user's filter on every row: on a week of
366
+ * 50 statements a second (30 M rows) 0.18 s for 6 users and 2.2–2.8 s for 66, where the join is ~0.15 s for any
367
+ * number; the Users page asks it every 30 s (docs/OBSERVABILITY.md "Architecture review").
368
+ */
369
+ async lastUsed(users) {
370
+ if (users.length === 0)
371
+ return [];
372
+ const params = [];
373
+ const at = (v) => { params.push(v); return `$${params.length}`; };
374
+ const windows = users.map((u, i) => `(${at(i)}::INTEGER, ${at(u.name)}::VARCHAR, ${at(u.since)}::TIMESTAMPTZ, ${at(u.until)}::TIMESTAMPTZ)`).join(", ");
375
+ const found = (await this.history.reader.runAndReadAll(`SELECT u.i, epoch_ms(max(r.started_at)) AS at FROM (VALUES ${windows}) AS u(i, name, since, until) ` +
376
+ `JOIN ${REQUESTS_TABLE} r ON r.user_name = u.name AND r.started_at >= u.since AND (u.until IS NULL OR r.started_at <= u.until) ` +
377
+ `WHERE r.sqlstate IS NULL OR r.sqlstate NOT IN ('28P01', '57P03', '28000') GROUP BY u.i`, params)).getRowsJS();
378
+ const row = new Map(found.map(([i, ms]) => [Number(i), ms]));
379
+ return users.map((_, i) => { const ms = row.get(i); return ms === null || ms === undefined ? null : new Date(Number(ms)).toISOString(); });
208
380
  }
209
381
  }
@@ -1,4 +1,4 @@
1
- import { pathIndex, SESSION_VARIABLE } from "./functions.js";
1
+ import { pathIndex, SESSION_VARIABLE, SETTINGS_VARIABLE } from "./functions.js";
2
2
  import { cellsOf as cellsOfProgram, numericLiteral } from "../lib/pg-numeric.js";
3
3
  import { caseTwins, joinsUsing } from "./pgtree.js";
4
4
  import { canonicalRead, EQUAL_STEPS, KEY_NULL_HEX, KEY_RECOMPUTE_HEX, MARKED_FUNCTIONS, markedError, ORDER_STEPS, readKey } from "./kept-route.js";
@@ -26,12 +26,12 @@ export function duckCollation(provider, collate, locale, known) {
26
26
  if (provider === "i") {
27
27
  const name = icuName(locale ?? collate);
28
28
  if (name === null)
29
- throw new Error(`the source database's ICU locale ${JSON.stringify(locale ?? collate)} has no DuckDB collation: Mason cannot sort text as it does`);
29
+ throw new Error(`the source database's ICU locale ${JSON.stringify(locale ?? collate)} has no collation in the engine: Mason cannot sort text as it does`);
30
30
  return { kind: "icu", name };
31
31
  }
32
32
  const name = icuName(collate);
33
33
  if (name === null)
34
- throw new Error(`the source database's collation ${JSON.stringify(collate)} has no DuckDB collation to follow: Mason cannot sort text as it does`);
34
+ throw new Error(`the source database's collation ${JSON.stringify(collate)} has no collation in the engine to follow: Mason cannot sort text as it does`);
35
35
  return { kind: "libc", icu: name };
36
36
  }
37
37
  export class Unrewritable extends Error {
@@ -739,7 +739,7 @@ class Rewriter {
739
739
  if (table !== undefined)
740
740
  return table.map((c) => ({ name: c.name, value: this.typedRef([alias, c.name], c.type, c.typmod), type: c.type }));
741
741
  if (t.type !== RECORD)
742
- throw new Unrewritable(`a row of ${this.userTypes.get(t.type)?.replace(/^rowtype:/, "") ?? "a table"} is not supported by Mason: Mason copied the table in part`, "name its columns instead", "row-value");
742
+ throw new Unrewritable(`a row of ${this.userTypes.get(t.type)?.replace(/^rowtype:/, "") ?? "a table"} is not supported by Mason: Mason synced the table in part`, "name its columns instead", "row-value");
743
743
  const source = this.fromSources(select).filter((q) => q.alias === alias);
744
744
  if (source.length !== 1)
745
745
  throw new Unrewritable("a row of this FROM item is not supported by Mason", "name its columns instead", "row-value");
@@ -1045,7 +1045,7 @@ class Rewriter {
1045
1045
  */
1046
1046
  withTies(b) {
1047
1047
  if (b.distinctClause !== undefined)
1048
- throw new Unrewritable("FETCH … WITH TIES over a SELECT DISTINCT is not supported by Mason (DuckDB has no WITH TIES)", "select the DISTINCT rows in a subquery and FETCH … WITH TIES from it", "with-ties-distinct");
1048
+ throw new Unrewritable("FETCH … WITH TIES over a SELECT DISTINCT is not supported by Mason (the engine has no WITH TIES)", "select the DISTINCT rows in a subquery and FETCH … WITH TIES from it", "with-ties-distinct");
1049
1049
  const order = b.sortClause ?? [];
1050
1050
  const count = b.limitCount;
1051
1051
  delete b.limitCount;
@@ -2922,6 +2922,8 @@ class Rewriter {
2922
2922
  case "family": return argType(0) === INET && args.length === 1
2923
2923
  ? caseWhen([[{ NullTest: { arg: this.cp(args[0]), nulltesttype: "IS_NULL" } }, nullConst()], [fn("contains", [args[0], strConst(":")]), intConst(6)]], intConst(4)) : null;
2924
2924
  case "abbrev": return argType(0) === INET && args.length === 1 ? inetOut(args[0]) : null;
2925
+ // the session's setting, never DuckDB's own (`settingRead`)
2926
+ case "current_setting": return args.length === 1 || args.length === 2 ? settingRead(this.cp, args) : null;
2925
2927
  // the transaction's start, as of the execution (sqlValueFunction says why)
2926
2928
  case "now":
2927
2929
  case "transaction_timestamp": return args.length === 0 ? this.now("exec") : null;
@@ -4713,6 +4715,24 @@ function replacement(r) {
4713
4715
  return strConst(s.replace(/\\(.)/g, (_m, c) => c === "&" ? "\\0" : `\\${c}`));
4714
4716
  }
4715
4717
  /** substring(text FROM pattern): the first parenthesised part when there is one, else the whole match; NULL without one */
4718
+ /**
4719
+ * `current_setting(name[, missing_ok])` BESIDE A COPIED TABLE IS THE SESSION'S (GitHub issue 40): the name read, in any
4720
+ * case, from the settings the session's statement runs under (`SETTINGS_VARIABLE`: src/api/postgres/server.ts
4721
+ * `duckSession`, the listener's `sessionSetting` laid over the catalog Postgres's own, set by src/duck/store.ts
4722
+ * `settle`) — never DuckDB's own `current_setting`, which answered DuckDB's settings (`search_path` its schemas,
4723
+ * `work_mem` an error). A name neither holds is Postgres's 42704, or NULL under missing_ok; strict, as Postgres's.
4724
+ */
4725
+ function settingRead(copy, args) {
4726
+ const [name, missingOk] = args;
4727
+ const settings = () => fn("getvariable", [strConst(SETTINGS_VARIABLE)]);
4728
+ const key = () => fn("lower", [copy(name)]);
4729
+ const unknown = fn("error", [concat(strConst('mason[42704]: unrecognized configuration parameter "'), copy(name), strConst('"'))]);
4730
+ return caseWhen([
4731
+ [missingOk === undefined ? isNull(name) : or([isNull(name), isNull(missingOk)]), nullConst()],
4732
+ [fn("map_contains", [settings(), key()]), fn("map_extract_value", [settings(), key()])],
4733
+ ...(missingOk === undefined ? [] : [[copy(missingOk), nullConst()]]),
4734
+ ], unknown);
4735
+ }
4716
4736
  function regexSubstring(copy, s, p) {
4717
4737
  const pattern = regexPattern(p, true);
4718
4738
  const text = constString(p);