@mason-data/cli 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.md +101 -73
  2. package/dist/src/api/error.js +3 -0
  3. package/dist/src/api/postgres/catalog-worker.js +49 -2
  4. package/dist/src/api/postgres/catalog.js +368 -25
  5. package/dist/src/api/postgres/encode.js +3 -1
  6. package/dist/src/api/postgres/explain.js +3 -2
  7. package/dist/src/api/postgres/listener.js +11 -3
  8. package/dist/src/api/postgres/protocol.js +39 -23
  9. package/dist/src/api/postgres/proxy.js +283 -0
  10. package/dist/src/api/postgres/schema.js +6 -0
  11. package/dist/src/api/postgres/server.js +1617 -245
  12. package/dist/src/api/postgres/translate.js +127 -10
  13. package/dist/src/api/request-log.js +18 -5
  14. package/dist/src/cdc/poll.js +5 -5
  15. package/dist/src/cli/background.js +47 -25
  16. package/dist/src/cli/bridge-stdin.js +101 -0
  17. package/dist/src/cli/cancel.js +75 -0
  18. package/dist/src/cli/command.js +170 -11
  19. package/dist/src/cli/commands.js +804 -209
  20. package/dist/src/cli/control.js +59 -0
  21. package/dist/src/cli/credentials.js +182 -0
  22. package/dist/src/cli/demo.js +20 -11
  23. package/dist/src/cli/deploy.js +574 -0
  24. package/dist/src/cli/dev.js +491 -130
  25. package/dist/src/cli/doctor.js +131 -38
  26. package/dist/src/cli/generated-files.js +143 -0
  27. package/dist/src/cli/guides.js +175 -36
  28. package/dist/src/cli/hosted.js +850 -0
  29. package/dist/src/cli/init.js +162 -65
  30. package/dist/src/cli/login.js +289 -0
  31. package/dist/src/cli/machine.js +13 -12
  32. package/dist/src/cli/main.js +11 -5
  33. package/dist/src/cli/mason.cjs +17 -0
  34. package/dist/src/cli/mcp.js +31 -22
  35. package/dist/src/cli/metrics.js +79 -0
  36. package/dist/src/cli/output-gate.js +176 -0
  37. package/dist/src/cli/page-share.js +96 -0
  38. package/dist/src/cli/pg-tls.js +14 -4
  39. package/dist/src/cli/pg-url.js +4 -0
  40. package/dist/src/cli/pin.js +19 -14
  41. package/dist/src/cli/pool-size.cjs +85 -0
  42. package/dist/src/cli/ports.js +2 -0
  43. package/dist/src/cli/preflight.js +138 -46
  44. package/dist/src/cli/project.js +142 -4
  45. package/dist/src/cli/readiness.js +76 -6
  46. package/dist/src/cli/registry.js +65 -38
  47. package/dist/src/cli/replica-hosted.js +665 -0
  48. package/dist/src/cli/replica.js +76 -0
  49. package/dist/src/cli/replication-checks.js +7 -5
  50. package/dist/src/cli/replication-mode.js +105 -26
  51. package/dist/src/cli/request-filter.js +134 -0
  52. package/dist/src/cli/roles.js +585 -0
  53. package/dist/src/cli/run-server.js +637 -48
  54. package/dist/src/cli/runs.js +1 -1
  55. package/dist/src/cli/security.js +67 -18
  56. package/dist/src/cli/serve.js +204 -46
  57. package/dist/src/cli/share-run.js +329 -0
  58. package/dist/src/cli/share.js +286 -0
  59. package/dist/src/cli/source-edit.js +75 -17
  60. package/dist/src/cli/source-role.js +344 -0
  61. package/dist/src/cli/sources.js +306 -47
  62. package/dist/src/cli/style.js +2 -1
  63. package/dist/src/cli/supervise.js +27 -24
  64. package/dist/src/cli/sync.js +55 -25
  65. package/dist/src/cli/token-hosted.js +337 -0
  66. package/dist/src/cli/tunnel.js +200 -0
  67. package/dist/src/cli/ui-ssh.js +460 -0
  68. package/dist/src/cli/ui.js +124 -17
  69. package/dist/src/cli/usage.js +128 -0
  70. package/dist/src/cli/user-hosted.js +299 -0
  71. package/dist/src/cli/user.js +573 -65
  72. package/dist/src/cli/version.js +9 -7
  73. package/dist/src/demo/seed.js +29 -0
  74. package/dist/src/duck/analyze.js +36 -20
  75. package/dist/src/duck/cancel.js +10 -1
  76. package/dist/src/duck/collation.js +266 -0
  77. package/dist/src/duck/constant-key.js +94 -0
  78. package/dist/src/duck/distinct-key.js +283 -26
  79. package/dist/src/duck/events.js +165 -0
  80. package/dist/src/duck/executor-worker.js +20 -11
  81. package/dist/src/duck/executor.js +47 -33
  82. package/dist/src/duck/explain.js +60 -46
  83. package/dist/src/duck/extensions.js +160 -0
  84. package/dist/src/duck/functions.js +461 -63
  85. package/dist/src/duck/gaps.js +48 -21
  86. package/dist/src/duck/generated-plan.js +1070 -0
  87. package/dist/src/duck/generated.js +906 -0
  88. package/dist/src/duck/grants.js +176 -0
  89. package/dist/src/duck/held-types.js +64 -0
  90. package/dist/src/duck/history.js +127 -0
  91. package/dist/src/duck/jsonb-order.js +9 -12
  92. package/dist/src/duck/kept-route.js +6 -5
  93. package/dist/src/duck/kept.js +15 -13
  94. package/dist/src/duck/late.js +1469 -40
  95. package/dist/src/duck/libc-bound.js +341 -0
  96. package/dist/src/duck/libc-order.js +892 -126
  97. package/dist/src/duck/pg-eval-worker.js +7 -1
  98. package/dist/src/duck/pg-eval.js +6 -4
  99. package/dist/src/duck/pgtree.js +774 -23
  100. package/dist/src/duck/plan.js +369 -47
  101. package/dist/src/duck/publication.js +20 -6
  102. package/dist/src/duck/requests.js +269 -93
  103. package/dist/src/duck/resolved.js +117 -2
  104. package/dist/src/duck/rewrite.js +4584 -580
  105. package/dist/src/duck/row-filter.js +655 -0
  106. package/dist/src/duck/statement.js +19 -0
  107. package/dist/src/duck/store.js +439 -177
  108. package/dist/src/duck/stream.js +19 -10
  109. package/dist/src/duck/sync.js +1411 -368
  110. package/dist/src/duck/token.js +4 -1
  111. package/dist/src/duck/types.js +69 -12
  112. package/dist/src/duck/usage.js +326 -0
  113. package/dist/src/duck/users.js +69 -15
  114. package/dist/src/duck/verified.js +161 -1
  115. package/dist/src/duck/views.js +5 -5
  116. package/dist/src/lib/database-name.js +62 -0
  117. package/dist/src/lib/database-url.js +55 -10
  118. package/dist/src/lib/dotenv.js +17 -4
  119. package/dist/src/lib/instant.js +50 -0
  120. package/dist/src/lib/log-buffer.js +77 -0
  121. package/dist/src/lib/log-read.js +196 -0
  122. package/dist/src/lib/pg-numeric.js +390 -49
  123. package/dist/src/lib/project-version.js +23 -23
  124. package/dist/src/lib/secret-mask.js +142 -0
  125. package/dist/src/lib/table-sync.js +282 -0
  126. package/dist/src/web/app.css +706 -66
  127. package/dist/src/web/app.js +331 -409
  128. package/dist/src/web/charts.js +229 -0
  129. package/dist/src/web/index.html +82 -18
  130. package/dist/src/web/pages/queries.js +192 -0
  131. package/dist/src/web/pages/status.js +611 -0
  132. package/dist/src/web/pages/syncs.js +155 -0
  133. package/dist/src/web/pages/tables.js +439 -0
  134. package/dist/src/web/pages/usage.js +397 -0
  135. package/dist/src/web/pages/users.js +219 -0
  136. package/dist/src/web/shared.js +584 -0
  137. package/dist/src/web/vendor/README.md +25 -0
  138. package/dist/src/web/vendor/geist-1.7.2/Geist-Variable.woff2 +0 -0
  139. package/dist/src/web/vendor/geist-1.7.2/GeistMono-Variable.woff2 +0 -0
  140. package/dist/src/web/vendor/geist-1.7.2/LICENSE.txt +92 -0
  141. package/dist/src/web/vendor/lucide-1.56.0/LICENSE.txt +43 -0
  142. package/dist/src/web/vendor/lucide-1.56.0/chart-column.mjs +29 -0
  143. package/dist/src/web/vendor/lucide-1.56.0/layout-dashboard.mjs +29 -0
  144. package/dist/src/web/vendor/lucide-1.56.0/logs.mjs +34 -0
  145. package/dist/src/web/vendor/lucide-1.56.0/menu.mjs +28 -0
  146. package/dist/src/web/vendor/lucide-1.56.0/square-terminal.mjs +28 -0
  147. package/dist/src/web/vendor/lucide-1.56.0/table.mjs +29 -0
  148. package/dist/src/web/vendor/lucide-1.56.0/users.mjs +29 -0
  149. package/dist/src/web/vendor/lucide-1.56.0/x.mjs +27 -0
  150. package/dist/src/web/vendor/uplot-1.6.32/LICENSE.txt +21 -0
  151. package/dist/src/web/vendor/uplot-1.6.32/uPlot.esm.js +6140 -0
  152. package/dist/src/web/vendor/uplot-1.6.32/uPlot.min.css +1 -0
  153. package/dist/src/web/vendor/zilla-slab-5.3.0/LICENSE.txt +93 -0
  154. package/dist/src/web/vendor/zilla-slab-5.3.0/zilla-slab-latin-700-normal.woff2 +0 -0
  155. package/package.json +12 -4
