@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,299 @@
1
+ /**
2
+ * A STORED JSON PATH, AS A STATEMENT READS IT (docs/DESIGN.md "Stored JSON paths"; the keeper is
3
+ * src/duck/kept.ts). A copied table's JSON column read by a path a statement names with literal steps — `payload ->>
4
+ * 'customer_id'`, `payload #> '{a,0}'`, `jsonb_extract_path_text(payload, 'a')` — is computed by a JavaScript function
5
+ * for every row of every statement (`__mason_json_get`, src/duck/functions.ts): a hop from DuckDB's thread to Node's
6
+ * main thread, ~4.5 µs a row on the test server, 98 s over 22 M rows. A path statements keep reading is stored instead,
7
+ * as a hidden column of the copy holding exactly what that function answers, computed once per row WRITTEN; the
8
+ * rewrite (src/duck/rewrite.ts `keptRead`) reads the column in place of the call once every row of it is stored.
9
+ *
10
+ * This module is what a statement's plan needs of it, and nothing that writes: the names, the READ a rewrite records,
11
+ * and `hideKept`, which keeps the hidden columns out of every answer — a hidden column is a column of the copied table
12
+ * to DuckDB, so a `*`, a `t.*`, a whole row or a NATURAL JOIN would list it. Postgres's analysis (the catalog, PGlite)
13
+ * never knows it, so a result's names, types and typmods are Postgres's whatever DuckDB holds.
14
+ */
15
+ import { createHash } from "node:crypto";
16
+ import { pathIndex } from "./functions.js";
17
+ /**
18
+ * A READ AS THE ONE PATH IT IS (the test server: `#>> '{order,status}'` and `-> 'order' ->> 'status'`
19
+ * were two ledger rows, two columns and two whole-table backfills of one path). A `#>` path whose every element is a
20
+ * key — none reads as an array index (`pathIndex`, the function's own definition) — reads what the same keys read by
21
+ * `->`: on an object each is a key, on anything else the read is NULL either way. So it IS the keys' read, and is
22
+ * written as one here: THE ONE PLACE every read passes through — the rewrite's (`keptRead`), the request log's key
23
+ * (`readKey`), the hidden column's name (`hiddenName`), the ledger and `mason status` (the keeper's rows are written
24
+ * from it). An element that reads as an index (`{arr,0}`) is not folded: on an array `#> '{arr,0}'` is its first
25
+ * element where `-> 'arr' -> '0'` (a key) is NULL.
26
+ */
27
+ export function canonicalRead(r) {
28
+ if (!r.either)
29
+ return r;
30
+ const steps = JSON.parse(r.steps);
31
+ return steps.every((s) => typeof s === "string" && pathIndex(s) === null) ? { ...r, either: false } : r;
32
+ }
33
+ /** a read as one string: the request log's key (its `->>` or `->` included: the policy counts both for one path) */
34
+ export const readKey = (r) => { const c = canonicalRead(r); return JSON.stringify([c.source, c.column, c.steps, c.either, c.canonical, c.asText]); };
35
+ /** a read's key, back (null for a string that is not one, and for a read of a kind this Mason does not know: `knownSteps`) */
36
+ export function readOfKey(key) {
37
+ let v;
38
+ try {
39
+ v = JSON.parse(key);
40
+ }
41
+ catch {
42
+ return null;
43
+ } // a key a later Mason wrote otherwise: not a read this one keeps
44
+ if (!Array.isArray(v) || v.length !== 6)
45
+ return null;
46
+ const [source, column, steps, either, canonical, asText] = v;
47
+ if (typeof source !== "string" || typeof column !== "string" || typeof steps !== "string" || typeof either !== "boolean" || typeof canonical !== "boolean" || typeof asText !== "boolean")
48
+ return null;
49
+ if (!knownSteps(steps))
50
+ return null;
51
+ return canonicalRead({ source, column, steps, either, canonical, asText });
52
+ }
53
+ /**
54
+ * WHETHER A READ'S STEPS ARE A KIND THIS MASON KNOWS: a path's (a JSON array of keys and indexes) or a jsonb's key
55
+ * (`isKeyRead`). A request log or a ledger written by another release may hold a kind this one does not know (an
56
+ * earlier release met `equal:…` and `order:…` and failed every write of the table, and its status, parsing them as a
57
+ * path): such a read is INERT, never fatal — not counted (`readOfKey`), its ledger row dropped at the start with its
58
+ * column (src/duck/kept.ts `open`), and said raw (`pathText`).
59
+ */
60
+ export function knownSteps(steps) {
61
+ if (isKeyRead({ steps }))
62
+ return true;
63
+ try {
64
+ return Array.isArray(JSON.parse(steps));
65
+ }
66
+ catch {
67
+ return false;
68
+ } // not JSON: not a path, so not a kind known here
69
+ }
70
+ /**
71
+ * THE STORED PATH A READ IS ANSWERED FROM: its canonical read (`canonicalRead`) without `->>`. One column serves a
72
+ * path's `->` and its `->>` (the test server: a popular key was stored twice, as `-> 'k'` and `->> 'k'`, two
73
+ * whole-table backfills): it holds the `->` answer, the value's JSON text, from which the `->>` answer is derived
74
+ * without the document (src/duck/rewrite.ts `keptRead`) — a string unquoted, a JSON null NULL, anything else its text,
75
+ * which is what `->>` answers (a jsonb number's text is numeric's either way, src/duck/jsonb.ts writeJson). The other
76
+ * way would lose what `->` answers: `->>` reads the string "5" and the number 5 alike.
77
+ */
78
+ export const pathKey = (r) => { const c = canonicalRead(r); return JSON.stringify([c.source, c.column, c.steps, c.either, c.canonical]); };
79
+ /**
80
+ * THE HIDDEN COLUMN'S NAME: `__mason_json_` and 12 hex digits of the stored path's hash (`pathKey`), over the table too, so two tables'
81
+ * columns never share a name (a NATURAL JOIN of two tables would join on it). Mason's prefix: a source column of that
82
+ * very name is refused by the keeper (DuckDB names are case-insensitive), never shadowed.
83
+ */
84
+ export const HIDDEN_PREFIX = "__mason_json_";
85
+ export const HIDDEN_RE = /^__mason_json_[0-9a-f]{12}$/;
86
+ export const hiddenName = (r) => `${HIDDEN_PREFIX}${createHash("sha256").update(pathKey(r)).digest("hex").slice(0, 12)}`;
87
+ /**
88
+ * THE VIEWS OF THE COPIES' VISIBLE COLUMNS: an attached in-memory DuckDB database of this name holds, for every served
89
+ * copy `<schema>.<table>`, a view `<schema>.<table>` of its visible columns in their order — the copy as it was before
90
+ * any column was stored. A statement that lists every column of a copy reads it there (`hideKept`): the same schema and
91
+ * table name, so `public.orders.id` and `orders.id` still bind.
92
+ */
93
+ export const VISIBLE_CATALOG = "__mason_visible";
94
+ /**
95
+ * A JSONB'S ORDER, AS A READ (src/duck/jsonb-order.ts; docs/DESIGN.md "jsonb's order"): `ORDER BY j`, `GROUP BY j`,
96
+ * `j = k` compare a key of the whole document, computed per row by a function — 19 s for `ORDER BY j LIMIT 100` over
97
+ * 20 M rows under C, 83 s under a libc locale, where the same sort from a stored key takes 0.1 s. So the key is a read
98
+ * like a path's, recorded, weighed and stored by the same keeper: its `steps` is `ORDER_STEPS` (never a path's, which
99
+ * is a JSON array). The rewrite records it as `ORDER_STEPS`; the keeper stores it as `ORDER_STEPS:<the collation's
100
+ * id>` (src/duck/kept.ts `own`), because a string's key is its collation's, so a key stored under another collation is
101
+ * another column, never read.
102
+ */
103
+ export const ORDER_STEPS = "order";
104
+ /** a stored key's marks (hex, DuckDB's `unhex`): a NULL document's, and a row whose key raised, computed when read. No
105
+ * key begins with a 0x00 (src/duck/jsonb-order.ts: every key starts with an array's or an object's tag) */
106
+ export const KEY_NULL_HEX = "00";
107
+ export const KEY_RECOMPUTE_HEX = "0021";
108
+ export const isOrderRead = (r) => r.steps === ORDER_STEPS || r.steps.startsWith(`${ORDER_STEPS}:`);
109
+ /**
110
+ * A JSONB'S EQUALITY, AS A READ, under a LIBC collation alone (src/duck/rewrite.ts `equalKey`): `GROUP BY j`, `DISTINCT
111
+ * j`, `j = k`, `j IN (…)`, a join on `=`, `count(DISTINCT j)` compare the C key — a libc order's key refuses a string
112
+ * its measured order does not cover, where equality never needed an order (docs/DESIGN.md "jsonb's order"), so the
113
+ * order's stored key cannot serve them, and over 20 M rows a GROUP BY computing the C key per row took 32–45 s of CPU
114
+ * (34–77 s wall; docs/DESIGN.md "Under libc, the C key stored for equality"), 0.15–0.23 s from it. So the C key
115
+ * is a read of its own, stored beside the order's as `EQUAL_STEPS:<id>` (src/duck/kept.ts `equalId`). Under C and ICU
116
+ * the order's key equates exactly and never refuses, so equality reads IT and this read is never recorded nor stored
117
+ * (src/duck/kept.ts `own` folds one into the order's).
118
+ */
119
+ export const EQUAL_STEPS = "equal";
120
+ export const isEqualRead = (r) => r.steps === EQUAL_STEPS || r.steps.startsWith(`${EQUAL_STEPS}:`);
121
+ /** a jsonb's key, the order's or the equality's: a BLOB column of the key's bytes, its marks `KEY_NULL_HEX` and
122
+ * `KEY_RECOMPUTE_HEX`, stored under an id of what its bytes depend on */
123
+ export const isKeyRead = (r) => isOrderRead(r) || isEqualRead(r);
124
+ /** a read as a developer writes it: `payload ->> 'customer_id'`, `payload #> '{a,0}'`; a stored path (no `asText`) as
125
+ * its `->` read; a jsonb's order and equality as what reads each (under libc the order's key sorts and the C key
126
+ * equates, two columns: `mason status` names each by its own) */
127
+ export function pathText(r) {
128
+ // a kind this Mason does not know (another release's): said as it is stored, never parsed (`knownSteps`)
129
+ if (!knownSteps(r.steps))
130
+ return `${r.column} (stored as ${r.steps}: a kind this Mason does not know)`;
131
+ if (isEqualRead(r))
132
+ return `GROUP BY ${r.column} (its jsonb equality: grouped, DISTINCT, = and IN)`;
133
+ if (isOrderRead(r))
134
+ return r.steps.startsWith(`${ORDER_STEPS}:libc:`) ? `ORDER BY ${r.column} (its jsonb order: sorted, and compared by < and >)`
135
+ : `ORDER BY ${r.column} (its jsonb order: sorted, grouped and compared)`;
136
+ const steps = JSON.parse(r.steps);
137
+ const op = (one) => one ? (r.asText ? "->>" : "->") : (r.asText ? "#>>" : "#>");
138
+ if (steps.length === 1 && !r.either) {
139
+ const s = steps[0];
140
+ return `${r.column} ${op(true)} ${typeof s === "number" ? String(s) : `'${s.replaceAll("'", "''")}'`}`;
141
+ }
142
+ const element = (s) => typeof s === "number" || /^[A-Za-z0-9_]+$/.test(s) ? String(s) : `"${String(s).replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
143
+ return `${r.column} ${op(false)} '{${steps.map(element).join(",").replaceAll("'", "''")}}'`;
144
+ }
145
+ /**
146
+ * WHAT AN EXACT READ RAISES AT A MARK (src/duck/rewrite.ts `keptRead`, `keptKey`): a row marked to be computed
147
+ * (`KEPT_RECOMPUTE`, `KEY_RECOMPUTE_HEX`) written AFTER the path was counted exact — by the stream, the poll or a copy's
148
+ * side table, before the detector (src/duck/kept.ts `check`) counts it and turns the reads to the CASE — is never read
149
+ * as a value: the exact read raises at it, by the column alone. A jsonb's key under a libc collation is marked where
150
+ * its strings hold a character the measured order does not cover, the gap the statement's own key refuses by name
151
+ * (`libc-uncovered`); any other mark is a Mason bug. Once counted (a minute at most), the CASE computes the row from its
152
+ * document, and raises the function's own error (the character named) or answers. Raised by `MARKED_FUNCTIONS`, never
153
+ * DuckDB's `error()`, which is volatile (a window's ORDER BY refuses it: the eager oracle's `rank() OVER (ORDER BY j)`).
154
+ */
155
+ export function markedError(r, libc) {
156
+ if (isOrderRead(r) && libc) {
157
+ return { code: "0A000", message: "a jsonb compared or sorted here holds a string Mason cannot order as the database's collation does (a combining mark, a Thai or " +
158
+ "Lao vowel written before its consonant, a private-use or unassigned code point), in a row written since its stored keys were counted; asked again " +
159
+ "in a minute, the error names the character (Mason gap libc-uncovered)" };
160
+ }
161
+ return { code: "XX000", message: `a stored value of ${pathText(r)} could not be computed when its row was written (a Mason bug: report it, with mason logs); ` +
162
+ "asked again in a minute, it is computed from the row's document" };
163
+ }
164
+ /** the functions that raise `markedError` at a marked row (src/duck/functions.ts): of the column read (a key as its hex,
165
+ * so neither is a constant DuckDB would fold), the code and the message; a key's answers a BLOB, a path's a VARCHAR */
166
+ export const MARKED_FUNCTIONS = { key: "__mason_marked_key", value: "__mason_marked_value" };
167
+ const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
168
+ const kindOf = (n) => isObject(n) && Object.keys(n).length === 1 && /^[A-Z]/.test(Object.keys(n)[0]) ? Object.keys(n)[0] : null;
169
+ const bodyOf = (n) => n[kindOf(n)];
170
+ const sval = (n) => kindOf(n) === "String" ? String(bodyOf(n).sval ?? "") : null;
171
+ /** the schemas a bare table name is looked up in, as the session's search_path says (`"$user"` names none here) */
172
+ export function searchSchemas(searchPath) {
173
+ return searchPath.split(",").map((s) => s.trim()).filter((s) => s !== "" && s !== '"$user"' && s !== "$user")
174
+ .map((s) => s.startsWith('"') && s.endsWith('"') ? s.slice(1, -1).replaceAll('""', '"') : s.toLowerCase());
175
+ }
176
+ /** the RangeVars a FROM list names itself, through its joins (never inside a subquery: its columns are its list's) */
177
+ function directTables(from) {
178
+ const out = [];
179
+ const visit = (v) => {
180
+ if (Array.isArray(v)) {
181
+ v.forEach(visit);
182
+ return;
183
+ }
184
+ const k = kindOf(v);
185
+ if (k === "RangeVar")
186
+ out.push(bodyOf(v));
187
+ else if (k === "JoinExpr") {
188
+ visit(bodyOf(v).larg);
189
+ visit(bodyOf(v).rarg);
190
+ }
191
+ };
192
+ visit(from);
193
+ return out;
194
+ }
195
+ /** the name a FROM item's columns are qualified by: its alias, else its table's name */
196
+ const aliasOf = (rv) => isObject(rv.alias) ? String(rv.alias.aliasname ?? "") : String(rv.relname ?? "");
197
+ /**
198
+ * WHETHER A STATEMENT LISTS A TABLE'S COLUMNS WITHOUT NAMING THEM (`hideKept`): a SELECT whose list holds a bare `*`
199
+ * and whose FROM names a table itself (a `*` over a subquery lists the subquery's columns); a `t.*` anywhere whose `t`
200
+ * is a table's name or alias in the statement; a NATURAL JOIN with a table on either side; or one bare name equal to a
201
+ * table's name or alias (a whole row — `row_to_json(t)`, `SELECT t FROM t` — or a column of that name, taken as one).
202
+ */
203
+ export function exposes(tree) {
204
+ const aliases = new Set();
205
+ const collect = (v) => {
206
+ if (Array.isArray(v)) {
207
+ v.forEach(collect);
208
+ return;
209
+ }
210
+ if (!isObject(v))
211
+ return;
212
+ const k = kindOf(v);
213
+ if (k === "RangeVar")
214
+ aliases.add(aliasOf(bodyOf(v)));
215
+ for (const x of Object.values(k === null ? v : bodyOf(v)))
216
+ collect(x);
217
+ };
218
+ collect(tree);
219
+ const star = (n) => kindOf(n) === "ColumnRef" && Array.isArray(bodyOf(n).fields) && bodyOf(n).fields.length === 1 && kindOf(bodyOf(n).fields[0]) === "A_Star";
220
+ const found = (v) => {
221
+ if (Array.isArray(v))
222
+ return v.some(found);
223
+ if (!isObject(v))
224
+ return false;
225
+ const k = kindOf(v);
226
+ const b = k === null ? v : bodyOf(v);
227
+ if (k === "SelectStmt" && Array.isArray(b.targetList) && directTables(b.fromClause).length > 0
228
+ && b.targetList.some((t) => kindOf(t) === "ResTarget" && star(bodyOf(t).val)))
229
+ return true;
230
+ if (k === "JoinExpr" && b.isNatural === true && directTables([b.larg, b.rarg]).length > 0)
231
+ return true;
232
+ if (k === "ColumnRef" && Array.isArray(b.fields)) {
233
+ const fields = b.fields;
234
+ const last = fields[fields.length - 1];
235
+ if (fields.length >= 2 && kindOf(last) === "A_Star" && aliases.has(sval(fields[fields.length - 2]) ?? ""))
236
+ return true;
237
+ if (fields.length === 1 && aliases.has(sval(fields[0]) ?? "\u0000"))
238
+ return true;
239
+ }
240
+ return Object.values(b).some(found);
241
+ };
242
+ return found(tree);
243
+ }
244
+ /**
245
+ * THE HIDDEN COLUMNS KEPT OUT OF THE ANSWER (the rewritten tree, before it is printed). A statement that lists a table's
246
+ * columns without naming them (`exposes`) — a hidden column is a column to DuckDB, and would be listed — reads every
247
+ * copy it names through that copy's view of its visible columns (`VISIBLE_CATALOG`), and is rewritten with no stored
248
+ * read at all (src/duck/plan.ts: a stored read names no document to fall back on, so it cannot be turned back into its
249
+ * call here), so it answers exactly what it answered before any column was stored — a plan kept from before a table's
250
+ * first stored column included. Every other statement reads the copies themselves, naming only what it reads.
251
+ *
252
+ * A table name is resolved as Postgres resolved it: a qualified one is that table; a bare one is a WITH query of that
253
+ * name in scope (left alone), else the first schema on the search path that has a served copy of it. Answers whether the
254
+ * statement exposed its columns. The tree is changed in place.
255
+ */
256
+ export function hideKept(tree, ctx) {
257
+ if (!exposes(tree))
258
+ return false;
259
+ // every copy it names, read through its view; a WITH query's name in scope is the WITH query's
260
+ const redirect = (v, ctes) => {
261
+ if (Array.isArray(v)) {
262
+ for (const x of v)
263
+ redirect(x, ctes);
264
+ return;
265
+ }
266
+ if (!isObject(v))
267
+ return;
268
+ const k = kindOf(v);
269
+ if (k === "RangeVar") {
270
+ const b = bodyOf(v);
271
+ if (typeof b.catalogname === "string" && b.catalogname !== "")
272
+ return;
273
+ const name = String(b.relname ?? "");
274
+ const schema = typeof b.schemaname === "string" && b.schemaname !== "" ? b.schemaname
275
+ : ctes.has(name) ? null : ctx.searchPath.find((s) => ctx.visible.has(`${s}.${name}`)) ?? null;
276
+ if (schema !== null && ctx.visible.has(`${schema}.${name}`)) {
277
+ b.catalogname = VISIBLE_CATALOG;
278
+ b.schemaname = schema;
279
+ }
280
+ return;
281
+ }
282
+ if (k === "SelectStmt") {
283
+ const b = bodyOf(v);
284
+ const w = isObject(b.withClause) ? b.withClause : null;
285
+ const list = w !== null && Array.isArray(w.ctes) ? w.ctes.filter((c) => kindOf(c) === "CommonTableExpr").map(bodyOf) : [];
286
+ const all = new Set([...ctes, ...list.map((c) => String(c.ctename ?? ""))]);
287
+ // a WITH query sees the ones before it (all of them, RECURSIVE); the rest of the statement sees all
288
+ list.forEach((c, i) => redirect(c.ctequery, w?.recursive === true ? all : new Set([...ctes, ...list.slice(0, i).map((x) => String(x.ctename ?? ""))])));
289
+ for (const [key, x] of Object.entries(b))
290
+ if (key !== "withClause")
291
+ redirect(x, all);
292
+ return;
293
+ }
294
+ for (const x of Object.values(k === null ? v : bodyOf(v)))
295
+ redirect(x, ctes);
296
+ };
297
+ redirect(tree, new Set());
298
+ return true;
299
+ }