@@ -0,0 +1,1070 @@
1
+ /**
2
+ * WHAT A GENERATED TABLE'S SQL IS, ASKED OF THE CATALOG POSTGRES (issue 124; src/duck/generated.ts keeps the table): the
3
+ * file's `sql:` planned as any statement is — analysed by Postgres, rewritten, held to the verified list, read back — and
4
+ * then held to what a table with no session can store. Everything here refuses, by file, key, reason and way out; none
5
+ * of it moves a table from one `kind:` to another.
6
+ *
7
+ * A TABLE HAS NO SESSION. So the plan is made in UTC under `public` (src/duck/generated.ts sets the connection it runs on
8
+ * to the same), and anything whose answer a session's settings decide is refused rather than given a default: a default
9
+ * zone would give wrong days with no error. Postgres already says which those are (`sessionReads`): a function, an
10
+ * operator or a text cast it marks IMMUTABLE reads no session, by its own rule for what an index may hold. The two
11
+ * exceptions are named: `age(timestamptz, timestamptz)` is marked immutable and reads the zone, `to_char(timestamp, …)`
12
+ * is marked stable and reads none of it, the zone being in the SQL by then. `now()` and its kin are instants, the run's.
13
+ *
14
+ * WHAT IS STORED IS WHAT A COPY OF THAT TYPE HOLDS (`STORED_TYPES`): the column's own Postgres type, so a statement over
15
+ * the table is answered by the very path that answers one over a copy. A `numeric`, an enum and a citext are stored as
16
+ * the engine answers them (`duck: null`), which is what a copy of that column holds: a numeric's DECIMAL, whose display
17
+ * scale the catalog's typmod pads on read; an enum's ENUM, in its declared order; a citext's text (stage 3, issue 175). A
18
+ * numeric whose digits a statement computes at the result's edge (`DuckPlan.exact`) is stored where Postgres's types fix
19
+ * its scale, written as the engine's DECIMAL arithmetic (`storedNumeric`); one they don't is refused with its own reason
20
+ * (`notStoredRefusal`). Any other type is refused by name.
21
+ */
22
+ import { deparseSync } from "pgsql-deparser";
23
+ import { parseSql, PostgresSyntaxError } from "../api/postgres/parser.js";
24
+ import { describeKeys } from "./resolved.js";
25
+ import { treeDifference } from "./rewrite.js";
26
+ import { StatementError } from "./statement.js";
27
+ import { exprType } from "./pgtree.js";
28
+ /** the Postgres type of an output column, by OID, as a copy stores it (src/duck/types.ts writes each back as Postgres's) */
29
+ const STORED_TYPES = new Map([
30
+ [16, "BOOLEAN"], [17, "BLOB"], [20, "BIGINT"], [21, "SMALLINT"], [23, "INTEGER"], [25, "VARCHAR"], [114, "JSON"], [700, "FLOAT"], [701, "DOUBLE"], [1043, "VARCHAR"],
31
+ [1082, "DATE"], [1083, "TIME"], [1114, "TIMESTAMP"], [1184, "TIMESTAMPTZ"], [1186, "INTERVAL"], [2950, "UUID"], [3802, "JSON"],
32
+ ]);
33
+ const NUMERIC_OID = 1700;
34
+ /** the most decimals a stored DECIMAL(38, s) may keep: past it a sum could overflow its 38 digits and fail the write */
35
+ const MAX_STORED_SCALE = 18;
36
+ /** the Postgres name `?column?` is an expression no one named, and a table's column needs one */
37
+ const UNNAMED = "?column?";
38
+ /** what the subquery of a definition with an `order_by:` is called: its output's columns are all it can read */
39
+ const SHELL_ALIAS = "__mason_generated";
40
+ const SHELL_KEYS = new Set(["targetList", "fromClause", "sortClause", "limitOption", "op"]);
41
+ const PER_VALUE = new Set(["sqrt", "exp", "ln", "log", "log10", "power", "pow"]);
42
+ const FIXES = new Set(["round", "trunc", "floor", "ceil", "ceiling"]);
43
+ const DOUBLES = new Set([700, 701]);
44
+ const TEXTS = new Set([25, 1042, 1043]);
45
+ const JSONS = new Set([114, 3802]);
46
+ const rec = (v) => typeof v === "object" && v !== null ? v : {};
47
+ const lastName = (names) => {
48
+ const list = Array.isArray(names) ? names : [];
49
+ const s = rec(rec(list[list.length - 1]).String).sval;
50
+ return typeof s === "string" ? s.toLowerCase() : "";
51
+ };
52
+ const isForm = (s) => s !== null && s !== "double";
53
+ const isTree = (v) => v !== undefined && v !== null && typeof v === "object" && !Array.isArray(v);
54
+ const int = (v) => typeof v === "string" && /^-?\d+$/.test(v) ? Number(v) : -1;
55
+ /** the nodes whose values an analysed node computes from: a call's or an operator's arguments, an aggregate's, the
56
+ * results of a CASE (never its conditions), a coercion's argument, a scalar sublink's subselect's column, what a VAR
57
+ * reads (`followed`: a subquery's, a CTE's or each set operation arm's column) */
58
+ function inputsOf(n) {
59
+ if (n._node === "AGGREF")
60
+ return (Array.isArray(n.args) ? n.args : []).map((e) => isTree(e) && isTree(e.expr) ? e.expr : null).filter(isTree);
61
+ if (n._node === "CASEEXPR")
62
+ return [...(Array.isArray(n.args) ? n.args : []).map((w) => isTree(w) ? w.result : null), n.defresult].filter(isTree);
63
+ if (n._node === "SUBLINK") {
64
+ const q = n.subselect, first = isTree(q) && Array.isArray(q.targetList) ? q.targetList.find((e) => isTree(e) && e.resjunk !== "true") : undefined;
65
+ return isTree(first) && isTree(first.expr) ? [first.expr] : [];
66
+ }
67
+ if (n._node === "VAR")
68
+ return Array.isArray(n.followed) ? n.followed.filter(isTree) : [];
69
+ if (Array.isArray(n.args))
70
+ return n.args.filter(isTree);
71
+ return isTree(n.arg) ? [n.arg] : [];
72
+ }
73
+ /** a form born at a node: its kind, the user's function, and the node's offset as its one site */
74
+ const bornAt = (why, fn, at, from = null) => ({ why, fn, fixed: false, from, sites: [{ at, vars: [] }], way: null, scaled: null });
75
+ /**
76
+ * WHAT THE TEXT A NUMERIC IS READ FROM WAS MADE FROM (PR 202 round 12): a value's form survives every conversion, so the
77
+ * walk goes down through every text and JSON node over it (a cast, `||`, a jsonb build and `->>`, a WITH or subquery
78
+ * column, any other) to the values it was made from. A numeric there keeps its own form (`seen`: a variance, sqrt and
79
+ * its kin, a division, a cast from a double, a text read as a numeric), and a double the statement computes is "a cast
80
+ * from a double" at its node: either is the text's form, so a typmod over the text never stores the engine's double. Where
81
+ * none is, `from` says what the text was, as the analysis typed it: a JSON value's (an operator or a function over json or
82
+ * jsonb: `->>`, `json_extract_path_text`), a stored double's (a column or a constant), or any other text (a stored or
83
+ * exactly computed numeric's among them). Any other type stops the walk: an integer's, a date's or a boolean's text is
84
+ * exact whatever made it.
85
+ */
86
+ function textFrom(n, w) {
87
+ const made = madeOf(n, w);
88
+ return { form: made.form, from: made.from ?? "text" };
89
+ }
90
+ function madeOf(x, w) {
91
+ const done = w.made.get(x);
92
+ if (done !== undefined)
93
+ return done;
94
+ const made = madeAt(x, w);
95
+ w.made.set(x, made);
96
+ return made;
97
+ }
98
+ function madeAt(x, w) {
99
+ const type = exprType(x)?.type ?? -1, at = int(x.location);
100
+ if (x._node === "VAR" && inputsOf(x).length > 0) {
101
+ // a column read from a subquery, a CTE or a set operation: the first arm's form, every arm of it a site, through this VAR
102
+ const made = inputsOf(x).map((i) => madeOf(i, w)), from = made.find((m) => m.from !== null)?.from ?? null;
103
+ return { form: throughVar(made.map((m) => m.form), at), from };
104
+ }
105
+ if (DOUBLES.has(type)) {
106
+ if (x._node === "VAR" || x._node === "CONST")
107
+ return { form: null, from: "double" };
108
+ return { form: bornAt("double", "", at), from: null };
109
+ }
110
+ if (type === NUMERIC_OID) {
111
+ const s = seen(x, w);
112
+ return { form: isForm(s) ? s : null, from: null };
113
+ }
114
+ if (!TEXTS.has(type) && !JSONS.has(type))
115
+ return { form: null, from: null };
116
+ const args = Array.isArray(x.args) ? x.args.filter(isTree) : [];
117
+ let from = (x._node === "OPEXPR" || x._node === "FUNCEXPR") && args[0] !== undefined && JSONS.has(exprType(args[0])?.type ?? -1) ? "json" : null;
118
+ for (const i of inputsOf(x)) {
119
+ const m = madeOf(i, w);
120
+ from ??= m.from;
121
+ if (m.form !== null)
122
+ return { form: m.form, from };
123
+ }
124
+ return { form: null, from };
125
+ }
126
+ /** the most sites a form's way out is written at: a statement whose set operations each read the level below in two
127
+ * arms doubles them at every level, so past this the reason stands and the way out is the column's whole (round 14) */
128
+ const MAX_SITES = 64;
129
+ /** a column read from a subquery, a CTE or a set operation (a VAR at `at`): the first arm's form says the reason, and
130
+ * every arm of that form is a site of the way out, reached through this VAR; at most one past `MAX_SITES` kept */
131
+ function throughVar(below, at) {
132
+ const forms = below.filter(isForm), first = forms[0];
133
+ if (first === undefined)
134
+ return null;
135
+ const sites = forms.filter((f) => f.why === first.why).flatMap((f) => f.sites).slice(0, MAX_SITES + 1)
136
+ .map((s) => ({ at: s.at, vars: at >= 0 ? [at, ...s.vars] : s.vars }));
137
+ return { ...first, sites };
138
+ }
139
+ /** the walk: each node's type first (a double is a double, anything but a numeric carries nothing), then the form below
140
+ * it passed up, marked by what is over it; `names` the functions' and operators' names, by `f<oid>` and `o<oid>` */
141
+ function seen(n, w) {
142
+ const done = w.seen.get(n);
143
+ if (done !== undefined)
144
+ return done;
145
+ const s = seenAt(n, w);
146
+ w.seen.set(n, s);
147
+ return s;
148
+ }
149
+ function seenAt(n, w) {
150
+ const names = w.names, t = exprType(n);
151
+ if (t !== null && DOUBLES.has(t.type))
152
+ return "double";
153
+ if (t === null || t.type !== NUMERIC_OID)
154
+ return null;
155
+ const inputs = inputsOf(n), below = inputs.map((x) => seen(x, w));
156
+ const inner = below.find(isForm) ?? null, at = int(n.location);
157
+ const born = (why, fn, from = null) => bornAt(why, fn, at, from);
158
+ // a text read as a numeric is the one form a node over it outranks: one scale for the text would not store a division,
159
+ // a variance or sqrt and its kin over it, so that node is the reason
160
+ const under = (outranks) => inner !== null && !(outranks && inner.why === "text") ? inner : null;
161
+ switch (n._node) {
162
+ case "FUNCEXPR": {
163
+ const cast = int(n.funcformat) === 1 || int(n.funcformat) === 2, typmod = t.typmod >= 0;
164
+ if (cast) {
165
+ // a numeric cast from a double; a typmod over a text read as a numeric gives it its one scale
166
+ if (below[0] === "double")
167
+ return { ...born("double", ""), fixed: typmod };
168
+ return inner === null || (inner.why === "text" && typmod) ? null : { ...inner, fixed: inner.fixed || typmod };
169
+ }
170
+ const name = names.get(`f${int(n.funcid)}`) ?? "", kept = under(PER_VALUE.has(name) || name === "div");
171
+ if (kept !== null)
172
+ return { ...kept, fixed: kept.fixed || FIXES.has(name) };
173
+ return PER_VALUE.has(name) ? born("per-value", name) : name === "div" ? born("divides", name) : null;
174
+ }
175
+ case "AGGREF": {
176
+ const name = names.get(`f${int(n.aggfnoid)}`) ?? "";
177
+ return under(VARIANCES.has(name) || name === "avg") ?? (VARIANCES.has(name) ? born("variance", name) : name === "avg" ? born("divides", name) : null);
178
+ }
179
+ case "OPEXPR": {
180
+ const divides = names.get(`o${int(n.opno)}`) === "/";
181
+ return under(divides) ?? (divides ? born("divides", "/") : null);
182
+ }
183
+ // a text read as a numeric keeps the form of what the text was made from (round 12: a variance's text under a typmod
184
+ // stored the engine's double); where nothing beneath has one, it keeps the decimals each value's text has
185
+ case "COERCEVIAIO": {
186
+ if (inputs[0] === undefined || !TEXTS.has(exprType(inputs[0])?.type ?? -1))
187
+ return inner;
188
+ const made = textFrom(inputs[0], w);
189
+ return made.form ?? born("text", "", made.from);
190
+ }
191
+ // a column read from a subquery, a CTE or a set operation: the first arm's form says the reason, and every arm of
192
+ // that form is a site of the way out, reached through this VAR
193
+ case "VAR": return throughVar(below, at);
194
+ // a coalesce, a greatest or least, a CASE's results, a relabel, a sublink: the value it takes
195
+ default: return inner;
196
+ }
197
+ }
198
+ /** the functions and operators of analysed expressions, as `f<oid>` and `o<oid>` */
199
+ function oidsOf(v, out, met = new Set()) {
200
+ if (Array.isArray(v)) {
201
+ for (const x of v)
202
+ oidsOf(x, out, met);
203
+ return;
204
+ }
205
+ if (!isTree(v) || met.has(v))
206
+ return;
207
+ met.add(v);
208
+ if (v._node === "FUNCEXPR" && int(v.funcid) > 0)
209
+ out.f.add(int(v.funcid));
210
+ if (v._node === "AGGREF" && int(v.aggfnoid) > 0)
211
+ out.f.add(int(v.aggfnoid));
212
+ if (v._node === "OPEXPR" && int(v.opno) > 0)
213
+ out.o.add(int(v.opno));
214
+ for (const [k, x] of Object.entries(v))
215
+ if (k !== "_node")
216
+ oidsOf(x, out, met);
217
+ }
218
+ /** the user's own top SELECT's body (under an `order_by:` shell, the shell's subquery's) */
219
+ function userTop(stmt) {
220
+ const body = rec(rec(stmt).SelectStmt), targets = body.targetList;
221
+ const from = Array.isArray(body.fromClause) ? body.fromClause : [];
222
+ const sub = rec(rec(from[0]).RangeSubselect);
223
+ const star = Array.isArray(targets) ? rec(rec(rec(rec(targets[0]).ResTarget).val).ColumnRef).fields : undefined;
224
+ if (Array.isArray(targets) && targets.length === 1 && from.length === 1 && rec(sub.alias).aliasname === SHELL_ALIAS && Array.isArray(star) && "A_Star" in rec(star[0]))
225
+ return userTop(sub.subquery);
226
+ return body;
227
+ }
228
+ /** the user's own expression of each output column (the raw tree's targets), null for a set operation's */
229
+ function rawTargets(stmt) {
230
+ const targets = userTop(stmt).targetList;
231
+ return Array.isArray(targets) ? targets.map((t) => rec(rec(t).ResTarget).val) : null;
232
+ }
233
+ const typeName = (name, typmods = []) => ({ names: [{ String: { sval: "pg_catalog" } }, { String: { sval: name } }],
234
+ ...(typmods.length === 0 ? {} : { typmods: typmods.map((m) => ({ A_Const: { ival: { ival: m } } })) }), typemod: -1 });
235
+ const DOUBLE_TYPE = typeName("float8");
236
+ /** the one scale a way out gives a text read as a numeric */
237
+ const ONE_SCALE = typeName("numeric", [20, 4]);
238
+ const isNumericCast = (n) => "TypeCast" in rec(n) && lastName(rec(rec(rec(n).TypeCast).typeName).names) === "numeric";
239
+ const isFixer = (n) => isNumericCast(n) || ("FuncCall" in rec(n) && FIXES.has(lastName(rec(rec(n).FuncCall).funcname)));
240
+ const castTo = (n, type) => ({ TypeCast: { arg: n, typeName: type } });
241
+ /** a cast made a cast to `type` (a call such as `numeric(x)`: its argument cast) */
242
+ const retyped = (n, type) => "TypeCast" in rec(n) ? { TypeCast: { ...rec(rec(n).TypeCast), typeName: type } }
243
+ : castTo(rec(rec(n).FuncCall).args?.[0], type);
244
+ /** an expression of the raw tree, as the deparser writes it */
245
+ const exprText = (val) => deparseSync({ SelectStmt: { targetList: [{ ResTarget: { val } }], limitOption: "LIMIT_OPTION_DEFAULT", op: "SETOP_NONE" } }, { pretty: false }).replace(/^SELECT /, "");
246
+ function pathTo(root, hit) {
247
+ const path = [];
248
+ const find = (v, key) => {
249
+ if (Array.isArray(v))
250
+ return v.some((x) => find(x, key));
251
+ if (typeof v !== "object" || v === null)
252
+ return false;
253
+ const node = v;
254
+ path.push({ key, node });
255
+ if (hit(node) || Object.entries(node).some(([k, x]) => find(x, k)))
256
+ return true;
257
+ path.pop();
258
+ return false;
259
+ };
260
+ return find(root, "") ? path : null;
261
+ }
262
+ const located = (kinds, at) => (o) => kinds.some((k) => k in o && rec(o[k]).location === at);
263
+ /**
264
+ * WHERE A WAY OUT IS WRITTEN, for a path to a node of the raw tree: the output column's own expression (its ResTarget,
265
+ * `name` null; a scalar sublink is inside it), or the query of the statement the node is in, named in the user's words
266
+ * (`the WITH query t`, `the subquery s`, `the UNION ALL`: an arm of a set operation is written as the whole of it).
267
+ * Undefined where that query has no name the user wrote.
268
+ */
269
+ function placeOf(path, top) {
270
+ const i = path.findIndex((s) => "ResTarget" in s.node);
271
+ if (i < 1)
272
+ return undefined;
273
+ if (path[i - 1].node === top)
274
+ return { key: path[i].node, name: null };
275
+ let r = i - 1;
276
+ while (r > 0 && (path[r].key === "larg" || path[r].key === "rarg"))
277
+ r--;
278
+ const body = path[r].node;
279
+ if (body === top)
280
+ return { key: body, name: `the ${String(body.op).replace(/^SETOP_/, "")}${body.all === true ? " ALL" : ""}` };
281
+ const wrapper = path[r - 1], parent = path[r - 2], alias = rec(parent?.node.alias).aliasname;
282
+ if (wrapper?.key === "ctequery" && parent?.key === "CommonTableExpr")
283
+ return { key: body, name: `the WITH query ${String(parent.node.ctename)}` };
284
+ if (wrapper?.key === "subquery" && parent?.key === "RangeSubselect" && typeof alias === "string")
285
+ return { key: body, name: `the subquery ${alias}` };
286
+ return undefined;
287
+ }
288
+ /**
289
+ * THE WAY OUT IN THE USER'S OWN WORDS (`Form.way`, `Form.scaled`): their SQL with only the cast moved, every aggregate and
290
+ * wrapper kept, at each of the form's sites. As a double (`double`), the value is cast to double precision at the node
291
+ * the engine can't store, or above the outermost round, trunc, floor, ceil or numeric cast over it, through every query
292
+ * the walk followed it across (a round of a double takes no decimals, and a numeric cast would make it a numeric again):
293
+ * a numeric cast there is made the cast to double, a numeric cast from a double with nothing over it is taken away, and a
294
+ * text's cast to numeric is made the cast to double. Given one scale (`scale`, a text read as a numeric), the text's cast
295
+ * is given `numeric(20,4)`. Written where the cast lands (`placeOf`); null where a node is not found in the user's text.
296
+ */
297
+ function wayOf(stmt, f, mode) {
298
+ const top = userTop(stmt), swaps = new Map(), places = new Map();
299
+ for (const site of f.sites) {
300
+ const born = pathTo(stmt, located(["FuncCall", "TypeCast", "A_Expr"], site.at));
301
+ if (born === null)
302
+ return null;
303
+ const target = born[born.length - 1].node;
304
+ // the expressions over the node within each query, the column's own first, through each VAR the walk followed
305
+ const segments = [...site.vars.map((v) => pathTo(stmt, located(["ColumnRef"], v))).filter((p) => p !== null), born];
306
+ const over = (p) => p.slice(p.findIndex((s) => "ResTarget" in s.node) + 1, -1).find((s) => isFixer(s.node));
307
+ const fixed = mode === "scale" ? undefined : segments.map((p) => ({ p, s: over(p) })).find((x) => x.s !== undefined);
308
+ const [node, path] = fixed?.s !== undefined ? [fixed.s.node, fixed.p] : [target, born];
309
+ swaps.set(node, mode === "scale" ? retyped(target, ONE_SCALE)
310
+ : fixed !== undefined ? (isNumericCast(node) ? retyped(node, DOUBLE_TYPE) : castTo(node, DOUBLE_TYPE))
311
+ : !isNumericCast(target) ? castTo(target, DOUBLE_TYPE) : f.why === "double" ? rec(rec(target).TypeCast).arg : retyped(target, DOUBLE_TYPE));
312
+ const place = placeOf(path, top);
313
+ if (place === undefined)
314
+ return null;
315
+ places.set(place.key, place.name);
316
+ }
317
+ const swap = (v) => swaps.has(v) ? swaps.get(v) : Array.isArray(v) ? v.map(swap)
318
+ : typeof v === "object" && v !== null ? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, swap(x)])) : v;
319
+ const all = [...places];
320
+ if (all.length === 1 && all[0][1] === null)
321
+ return { expr: exprText(swap(rec(rec(all[0][0]).ResTarget).val)) };
322
+ if (all.some(([, name]) => name === null))
323
+ return null;
324
+ return { places: all.map(([body, name]) => ({ place: name, select: deparseSync({ SelectStmt: swap(body) }, { pretty: false }) })) };
325
+ }
326
+ /** each output column's form (`DefinitionPlan.forms`), read from its analysed expression, with its ways out written over
327
+ * the user's own SQL: null where it has none */
328
+ async function formsOf(rows, outputs, stmt, n) {
329
+ if (outputs === undefined || outputs.length !== n)
330
+ return Array(n).fill(null);
331
+ const oids = { f: new Set(), o: new Set() };
332
+ oidsOf(outputs, oids);
333
+ const names = new Map(oids.f.size + oids.o.size === 0 ? [] : (await rows(`SELECT 'f' || oid::text, proname::text FROM pg_proc WHERE oid IN (${[0, ...oids.f].join(", ")}) UNION ALL SELECT 'o' || oid::text, oprname::text FROM pg_operator WHERE oid IN (${[0, ...oids.o].join(", ")})`))
334
+ .map(([k, name]) => [k, name]));
335
+ const w = { names, seen: new Map(), made: new Map() };
336
+ return outputs.map((e) => {
337
+ const s = e === null ? null : seen(e, w);
338
+ if (!isForm(s))
339
+ return null;
340
+ const found = s.sites.length <= MAX_SITES && s.sites.every((x) => x.at >= 0);
341
+ return { ...s, way: found ? wayOf(stmt, s, "double") : null, scaled: found && s.why === "text" ? wayOf(stmt, s, "scale") : null };
342
+ });
343
+ }
344
+ const asRecord = (v) => v;
345
+ const WAY_OUT_DOCS = "write it as a SELECT: one statement, reading the tables the sources file syncs";
346
+ /** the file's `sql:` parsed: one SELECT, or the refusal */
347
+ async function selectOf(sql) {
348
+ let stmts;
349
+ try {
350
+ stmts = (await parseSql(sql)).stmts ?? [];
351
+ }
352
+ catch (e) {
353
+ if (!(e instanceof PostgresSyntaxError))
354
+ throw e;
355
+ return { refused: { key: "sql", message: `it is not SQL Postgres reads: ${e.message}`, why: null, options: [WAY_OUT_DOCS] } };
356
+ }
357
+ const only = stmts[0]?.stmt;
358
+ if (stmts.length !== 1 || only === undefined)
359
+ return { refused: { key: "sql", message: `it holds ${stmts.length} statements`, why: "A generated table is what one SELECT gives.", options: [WAY_OUT_DOCS] } };
360
+ const body = asRecord(only).SelectStmt;
361
+ if (body === undefined)
362
+ return { refused: { key: "sql", message: "it is not a SELECT", why: "Mason reads and stores: a generated table is what a SELECT gives.", options: [WAY_OUT_DOCS] } };
363
+ if (body.intoClause !== undefined || body.lockingClause !== undefined)
364
+ return { refused: { key: "sql", message: "it is a SELECT with an INTO or a locking clause", why: "Neither means anything in a table Mason keeps.", options: ["remove the clause"] } };
365
+ return { stmt: only };
366
+ }
367
+ /** a top-level ORDER BY with no LIMIT: it can't change which rows the table holds, so nothing is refused for it */
368
+ const UNUSED_ORDER = "sql: its ORDER BY doesn't change what the table holds. Set order_by: for how its rows are laid out, and sort in the statements that read it.";
369
+ /**
370
+ * THE STATEMENT A DEFINITION RUNS: its `sql:`, and with an `order_by:` that SELECT read by a shell sorted by it, so Postgres
371
+ * resolves the order over the output's columns and the rewrite sorts as a statement sorts. The shell is held to its own
372
+ * shape and the `sql:` put in as a tree, never pasted into text: `order_by:` is one clause, so `x LIMIT 1` or `x; DROP`
373
+ * is refused.
374
+ */
375
+ export async function definitionStatement(sql, orderBy) {
376
+ const select = await selectOf(sql);
377
+ if ("refused" in select)
378
+ return select;
379
+ const top = asRecord(select.stmt).SelectStmt;
380
+ const warnings = top.sortClause !== undefined && top.limitCount === undefined && top.limitOffset === undefined ? [UNUSED_ORDER] : [];
381
+ if (orderBy === null)
382
+ return { text: sql, stmt: select.stmt, warnings };
383
+ const bad = (why) => ({ refused: { key: "order_by", message: `is not what follows ORDER BY over the table's columns: ${why}`, why: null, options: ["write one SQL string, like order_by: day, tenant_id"] } });
384
+ let stmts;
385
+ try {
386
+ stmts = (await parseSql(`SELECT * FROM (SELECT 1) AS ${SHELL_ALIAS} ORDER BY ${orderBy}`)).stmts ?? [];
387
+ }
388
+ catch (e) {
389
+ if (!(e instanceof PostgresSyntaxError))
390
+ throw e;
391
+ return bad(e.message);
392
+ }
393
+ const shell = stmts[0]?.stmt, body = asRecord(shell ?? {}).SelectStmt;
394
+ if (stmts.length !== 1 || shell === undefined || body === undefined)
395
+ return bad("it ends the statement");
396
+ const stray = Object.keys(body).filter((k) => !SHELL_KEYS.has(k));
397
+ const from = body.fromClause ?? [];
398
+ if (stray.length > 0 || from.length !== 1 || (body.op !== undefined && body.op !== "SETOP_NONE") || (body.limitOption !== undefined && body.limitOption !== "LIMIT_OPTION_DEFAULT"))
399
+ return bad("it says more than an order");
400
+ asRecord(from[0]).RangeSubselect.subquery = select.stmt;
401
+ const text = deparseSync(shell, { pretty: false });
402
+ const back = (await parseSql(text)).stmts?.[0]?.stmt;
403
+ const difference = back === undefined ? "nothing" : treeDifference(back, shell);
404
+ if (back === undefined || difference !== null)
405
+ throw new Error(`a generated table's statement does not read back as itself (Mason bug: report it): ${difference}`);
406
+ return { text, stmt: back, warnings };
407
+ }
408
+ const THE_SESSION_WHY = "A generated table has no session, so nothing may depend on one's time zone or settings: a default would give wrong days with no error.";
409
+ const THE_SESSION_WAYS = [
410
+ "name the zone in the SQL: t AT TIME ZONE 'UTC', or date_trunc('day', t, 'UTC')",
411
+ "for today, (now() AT TIME ZONE 'UTC')::date",
412
+ ];
413
+ /** the SQLVALUEFUNCTION kinds that are instants, the run's (`field:SQLVALUEFUNCTION.op=`): CURRENT_TIMESTAMP and (n) */
414
+ const INSTANTS = new Set(["3", "4"]);
415
+ /** Marked stable or volatile, and reading no session here: the clock, as of the run (`DuckPlan.clock`), and a timestamp's
416
+ * text, whose zone the SQL has already said */
417
+ const NOT_THE_SESSION = ["now()", "transaction_timestamp()", "statement_timestamp()", "clock_timestamp()", "random()", "to_char(timestamp without time zone, text)"];
418
+ /** Marked immutable, and reading the session's zone (timestamptz_age splits both instants in it) */
419
+ const READS_ZONE_ANYWAY = ["age(timestamp with time zone, timestamp with time zone)"];
420
+ /**
421
+ * What the plan's keys read of a session, as a person reads each (none: nothing). The keys are what Postgres resolved the
422
+ * statement to (src/duck/resolved.ts): a function by its OID, an operator by its, a cast through text by its two types'
423
+ * output and input functions, read back from the catalog so nothing here is a list of names to keep current.
424
+ */
425
+ export async function sessionReads(rows, keys) {
426
+ // the listener's own current_setting (src/api/postgres/catalog.ts), which reads a session's settings by construction
427
+ const out = keys.filter((k) => /^function:ext:current_setting\(/.test(k));
428
+ const values = (kind) => keys.filter((k) => k.startsWith(`${kind}:`)).map((k) => k.slice(kind.length + 1));
429
+ for (const k of keys) {
430
+ const m = /^field:SQLVALUEFUNCTION\.op=(\d+)$/.exec(k);
431
+ if (m !== null && !INSTANTS.has(m[1]))
432
+ out.push(k);
433
+ }
434
+ const procs = [...values("function"), ...values("aggregate"), ...values("window")].filter((v) => /^\d+$/.test(v));
435
+ const allowed = NOT_THE_SESSION.map((s) => `'${s}'::regprocedure::oid`).join(", ");
436
+ const denied = READS_ZONE_ANYWAY.map((s) => `'${s}'::regprocedure::oid`).join(", ");
437
+ if (procs.length > 0) {
438
+ // a stable function over "any" or a polymorphic type is stable for its arguments' text alone: `textReads` judges it
439
+ const hit = await rows(`SELECT p.oid::text FROM pg_proc p WHERE p.oid IN (${procs.join(", ")}) AND (p.oid IN (${denied}) OR (p.provolatile <> 'i' AND p.oid NOT IN (${allowed}) ` +
440
+ `AND NOT (p.provolatile = 's' AND ${FOR_ITS_ARGUMENTS})))`);
441
+ const set = new Set(hit.map(([o]) => o));
442
+ for (const k of keys) {
443
+ const v = k.slice(k.indexOf(":") + 1);
444
+ if (/^(function|aggregate|window):/.test(k) && set.has(v))
445
+ out.push(k);
446
+ }
447
+ }
448
+ const ops = values("operator").filter((v) => /^\d+$/.test(v));
449
+ if (ops.length > 0) {
450
+ const hit = new Set((await rows(`SELECT o.oid::text FROM pg_operator o JOIN pg_proc p ON p.oid = o.oprcode WHERE o.oid IN (${ops.join(", ")}) AND (p.oid IN (${denied}) OR (p.provolatile <> 'i' AND p.oid NOT IN (${allowed})))`)).map(([o]) => o));
451
+ for (const v of ops)
452
+ if (hit.has(v))
453
+ out.push(`operator:${v}`);
454
+ }
455
+ // a cast through text calls its type's output function, and the next type's input function (`date::text` reads DateStyle)
456
+ const casts = values("cast").map((v) => v.split(">")).filter((p) => p.length === 2 && p.every((x) => /^\d+$/.test(x)));
457
+ if (keys.includes("node:COERCEVIAIO") && casts.length > 0) {
458
+ const list = casts.map(([from, to]) => `(${from}, ${to})`).join(", ");
459
+ const hit = await rows(`SELECT c.a::text, c.b::text FROM (VALUES ${list}) AS c(a, b) JOIN pg_type f ON f.oid = c.a JOIN pg_type t ON t.oid = c.b ` +
460
+ `JOIN pg_proc o ON o.oid = f.typoutput::oid JOIN pg_proc i ON i.oid = t.typinput::oid WHERE o.provolatile <> 'i' OR i.provolatile <> 'i'`);
461
+ for (const [a, b] of hit)
462
+ out.push(`cast:${a}>${b}`);
463
+ }
464
+ if (out.length === 0)
465
+ return [];
466
+ const names = await describeKeys(rows, out);
467
+ return [...new Set(out.map((k) => names.get(k) ?? k))];
468
+ }
469
+ /** a definition that reads a session, `session` what it reads as a person reads each */
470
+ const sessionRefusal = (session) => ({ key: "sql", message: `it reads a session: ${session.slice(0, 5).join("; ")}`, why: THE_SESSION_WHY, options: THE_SESSION_WAYS });
471
+ /** a function (`p`) taking "any" or a polymorphic type: Postgres marks one stable when an argument's text may read a
472
+ * setting (`jsonb_build_object`, `concat`, `format`, `to_jsonb`, `jsonb_agg`) */
473
+ const FOR_ITS_ARGUMENTS = "EXISTS (SELECT 1 FROM unnest(p.proargtypes::oid[] || p.provariadic) AS a(t) JOIN pg_type y ON y.oid = a.t WHERE y.typtype = 'p')";
474
+ /**
475
+ * What a stable function over "any" or a polymorphic type reads of a session through its arguments' text, as a person
476
+ * reads each (none: nothing): a function given an argument whose type's text Postgres writes by a setting (a timestamptz,
477
+ * an interval, a date, an array or row of one). Judged by each argument the statement gives it (`argTypes`, src/duck/resolved.ts
478
+ * `argumentTypes`), from the type's own output function, as `sessionReads` judges a cast through text.
479
+ */
480
+ export async function textReads(rows, keys, argTypes) {
481
+ const procs = keys.filter((k) => /^(function|aggregate|window):\d+$/.test(k)).map((k) => k.slice(k.indexOf(":") + 1));
482
+ if (procs.length === 0)
483
+ return [];
484
+ const flagged = new Set((await rows(`SELECT p.oid::text FROM pg_proc p WHERE p.oid IN (${procs.join(", ")}) AND p.provolatile = 's' AND ${FOR_ITS_ARGUMENTS}`)).map(([o]) => o));
485
+ if (flagged.size === 0)
486
+ return [];
487
+ const pairs = argTypes.map((a) => a.split(":")).filter((x) => x.length === 2 && flagged.has(x[0]) && /^\d+$/.test(x[1]));
488
+ const out = [];
489
+ // a function the statement gives no argument of a known type: nothing shows what it reads, so it is refused
490
+ const unknown = [...flagged].filter((f) => !pairs.some(([g]) => g === f));
491
+ if (unknown.length > 0)
492
+ for (const [n] of await rows(`SELECT oid::regprocedure::text FROM pg_proc WHERE oid IN (${unknown.join(", ")})`))
493
+ out.push(n);
494
+ if (pairs.length > 0) {
495
+ const hit = await rows(`SELECT DISTINCT format('%s over %s', (SELECT proname FROM pg_proc WHERE oid = a.f), format_type(a.t, NULL)) FROM (VALUES ${pairs.map(([f, t]) => `(${f}::oid, ${t}::oid)`).join(", ")}) AS a(f, t) ` +
496
+ `JOIN pg_type y ON y.oid = a.t JOIN pg_proc o ON o.oid = y.typoutput LEFT JOIN pg_type el ON y.typelem <> 0 AND el.oid = y.typelem LEFT JOIN pg_proc eo ON eo.oid = el.typoutput ` +
497
+ `WHERE CASE WHEN y.typcategory = 'A' THEN coalesce(eo.provolatile, 'v') <> 'i' ELSE o.provolatile <> 'i' END`);
498
+ for (const [n] of hit)
499
+ out.push(n);
500
+ }
501
+ return out;
502
+ }
503
+ /** a definition whose function reads a session through an argument's text */
504
+ const textRefusal = (reads) => ({
505
+ key: "sql", message: `it calls ${reads.slice(0, 3).join("; ")}, a type whose text Postgres writes by the session's settings`,
506
+ why: "Postgres writes that value's text by the session's DateStyle, TimeZone or IntervalStyle, and a generated table has no session.",
507
+ options: ["give it that value as text you write, with the zone named: to_char(t AT TIME ZONE 'UTC', 'YYYY-MM-DD HH24:MI:SS')", "or call it in the statement that reads the table, over the column this one stores"],
508
+ });
509
+ /** the reason a table's column cannot be stored, with its ways out; null: it can */
510
+ function columnProblem(f, typeName, kind) {
511
+ const it = `column ${f.name}`;
512
+ if (f.name === UNNAMED)
513
+ return { key: "sql", message: "an output column has no name", why: "A table's columns are named, and Postgres has no name for an expression.", options: ["give it one: count(*) + 1 AS total"] };
514
+ if (f.name.startsWith("__mason"))
515
+ return { key: "sql", message: `${it} has a name Mason keeps for itself`, why: null, options: ["rename it with AS"] };
516
+ if (STORED_TYPES.has(f.oid) || asAnswered(f.oid, kind))
517
+ return null;
518
+ if (kind === "d")
519
+ return { key: "sql", message: `${it} is of type ${typeName}, a domain, which a generated table can't store yet`, why: null, options: [`cast it to the domain's own type, or to text: ${f.name}::text`] };
520
+ return { key: "sql", message: `${it} is of type ${typeName}, which a generated table can't store yet`, why: null, options: [`cast it to a type it can store: ${f.name}::text`] };
521
+ }
522
+ /** a column stored as the engine answers it (`GeneratedColumn.duck` null): a numeric, an enum (`typtype` e), a citext */
523
+ const asAnswered = (oid, kind) => oid === NUMERIC_OID || kind === "e" || kind === "citext";
524
+ /** the forms whose value the engine computes as a double, where Postgres computes it exactly (a variance; sqrt, exp, ln,
525
+ * log or power; avg or `/`) or from a double's digits (a numeric cast from one): never stored, whatever the program */
526
+ const ENGINE_DOUBLES = new Set(["variance", "per-value", "divides", "double"]);
527
+ /** the variance family: Postgres computes each of a numeric exactly, at decimals it picks by the value's size */
528
+ const VARIANCES = new Set(["variance", "var_samp", "var_pop", "stddev", "stddev_samp", "stddev_pop"]);
529
+ const INTEGER_TYPES = new Set(["TINYINT", "SMALLINT", "INTEGER", "BIGINT", "HUGEINT", "UTINYINT", "USMALLINT", "UINTEGER", "UBIGINT", "UHUGEINT"]);
530
+ const DECIMAL_TYPE = /^DECIMAL\((\d+),(\d+)\)$/;
531
+ /** the scale of values combined: what decides it per row, by precedence, else `numbers` of the numbers */
532
+ const scaleOf = (xs, numbers) => {
533
+ for (const k of ["text", "value", "rows", "picks"])
534
+ if (xs.some((x) => x.scale === k))
535
+ return k;
536
+ return numbers(xs.map((x) => x.scale));
537
+ };
538
+ const made = (sql, held, scale, from) => ({ sql, held, scale, peak: Math.max(held, ...from.map((x) => x.peak)), engine: from.some((x) => x.engine) });
539
+ /**
540
+ * A NUMERIC COMPUTED AT THE RESULT'S EDGE (`ExactColumn`), WRITTEN AS THE ENGINE'S DECIMAL ARITHMETIC over the plan's cells
541
+ * (`__mason_output."__mason_cell_<key>"`, src/duck/rewrite.ts). Each cell's ENGINE type is what DuckDB says the plan's SQL
542
+ * computes (`types`, never assumed): a DECIMAL is used at its own scale, an integer (a sum of int8 is a HUGEINT) is cast
543
+ * to DECIMAL(38,0) first, since DuckDB's floor, ceil and round of an integer are a double's; a text cell (sqrt, exp, ln,
544
+ * power, computed by the catalog's Postgres) is a value at its own decimals, and a double cell is a double: each judged
545
+ * at the top (`verdict`), where what is around it is known. DuckDB's DECIMAL keeps the scale Postgres's numeric does for
546
+ * `+ - *` (the larger; the sum), a negation, `abs` and `mod` (the larger), and an explicit one for `round(x, n)` and
547
+ * `trunc(x, n)` (n < 0: none), a cast to numeric(p,s), `floor` and `ceil`, each rounded half away from zero as Postgres
548
+ * rounds. A GREATEST, LEAST, COALESCE or CASE is the value it takes, each argument cast to the most decimals any holds so
549
+ * none loses a digit. Not stored here: a division (the engine divides a numeric as a double; an average is one).
550
+ * One node at a time: its children are `sub`'s (`storedProgram`, which walks each distinct node once).
551
+ */
552
+ function storedNumeric(p, types, cells, sub) {
553
+ // a numeric read from text is its text until a typmod gives it a scale: nothing else computes over it
554
+ const all = (args) => {
555
+ const out = [];
556
+ for (const a of args) {
557
+ const x = sub(a);
558
+ if ("why" in x)
559
+ return x;
560
+ if (x.scale === "text")
561
+ return { why: "text" };
562
+ out.push(x);
563
+ }
564
+ return out;
565
+ };
566
+ const one = (arg, make) => {
567
+ const x = sub(arg);
568
+ return "why" in x ? x : x.scale === "text" ? { why: "text" } : make(x);
569
+ };
570
+ // a scale the types fix again: the value cast to it
571
+ const fixed = (x, sql, scale, precision = 38) => made(`CAST(${sql} AS DECIMAL(${precision},${scale}))`, scale, scale, [x]);
572
+ // round or trunc to n decimals; n < 0 rounds left of the point, at no decimals
573
+ const toPlace = (fn, arg, n) => one(arg, (x) => fixed(x, `${fn}(${x.sql}, ${n})`, Math.max(0, n)));
574
+ switch (p.op) {
575
+ case "cell": {
576
+ const name = `__mason_cell_${p.key}`, type = types.get(name);
577
+ const sql = `__mason_output.${JSON.stringify(name)}`;
578
+ const d = type === undefined ? null : DECIMAL_TYPE.exec(type);
579
+ if (d !== null)
580
+ return { sql, held: Number(d[2]), peak: Number(d[2]), scale: Number(d[2]), engine: false };
581
+ if (type !== undefined && INTEGER_TYPES.has(type))
582
+ return { sql: `CAST(${sql} AS DECIMAL(38,0))`, held: 0, peak: 0, scale: 0, engine: false };
583
+ // a text cell: a numeric read from text (stored under a typmod, which rounds it as numeric_in does, half away from
584
+ // zero), or a function the catalog's Postgres computes; a double cell: the engine's. Neither of the last two is
585
+ // stored, and why is the column's form's
586
+ if (type === "VARCHAR" && cells[Number(p.key)] === "parsed")
587
+ return { sql, held: 0, peak: 0, scale: "text", engine: false };
588
+ if (type === "VARCHAR" || type === "DOUBLE" || type === "FLOAT")
589
+ return { sql: "NULL", held: 0, peak: 0, scale: "value", engine: true };
590
+ return { why: "other", what: `a value the engine holds as ${type ?? "nothing it names"}` };
591
+ }
592
+ case "literal": {
593
+ const m = /^-?(\d+)(?:\.(\d*))?$/.exec(p.text);
594
+ if (m === null)
595
+ return { why: "other", what: `the literal ${p.text}` };
596
+ const scale = m[2]?.length ?? 0;
597
+ return { sql: `CAST('${p.text}' AS DECIMAL(${Math.min(38, Math.max(1, m[1].length + scale))},${scale}))`, held: scale, peak: scale, scale, engine: false };
598
+ }
599
+ case "add":
600
+ case "sub":
601
+ case "mul":
602
+ case "mod": {
603
+ const xs = all(p.args);
604
+ if (!Array.isArray(xs))
605
+ return xs;
606
+ const [a, b] = xs;
607
+ if (p.op === "mul")
608
+ return made(`(${a.sql} * ${b.sql})`, a.held + b.held, scaleOf(xs, (n) => n[0] + n[1]), xs);
609
+ // a mod by zero raises, as Postgres's does: the engine's would be NULL
610
+ const sql = p.op === "mod" ? `(CASE WHEN ${b.sql} = 0 THEN error('division by zero') ELSE ${a.sql} % ${b.sql} END)` : `(${a.sql} ${p.op === "add" ? "+" : "-"} ${b.sql})`;
611
+ return made(sql, Math.max(a.held, b.held), scaleOf(xs, (n) => Math.max(...n)), xs);
612
+ }
613
+ case "neg": return one(p.arg, (x) => ({ ...x, sql: `(-${x.sql})` }));
614
+ case "abs": return one(p.arg, (x) => ({ ...x, sql: `abs(${x.sql})` }));
615
+ case "floor":
616
+ case "ceil": return one(p.arg, (x) => fixed(x, `${p.op}(${x.sql})`, 0));
617
+ case "round": return toPlace("round", p.arg, p.scale);
618
+ case "trunc": return toPlace("trunc", p.arg, p.scale);
619
+ // numeric(p, s) past 38 digits holds what DECIMAL(38, s) does, for a value the engine computed
620
+ case "typmod": {
621
+ const x = sub(p.arg);
622
+ return "why" in x ? x : fixed(x, x.sql, p.scale, Math.min(38, p.precision));
623
+ }
624
+ // past MAX_STORED_SCALE the cast could overflow: the value as held, its scale the types' (refused at the top, unless
625
+ // a round or a cast above fixes a smaller one); a scale decided per row stays so (a pad is at least, never at)
626
+ case "pad": return one(p.arg, (x) => typeof x.scale !== "number" ? x : p.scale <= MAX_STORED_SCALE ? fixed(x, x.sql, p.scale) : { ...x, scale: p.scale });
627
+ case "greatest":
628
+ case "least":
629
+ case "coalesce":
630
+ case "pick": {
631
+ // a sum of numbers read from text (src/duck/rewrite.ts `summed`): its exact sum, or the double's NaN or Infinity,
632
+ // which no stored numeric holds; the exact sum's refusal is the form's true reason
633
+ if (p.op === "pick" && p.args[0]?.op === "rescale")
634
+ return sub(p.args[0]);
635
+ const xs = all(p.args);
636
+ if (!Array.isArray(xs))
637
+ return xs;
638
+ const held = Math.max(0, ...xs.map((x) => x.held));
639
+ const args = xs.map((x) => `CAST(${x.sql} AS DECIMAL(38,${held}))`);
640
+ const scales = new Set(xs.map((x) => x.scale));
641
+ const scale = scales.size === 1 ? [...scales][0] : scaleOf(xs, () => -1) === -1 ? "picks" : scaleOf(xs, () => -1);
642
+ // (differing numbers: the value taken decides, "picks"; a scale decided per row stays so)
643
+ if (p.op !== "pick")
644
+ return made(`${p.op}(${args.join(", ")})`, held, scale, xs);
645
+ const index = sub(p.index);
646
+ if ("why" in index)
647
+ return index;
648
+ return made(`(CASE CAST(${index.sql} AS BIGINT) ${args.map((a, i) => `WHEN ${i} THEN ${a}`).join(" ")} END)`, held, scale, [...xs, index]);
649
+ }
650
+ case "div": return { why: "divides" };
651
+ // a variance's digits: a square root Postgres takes at decimals it picks by the value's size
652
+ case "stddev": return { sql: "NULL", held: 0, peak: 0, scale: "value", engine: true };
653
+ case "rescale": return one(p.arg, (x) => ({ ...x, scale: typeof x.scale === "number" ? "rows" : x.scale }));
654
+ }
655
+ }
656
+ /**
657
+ * A PROGRAM AS STORED, LINEAR IN ITS DISTINCT NODES (issue 244, #484): a program's node may be shared by several parents
658
+ * (src/lib/pg-numeric.ts; a FROM item's column read k times a level, `c + c - c` through every level), so each node is
659
+ * judged ONCE (`storedNumeric`, memoised), and a node several parents read is written ONCE: its DECIMAL expression is a
660
+ * column of its own (`layers`, `__mason_output."__mason_s<column>_<k>"`, computed below the column's SELECT, each layer
661
+ * over the one before: src/duck/generated.ts), and every parent reads that column.
662
+ * A LAYER IS COMPUTED ONLY FOR THE ROWS THE INLINE EXPRESSION COMPUTES IT FOR (PR 520 review r1, the class
663
+ * shared-node-hoisted-out-of-its-condition): what reaches a node is read from the SQL `storedNumeric` writes (`edgesOf`):
664
+ * an operand of `+ - *`, a cast, a CASE's index, GREATEST's, COALESCE's first argument is computed whenever its parent
665
+ * is; a CASE's arm only where the index takes it, a later COALESCE argument only where every one before it is NULL, a
666
+ * `mod`'s dividend only where its divisor is not zero. A node every row computes (`always`) is a plain layer; any other
667
+ * is a layer under its GUARD, `CASE WHEN <guard> THEN … END`, the guard the OR, over each way the node is reached, of
668
+ * its parent's guard AND that way's condition (a boolean column `__mason_g<column>_<k>`, or one condition written in
669
+ * place) — so the engine computes it for no row it would not have computed it for written in place (whatever the
670
+ * engine may raise on: an overflow, a division). The columns are written ONE SELECT A LEVEL (PR 520 review r2): the
671
+ * nesting is the longest chain of them, never their number, which the engine's expression depth bounds.
672
+ * A condition is written over leaves only (a cell, a literal), so every guard is computed before any value;
673
+ * a node reached under a condition over a computed value is written in its parents, as before this PR. A leaf is short,
674
+ * and written where it is read.
675
+ */
676
+ function storedProgram(p, types, cells, column) {
677
+ const leaf = (x) => x.op === "cell" || x.op === "literal";
678
+ const judged = new Map();
679
+ // a leaf's SQL, or null where it has none to compare (a value not stored): its condition cannot be written
680
+ const leafSql = (x) => {
681
+ if (!leaf(x))
682
+ return null;
683
+ const s = sub(x);
684
+ return "why" in s || s.sql === "NULL" ? null : s.sql;
685
+ };
686
+ // HOW `storedNumeric` READS EACH CHILD: the edges of its SQL, by op (a CASE of a rescale is its first arm alone, and a
687
+ // division, a variance and a rescale's scale write none)
688
+ const edgesOf = (x) => {
689
+ const always = (c) => ({ child: c, when: "always" });
690
+ switch (x.op) {
691
+ case "cell":
692
+ case "literal":
693
+ case "div":
694
+ case "stddev": return [];
695
+ case "add":
696
+ case "sub":
697
+ case "mul":
698
+ case "greatest":
699
+ case "least": return x.args.map(always);
700
+ case "neg":
701
+ case "abs":
702
+ case "floor":
703
+ case "ceil":
704
+ case "round":
705
+ case "trunc":
706
+ case "typmod":
707
+ case "pad":
708
+ case "rescale": return [always(x.arg)];
709
+ // CASE WHEN b = 0 THEN error(…) ELSE a % b END: the dividend only where the divisor is not zero
710
+ case "mod": return [always(x.args[1]), { child: x.args[0], when: (g) => { const b = leafSql(x.args[1]); return b === null ? null : `(${g} AND (${b} = 0) IS NOT TRUE)`; } }];
711
+ case "coalesce": return x.args.map((c, i) => i === 0 ? always(c) : { child: c, when: (g) => {
712
+ const before = x.args.slice(0, i).map(leafSql);
713
+ return before.some((b) => b === null) ? null : `(${g} AND ${before.map((b) => `${b} IS NULL`).join(" AND ")})`;
714
+ } });
715
+ case "pick": {
716
+ if (x.args[0]?.op === "rescale")
717
+ return [always(x.args[0])];
718
+ return [always(x.index), ...x.args.map((c, k) => ({ child: c, when: (g) => { const i = leafSql(x.index); return i === null ? null : `(${g} AND CAST(${i} AS BIGINT) = ${k})`; } }))];
719
+ }
720
+ }
721
+ };
722
+ // each distinct node's ways in, from the top (a node read twice by one parent, `c + c`, is read twice)
723
+ const into = new Map();
724
+ const walk = (x) => {
725
+ for (const edge of edgesOf(x)) {
726
+ const known = into.get(edge.child);
727
+ if (known === undefined)
728
+ into.set(edge.child, [{ parent: x, edge }]);
729
+ else
730
+ known.push({ parent: x, edge });
731
+ if (known === undefined)
732
+ walk(edge.child);
733
+ }
734
+ };
735
+ walk(p);
736
+ // the nodes computed for every row: reached from the top along "always" edges alone
737
+ const always = new Set();
738
+ const reach = (x) => {
739
+ if (always.has(x))
740
+ return;
741
+ always.add(x);
742
+ for (const e of edgesOf(x))
743
+ if (e.when === "always")
744
+ reach(e.child);
745
+ };
746
+ reach(p);
747
+ const guardLevels = [], guarded = new Map();
748
+ let guardCount = 0;
749
+ const guardOf = (x) => {
750
+ if (always.has(x))
751
+ return { sql: "TRUE", level: -1, plain: true };
752
+ if (guarded.has(x))
753
+ return guarded.get(x);
754
+ guarded.set(x, null);
755
+ const ways = [];
756
+ let level = -1, plain = true;
757
+ for (const { parent, edge } of into.get(x) ?? []) {
758
+ const g = guardOf(parent);
759
+ const way = g === null ? null : edge.when === "always" ? g.sql : edge.when(g.sql);
760
+ if (g === null || way === null)
761
+ return null;
762
+ level = Math.max(level, g.level);
763
+ plain &&= g.plain;
764
+ if (!ways.includes(way))
765
+ ways.push(way);
766
+ }
767
+ let guard;
768
+ if (ways.length === 1 && plain)
769
+ guard = { sql: ways[0], level, plain: false };
770
+ else {
771
+ const name = `__mason_g${column}_${guardCount++}`;
772
+ (guardLevels[level + 1] ??= []).push(`(${ways.join(" OR ")}) AS ${JSON.stringify(name)}`);
773
+ guard = { sql: `__mason_output.${JSON.stringify(name)}`, level: level + 1, plain: true };
774
+ }
775
+ guarded.set(x, guard);
776
+ return guard;
777
+ };
778
+ // the highest value LEVEL a node's SQL reads (-1: none): a laid node's own, another's its children's highest
779
+ const valueLevels = [], laid = new Map(), reads = new Map();
780
+ let valueCount = 0;
781
+ const readsLevel = (x) => {
782
+ const own = laid.get(x) ?? reads.get(x);
783
+ if (own !== undefined)
784
+ return own;
785
+ const level = edgesOf(x).reduce((m, e) => Math.max(m, readsLevel(e.child)), -1);
786
+ reads.set(x, level);
787
+ return level;
788
+ };
789
+ const sub = (x) => {
790
+ const known = judged.get(x);
791
+ if (known !== undefined)
792
+ return known;
793
+ let s = storedNumeric(x, types, cells, sub);
794
+ if (!("why" in s) && (into.get(x)?.length ?? 0) > 1 && !leaf(x) && s.sql !== "NULL") {
795
+ const guard = guardOf(x);
796
+ if (guard !== null) {
797
+ const name = `__mason_s${column}_${valueCount++}`, level = 1 + edgesOf(x).reduce((m, e) => Math.max(m, readsLevel(e.child)), -1);
798
+ (valueLevels[level] ??= []).push(`${guard.sql === "TRUE" ? s.sql : `CASE WHEN ${guard.sql} THEN ${s.sql} END`} AS ${JSON.stringify(name)}`);
799
+ laid.set(x, level);
800
+ s = { ...s, sql: `__mason_output.${JSON.stringify(name)}` };
801
+ }
802
+ }
803
+ judged.set(x, s);
804
+ return s;
805
+ };
806
+ const stored = sub(p);
807
+ // ONE LAYER A LEVEL (PR 520 review r2, the class guard-layers-multiply-the-nesting): a column of a level reads only the
808
+ // levels below it, so a level is one SELECT; the guards' levels first (they read leaves and guards alone), then the
809
+ // values' (each reads its guard and the values below it). The nesting is the longest chain of shared nodes, never
810
+ // their count, and a guard with one way in is no column at all
811
+ return { stored, guards: guardLevels, values: valueLevels };
812
+ }
813
+ /** a stored numeric's verdict, where all that is around it is known: null where it is stored (a picked value's `scale`:
814
+ * the most decimals any value it picks from holds, which one scale for it keeps) */
815
+ function verdict(s) {
816
+ if (s.scale === "text")
817
+ return { why: "text" };
818
+ if (s.engine || s.scale === "value")
819
+ return { why: "engine" };
820
+ if (s.scale === "rows")
821
+ return { why: "rows" };
822
+ if (s.scale === "picks")
823
+ return { why: "picks", scale: s.held };
824
+ if (s.scale > MAX_STORED_SCALE)
825
+ return { why: "scale", scale: s.scale };
826
+ if (s.peak > MAX_STORED_SCALE)
827
+ return { why: "held", scale: s.peak };
828
+ return null;
829
+ }
830
+ /** what a text read as a numeric was, in the words of its refusal */
831
+ const TEXT_WORDS = { json: "a JSON number's ->> cast to numeric", double: "the text of a double precision value, cast to numeric", text: "a text value cast to numeric" };
832
+ /**
833
+ * A numeric not stored, said by its own reason, with ways out that hold. Where the engine computes the value
834
+ * ("engine"), and for a numeric read from text, the reason and the way out are its FORM's (`formsOf`), in the user's own
835
+ * function's name; the scale's reasons are the program's walk's (`verdict`). Every way out is the user's own SQL
836
+ * (`Form.way`, `Form.scaled`, or the column's own expression, `root`, cast whole), never a placeholder, and never the
837
+ * output column named as if its source had it. Nothing here reads a reason from an engine type, the outermost node or a
838
+ * constant.
839
+ */
840
+ function notStoredRefusal(name, n, f, root) {
841
+ const atRead = "or compute it in the statement that reads the synced table";
842
+ const written = (w) => "expr" in w ? `${w.expr} AS ${name}` : w.places.map((p) => `write ${p.place} as ${p.select}`).join("; and ");
843
+ // the column's own expression cast whole, where the user wrote one (a set operation's column has none)
844
+ const whole = (type) => root === null || root === undefined ? null : { expr: exprText(castTo(root, type)) };
845
+ const doubleWay = f?.way ?? whole(DOUBLE_TYPE);
846
+ const asDouble = (or) => `${or ? "or " : ""}store it as a double if it need not be exact: ${doubleWay === null ? "cast it to double precision where it is computed" : written(doubleWay)}`;
847
+ const scaled = (words, fallback) => `${words}: ${f?.why === "text" && f.scaled !== null ? written(f.scaled) : fallback}`;
848
+ // a value the engine computes (a variance, sqrt and its kin, a double) is its form's reason, whatever the program saw
849
+ // around it (a sum of sqrt's texts is "the rows decide" to the program, and no scale given to its rows would store
850
+ // it); a division over a text read as a numeric is the division's, since one scale for the text would not store it
851
+ const strong = f !== null && (f.why === "variance" || f.why === "per-value" || f.why === "double");
852
+ const why = n.why === "engine" ? f?.why ?? "engine" : strong ? f.why : n.why === "text" ? (f?.why === "divides" ? "divides" : "text") : null;
853
+ switch (why) {
854
+ case "variance": return { key: "sql",
855
+ message: `column ${name} is a ${f.fn}, which Postgres computes as an exact numeric at decimals it picks by the size of each value, and the engine only as a double`,
856
+ why: "A stored numeric keeps one scale for every row, and a stored double holds neither Postgres's digits nor its type.",
857
+ options: [asDouble(false), atRead] };
858
+ case "per-value": return f.fixed
859
+ ? { key: "sql", message: `column ${name} rounds or casts a numeric Postgres computes for each row at decimals it picks (${f.fn}), which the engine computes as a double`, why: "Postgres's digits for it are worked out when a statement is answered, and a stored column skips that step.",
860
+ options: [asDouble(false), atRead] }
861
+ : { key: "sql", message: `column ${name} is a numeric whose decimals Postgres picks by the size of each value (${f.fn})`, why: "A stored numeric keeps one scale for every row.",
862
+ options: [asDouble(false), atRead] };
863
+ case "text": return { key: "sql", message: `column ${name} is a numeric read from text (${TEXT_WORDS[f?.why === "text" && f.from !== null ? f.from : "text"]}), which keeps the decimals each value's text has`,
864
+ why: "A stored numeric keeps one scale for every row.",
865
+ options: [scaled("give it one scale", "cast the text to numeric(20,4) where it is read"), asDouble(true)] };
866
+ case "divides": return divides(name, asDouble(false));
867
+ case "double": return { key: "sql", message: `column ${name} is a numeric cast from a double precision value (x::float8::numeric)`, why: "Postgres writes such a numeric from the double's digits when a statement is answered, a step a stored column skips.",
868
+ options: [`store the double it is: ${f?.way === null || f?.way === undefined ? "take its cast to numeric away" : written(f.way)}`, atRead] };
869
+ case "engine": return { key: "sql", message: `column ${name} is a numeric the engine computes as a double, where Postgres computes it exactly`, why: "A stored double would hold neither Postgres's digits nor its type.",
870
+ options: [asDouble(false), atRead] };
871
+ case null: break;
872
+ }
873
+ switch (n.why) {
874
+ case "divides": return divides(name, asDouble(false));
875
+ case "scale": return { key: "sql", message: `column ${name} has ${n.scale} decimals, more than the ${MAX_STORED_SCALE} a stored numeric keeps`, why: "A sum at that scale could overflow the 38 digits a stored numeric has.",
876
+ options: [`round it to fewer: round(sum(x), 2) AS ${name}`, asDouble(true)] };
877
+ case "held": return { key: "sql", message: `column ${name} is computed at ${n.scale} decimals on the way to its own, more than the ${MAX_STORED_SCALE} a stored numeric keeps (a sum of numbers read from text is added at 36)`, why: "A value at that scale could overflow the 38 digits a stored numeric has before it is rounded.",
878
+ options: [scaled("give the values fewer decimals first", "cast each value to numeric(20,4) before it is summed or rounded"), asDouble(true)] };
879
+ case "rows": return { key: "sql", message: `column ${name} sums values Postgres keeps at differing scales, so the rows decide its decimals`, why: "A stored numeric keeps one scale for every row.",
880
+ options: [scaled("give the values one scale first", "cast each value to numeric(20,4) before it is summed"), asDouble(true)] };
881
+ case "picks": {
882
+ const one = whole(typeName("numeric", [38, n.scale]));
883
+ return { key: "sql", message: `column ${name} is whichever value a GREATEST, LEAST, COALESCE or CASE takes, at that value's own decimals, and its values have differing ones`, why: "A stored numeric keeps one scale for every row.",
884
+ options: [`give it one scale: ${one === null ? `cast it to numeric(38,${n.scale}) where it is computed` : written(one)}`, asDouble(true)] };
885
+ }
886
+ case "other": return { key: "sql", message: `column ${name} computes a numeric from ${n.what}, which Mason can't store exactly yet`, why: null,
887
+ options: [`compute it in the statement that reads the table`, asDouble(true)] };
888
+ default: throw new Error(`a numeric not stored with no reason said: ${n.why}`);
889
+ }
890
+ }
891
+ const divides = (name, double) => ({ key: "sql", message: `column ${name} divides a numeric, which the engine computes as a double`,
892
+ why: "Postgres divides a numeric exactly (an average is a division), so a stored double would not hold its digits.",
893
+ options: [double, "or store what it divides, and divide in the statement that reads the table"] });
894
+ /** a numeric output column, as the catalog spells its type */
895
+ export const isNumeric = (c) => /^numeric\b/.test(c.type);
896
+ /**
897
+ * EVERY NUMERIC OF A DEFINITION, AS STORED, held to the engine type DuckDB says the plan's SQL computes (`types`), or refused
898
+ * by its own reason. A computed one (`DefinitionPlan.numerics`) is written as DECIMAL arithmetic over its cells; one the
899
+ * engine answers itself (no program, "as the engine answers it", a passthrough's among them) is stored as the engine's
900
+ * DECIMAL, an integer as HUGEINT (one type for a numeric of no decimals: a sum of int8, a sign), and a double refused.
901
+ * Before either, the ONE authority: a column whose analysed form the engine computes as a double (`ENGINE_DOUBLES`) is
902
+ * refused by that form, whether the rewrite made a program for it or not.
903
+ * At the check (`planDefinition` with a `describe`) and again at the write, over the SQL that runs. `layers`: the
904
+ * columns a computed numeric's shared nodes are written once in (`storedProgram`), each `<expression> AS "<name>"`
905
+ * over `__mason_output` as the layer before it left it, in order: the write computes each below the table's SELECT.
906
+ */
907
+ export function storedColumns(d, types) {
908
+ const refused = [], exprs = new Map(), guards = [], values = [];
909
+ // a level of each column joins that level of every other: no column reads another's (each names its own)
910
+ const merge = (all, own) => own.forEach((level, i) => { (all[i] ??= []).push(...level); });
911
+ // what the analysis says the engine computes as a double is refused by its form, whether or not the rewrite made a
912
+ // program for it: no exact program carries one, and the rewrite's silence (src/duck/rewrite.ts `programNeeded`, the
913
+ // wire's rule) stored `stddev(x)::numeric(p,s)` from the engine's double (PR 202 round 11)
914
+ const engineDouble = (i) => { const f = d.forms[i]; return f !== null && f !== undefined && ENGINE_DOUBLES.has(f.why); };
915
+ for (const { column, program, cells } of d.numerics) {
916
+ const name = d.columns[column].name, { stored, guards: ownGuards, values: ownValues } = storedProgram(program, types, cells, column);
917
+ const no = engineDouble(column) ? { why: "engine" } : "why" in stored ? stored : verdict(stored);
918
+ if (no !== null)
919
+ refused.push(notStoredRefusal(name, no, d.forms[column] ?? null, d.roots[column]));
920
+ else {
921
+ exprs.set(column, stored.sql);
922
+ merge(guards, ownGuards);
923
+ merge(values, ownValues);
924
+ }
925
+ }
926
+ d.columns.forEach((c, i) => {
927
+ if (!isNumeric(c) || d.numerics.some((n) => n.column === i))
928
+ return;
929
+ if (engineDouble(i)) {
930
+ refused.push(notStoredRefusal(c.name, { why: "engine" }, d.forms[i] ?? null, d.roots[i]));
931
+ return;
932
+ }
933
+ const type = types.get(`#${i}`);
934
+ if (type !== undefined && (DECIMAL_TYPE.test(type) || type === "HUGEINT"))
935
+ return;
936
+ if (type !== undefined && INTEGER_TYPES.has(type))
937
+ exprs.set(i, `CAST(__mason_output.__mason_c${i} AS HUGEINT)`);
938
+ else
939
+ refused.push(notStoredRefusal(c.name, type === "DOUBLE" || type === "FLOAT" ? { why: "engine" } : { why: "other", what: `a value the engine holds as ${type ?? "nothing it names"}` }, d.forms[i] ?? null, d.roots[i]));
940
+ });
941
+ if (refused.length > 0)
942
+ return { refused };
943
+ const layers = [...guards, ...values].filter((level) => level !== undefined && level.length > 0).map((level) => level.join(", "));
944
+ return { columns: d.columns.map((c, i) => { const expr = exprs.get(i); return expr === undefined ? c : { ...c, duck: null, expr }; }), layers };
945
+ }
946
+ /** the tables a definition's SQL names, by name alone (`schema.table`, a bare name `public`'s): what a cycle is told
947
+ * from before anything is planned. A bare name some WITH of the statement holds is a CTE's and left out. Nothing,
948
+ * where the SQL does not parse: that is refused at its check. */
949
+ export async function readsByName(sql) {
950
+ const select = await selectOf(sql);
951
+ if ("refused" in select)
952
+ return [];
953
+ const ctes = new Set(), qualified = new Set(), bare = new Set();
954
+ const walk = (v) => {
955
+ if (Array.isArray(v)) {
956
+ v.forEach(walk);
957
+ return;
958
+ }
959
+ if (typeof v !== "object" || v === null)
960
+ return;
961
+ const o = v;
962
+ const cte = o.CommonTableExpr;
963
+ if (cte?.ctename !== undefined)
964
+ ctes.add(cte.ctename);
965
+ const r = o.RangeVar;
966
+ if (r !== undefined)
967
+ (r.schemaname === undefined ? bare : qualified).add(r.schemaname === undefined ? r.relname : `${r.schemaname}.${r.relname}`);
968
+ Object.values(o).forEach(walk);
969
+ };
970
+ walk(select.stmt);
971
+ return [...new Set([...qualified, ...[...bare].filter((n) => !ctes.has(n)).map((n) => `public.${n}`)])].sort();
972
+ }
973
+ /** a definition in a cycle of generated tables (`cycle`: the tables, from this one round to it again) */
974
+ export function cycleRefusal(cycle) {
975
+ return { key: "sql", message: cycle.length <= 2 ? `it reads ${cycle[0]}, itself` : `it reads itself through other generated tables: ${cycle.join(" -> ")}`,
976
+ why: "A generated table is built from the tables built before it, so none can read itself, through others or not.",
977
+ options: ["read the tables the cycle is generated from instead", "or write the other table's SQL into this one, as a subquery"] };
978
+ }
979
+ /** a definition whose check failed on what Mason did not expect (src/duck/generated.ts `check`): a verdict all the same,
980
+ * in a user's words, never the error's, which names the engine (its text goes to the log, for the report) */
981
+ export function uncheckedRefusal() {
982
+ return { key: "sql", message: "Mason could not check this definition (a Mason bug: report it)",
983
+ why: "Checking it stopped on something Mason did not expect, so the table is not built; Mason's log says what, for the report.",
984
+ options: ["report it, with the file's SQL and the log's line for this file", "or write the SQL another way: a CTE or a subquery as a join, or each UNION ALL arm as its own generated table"] };
985
+ }
986
+ /**
987
+ * A DEFINITION HELD TO WHAT A TABLE CAN STORE, its reads named. `relations`: for a run of a chain, each parent built in
988
+ * the same run (`schema.table`) and the side table read in its place (src/duck/plan.ts swapRelations). Throws what `Planner.plan` throws: a statement Postgres or the rewrite refuses is the
989
+ * caller's to say at its key, and a table not there yet its to wait for.
990
+ */
991
+ export async function planDefinition(p, sql, orderBy, relations, describe) {
992
+ const statement = await definitionStatement(sql, orderBy);
993
+ if ("refused" in statement)
994
+ return { refused: [statement.refused] };
995
+ let plan;
996
+ try {
997
+ plan = await p.plan(statement.text, statement.stmt, relations);
998
+ }
999
+ catch (e) {
1000
+ // a statement refused for what Mason has not verified (`current_time`, a timetz) that also reads a session is refused
1001
+ // for the session, with the line to write: verifying it would not make it a table's to hold
1002
+ if (!(e instanceof StatementError) || e.gap !== "unverified")
1003
+ throw e;
1004
+ const keys = (/: ([^]*)$/.exec(e.fields.detail ?? "")?.[1] ?? "").split(", ").filter((k) => k !== "");
1005
+ const session = await sessionReads(p.rows, keys);
1006
+ if (session.length === 0)
1007
+ throw e;
1008
+ return { refused: [sessionRefusal(session)] };
1009
+ }
1010
+ const bases = [...new Set((plan.reads ?? []).map((r) => r.table))].sort();
1011
+ const refused = [];
1012
+ const session = await sessionReads(p.rows, plan.resolved);
1013
+ if (session.length > 0)
1014
+ refused.push(sessionRefusal(session));
1015
+ const text = await textReads(p.rows, plan.resolved, plan.argumentTypes ?? []);
1016
+ if (text.length > 0)
1017
+ refused.push(textRefusal(text));
1018
+ if (plan.clock?.parse !== undefined)
1019
+ refused.push({ key: "sql", message: "it reads a relative literal ('now', 'today')", why: THE_SESSION_WHY, options: ["use now(), as of the run, with the zone named if it reads a day: (now() AT TIME ZONE 'UTC')::date"] });
1020
+ if (plan.withTies)
1021
+ refused.push({ key: "sql", message: "it ends in FETCH … WITH TIES", why: "That row count isn't a table's to keep.", options: ["use LIMIT, with an ORDER BY that decides every row"] });
1022
+ const fields = plan.fields ?? [];
1023
+ const twice = fields.find((f, i) => f.name !== UNNAMED && fields.findIndex((x) => x.name === f.name) !== i);
1024
+ if (twice !== undefined)
1025
+ refused.push({ key: "sql", message: `it names column ${twice.name} twice`, why: "A table's columns have one name each.", options: ["rename one with AS"] });
1026
+ const typeNames = new Map((await p.rows(fields.length === 0 ? "SELECT 0, ''" : `SELECT v.i::text, format_type(v.oid, v.typmod) FROM (VALUES ${fields.map((f, i) => `(${i}, ${Math.trunc(f.oid)}::oid, ${Math.trunc(f.typmod)})`).join(", ")}) AS v(i, oid, typmod)`))
1027
+ .map(([i, name]) => [Number(i), name]));
1028
+ // a type a user or an extension made: an enum (`e`) and a citext are stored, a domain and a composite are not. Judged
1029
+ // by the column's INDEX and the catalog's type of the name the wire gives its OID: a field carries the SOURCE's OID
1030
+ // for a made type (src/duck/plan.ts), which the catalog's pg_type does not have
1031
+ const made = fields.map((f, i) => [f, i]).filter(([f]) => !STORED_TYPES.has(f.oid) && f.oid !== NUMERIC_OID);
1032
+ const kinds = new Map(made.length === 0 ? [] : (await p.rows(`SELECT v.i::text, CASE WHEN t.typname = 'citext' AND t.typtype = 'b' THEN 'citext' ELSE t.typtype::text END ` +
1033
+ `FROM (VALUES ${made.map(([f, i]) => `(${i}, ${Math.trunc(f.oid)}::oid)`).join(", ")}) AS v(i, oid) JOIN pg_type t ON t.oid = coalesce(to_regtype(public.mason_wire_type(v.oid, -1)), v.oid)`))
1034
+ .map(([i, kind]) => [Number(i), kind]));
1035
+ fields.forEach((f, i) => { const bad = columnProblem(f, typeNames.get(i) ?? String(f.oid), kinds.get(i)); if (bad !== null)
1036
+ refused.push(bad); });
1037
+ // a numeric computed after the engine's answer: stored as DECIMAL arithmetic where the types fix its scale, over the
1038
+ // engine types DuckDB says the plan computes (`storedColumns`)
1039
+ const numerics = [];
1040
+ for (const e of refused.length === 0 ? plan.exact ?? [] : []) {
1041
+ const f = fields[e.column];
1042
+ if (e.text) {
1043
+ refused.push({ key: "sql", message: `column ${f.name} is the text of a numeric computed when a statement is answered, which a generated table can't store yet`, why: "Postgres's digits for it are worked out when a statement is answered, and a stored column skips that step.", options: [`store the numeric, and write its text in the statement that reads the table`] });
1044
+ continue;
1045
+ }
1046
+ numerics.push({ column: e.column, program: e.program, cells: e.cells });
1047
+ }
1048
+ if (refused.length > 0)
1049
+ return { refused };
1050
+ const definition = { plan, bases, warnings: statement.warnings, numerics, forms: await formsOf(p.rows, plan.outputs, statement.stmt, fields.length),
1051
+ roots: ((raw) => fields.map((_, i) => raw !== null && raw.length === fields.length ? raw[i] : null))(rawTargets(statement.stmt)),
1052
+ columns: fields.map((f, i) => ({ name: f.name, type: typeNames.get(i), duck: numerics.some((n) => n.column === i) ? null : STORED_TYPES.get(f.oid) ?? null })) };
1053
+ if (describe === undefined || !definition.columns.some(isNumeric))
1054
+ return { definition };
1055
+ const stored = storedColumns(definition, await describe(plan.sql.text));
1056
+ return "refused" in stored ? stored : { definition: { ...definition, columns: stored.columns } };
1057
+ }
1058
+ /** A refusal said to a person: the file, the key, then the reason and the ways out a line each (issue 124's shape). */
1059
+ export function refusalText(file, r) {
1060
+ return [`${file}: ${r.key === null ? "" : `${r.key}: `}${r.message}.`, ...(r.why === null ? [] : [` ${r.why}`]),
1061
+ ...(r.options.length === 0 ? [] : [" Your options:", ...r.options.map((o) => ` - ${o}`)])].join("\n");
1062
+ }
1063
+ /** A statement's refusal as a table's: Postgres's or a gap's message, at `sql:`, with its hint as the way out. */
1064
+ export function refusalOf(e) {
1065
+ const options = e.fields.hint === undefined ? [] : [e.fields.hint];
1066
+ // a column the database has and the copy doesn't is the likely reason a generated table meets this
1067
+ if (e.fields.code === "42703")
1068
+ options.push("if the table has that column, sync it: add it to the table's columns:, or take it out of exclude:, in the sources file");
1069
+ return { key: "sql", message: e.fields.message, why: e.fields.detail ?? null, options };
1070
+ }