@mason-data/cli 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/README.md +13 -4
  2. package/dist/src/api/postgres/catalog-worker.js +49 -2
  3. package/dist/src/api/postgres/catalog.js +166 -32
  4. package/dist/src/api/postgres/listener.js +11 -3
  5. package/dist/src/api/postgres/proxy.js +283 -0
  6. package/dist/src/api/postgres/schema.js +6 -0
  7. package/dist/src/api/postgres/server.js +505 -99
  8. package/dist/src/api/postgres/translate.js +47 -2
  9. package/dist/src/cli/background.js +18 -19
  10. package/dist/src/cli/bridge-stdin.js +101 -0
  11. package/dist/src/cli/command.js +169 -11
  12. package/dist/src/cli/commands.js +207 -51
  13. package/dist/src/cli/credentials.js +182 -0
  14. package/dist/src/cli/demo.js +2 -2
  15. package/dist/src/cli/deploy.js +574 -0
  16. package/dist/src/cli/dev.js +148 -27
  17. package/dist/src/cli/doctor.js +116 -33
  18. package/dist/src/cli/generated-files.js +143 -0
  19. package/dist/src/cli/guides.js +97 -6
  20. package/dist/src/cli/hosted.js +850 -0
  21. package/dist/src/cli/init.js +47 -11
  22. package/dist/src/cli/login.js +289 -0
  23. package/dist/src/cli/machine.js +1 -1
  24. package/dist/src/cli/main.js +8 -2
  25. package/dist/src/cli/mason.cjs +17 -0
  26. package/dist/src/cli/mcp.js +2 -2
  27. package/dist/src/cli/output-gate.js +176 -0
  28. package/dist/src/cli/page-share.js +96 -0
  29. package/dist/src/cli/pg-tls.js +1 -1
  30. package/dist/src/cli/pin.js +6 -2
  31. package/dist/src/cli/pool-size.cjs +85 -0
  32. package/dist/src/cli/ports.js +2 -0
  33. package/dist/src/cli/preflight.js +130 -39
  34. package/dist/src/cli/project.js +99 -0
  35. package/dist/src/cli/readiness.js +42 -6
  36. package/dist/src/cli/registry.js +12 -4
  37. package/dist/src/cli/replica-hosted.js +665 -0
  38. package/dist/src/cli/replica.js +76 -0
  39. package/dist/src/cli/roles.js +2 -1
  40. package/dist/src/cli/run-server.js +154 -7
  41. package/dist/src/cli/security.js +57 -10
  42. package/dist/src/cli/serve.js +63 -7
  43. package/dist/src/cli/share-run.js +51 -8
  44. package/dist/src/cli/share.js +3 -3
  45. package/dist/src/cli/source-edit.js +10 -4
  46. package/dist/src/cli/source-role.js +344 -0
  47. package/dist/src/cli/sources.js +30 -20
  48. package/dist/src/cli/style.js +2 -1
  49. package/dist/src/cli/supervise.js +15 -23
  50. package/dist/src/cli/token-hosted.js +337 -0
  51. package/dist/src/cli/ui-ssh.js +460 -0
  52. package/dist/src/cli/ui.js +115 -11
  53. package/dist/src/cli/user-hosted.js +299 -0
  54. package/dist/src/cli/user.js +126 -74
  55. package/dist/src/demo/seed.js +26 -0
  56. package/dist/src/duck/analyze.js +36 -20
  57. package/dist/src/duck/collation.js +266 -0
  58. package/dist/src/duck/constant-key.js +94 -0
  59. package/dist/src/duck/distinct-key.js +283 -26
  60. package/dist/src/duck/events.js +39 -8
  61. package/dist/src/duck/executor-worker.js +2 -2
  62. package/dist/src/duck/executor.js +6 -5
  63. package/dist/src/duck/explain.js +4 -0
  64. package/dist/src/duck/functions.js +448 -49
  65. package/dist/src/duck/gaps.js +34 -16
  66. package/dist/src/duck/generated-plan.js +1070 -0
  67. package/dist/src/duck/generated.js +906 -0
  68. package/dist/src/duck/grants.js +1 -1
  69. package/dist/src/duck/held-types.js +64 -0
  70. package/dist/src/duck/history.js +4 -0
  71. package/dist/src/duck/jsonb-order.js +9 -12
  72. package/dist/src/duck/kept-route.js +6 -5
  73. package/dist/src/duck/kept.js +9 -7
  74. package/dist/src/duck/late.js +1469 -40
  75. package/dist/src/duck/libc-bound.js +341 -0
  76. package/dist/src/duck/libc-order.js +892 -126
  77. package/dist/src/duck/pg-eval-worker.js +7 -1
  78. package/dist/src/duck/pg-eval.js +6 -4
  79. package/dist/src/duck/pgtree.js +774 -23
  80. package/dist/src/duck/plan.js +275 -30
  81. package/dist/src/duck/publication.js +14 -0
  82. package/dist/src/duck/requests.js +9 -5
  83. package/dist/src/duck/resolved.js +117 -2
  84. package/dist/src/duck/rewrite.js +4567 -583
  85. package/dist/src/duck/row-filter.js +12 -6
  86. package/dist/src/duck/statement.js +19 -0
  87. package/dist/src/duck/store.js +294 -68
  88. package/dist/src/duck/stream.js +9 -5
  89. package/dist/src/duck/sync.js +602 -126
  90. package/dist/src/duck/token.js +4 -1
  91. package/dist/src/duck/types.js +63 -7
  92. package/dist/src/duck/usage.js +6 -4
  93. package/dist/src/duck/users.js +5 -0
  94. package/dist/src/duck/verified.js +159 -2
  95. package/dist/src/duck/views.js +4 -3
  96. package/dist/src/lib/database-url.js +55 -10
  97. package/dist/src/lib/instant.js +50 -0
  98. package/dist/src/lib/log-buffer.js +13 -4
  99. package/dist/src/lib/log-read.js +196 -0
  100. package/dist/src/lib/pg-numeric.js +387 -49
  101. package/dist/src/lib/secret-mask.js +142 -0
  102. package/dist/src/lib/table-sync.js +24 -9
  103. package/dist/src/web/app.js +9 -4
  104. package/dist/src/web/pages/status.js +178 -13
  105. package/dist/src/web/pages/syncs.js +1 -1
  106. package/dist/src/web/pages/tables.js +8 -3
  107. package/dist/src/web/pages/users.js +4 -3
  108. package/dist/src/web/shared.js +19 -3
  109. package/package.json +8 -7
@@ -1,3 +1,167 @@
1
+ /**
2
+ * THE SELECT LIST AFTER THE LIMIT (the Metabase test of #517: a table's preview, `… ORDER BY id LIMIT 2000` with a JSON
3
+ * column's fields and a join, took 188 ms). DuckDB computes a statement's select list BELOW its Top N: every row's JSON
4
+ * is read to keep 2,000 of them. Its own late materialization covers one table's plain columns only, and it takes
5
+ * Mason's functions for cheap. So a statement that sorts and limits a computed select list is written as Postgres
6
+ * plans it through an index — the rows chosen first, the list computed over them:
7
+ *
8
+ * SELECT <list over __mason_late's columns> FROM (
9
+ * SELECT <each column the list reads> AS __mason_c<i>, <each sort key> AS __mason_o<i>
10
+ * FROM … WHERE … ORDER BY <the sort keys> LIMIT … OFFSET …) AS __mason_late
11
+ * ORDER BY __mason_late.__mason_o<i> …
12
+ *
13
+ * The rows are the same rows (the inner query keeps the statement's FROM, WHERE, ORDER BY, LIMIT and OFFSET, each
14
+ * column read by its own reference in its own scope), each value is the same expression over the same columns, and the
15
+ * outer ORDER BY is the inner one's keys again. What changes is WHICH ROWS the list is computed for: a value that
16
+ * raises (`'x'::int`) in a row the LIMIT leaves out raises in Postgres only when its plan sorts every row (no index
17
+ * gives the order), and never here — the decided difference `limit-errors` (docs/SQL-SUPPORT.md).
18
+ *
19
+ * Every SELECT of the statement so shaped is written so, a derived table's and a WITH query's too (a tool wraps one
20
+ * in another), when Postgres's analysis has no aggregate, window function, set-returning select list or subquery
21
+ * expression anywhere in the statement, and the SELECT itself has no DISTINCT, GROUP BY, HAVING, WINDOW, WITH, set
22
+ * operation, row lock, WITH TIES or `*` in its list.
23
+ *
24
+ * THE ROWS OF A JOIN, CHOSEN BY ROW NUMBER (issue 105: a table preview over three joined tables spilled every row's wide
25
+ * columns to choose 100, and filled the disk). DuckDB's own late materialization stops at a join, and the form above
26
+ * still carries each column the list reads through the join and the top N. So where every FROM item is a copied table
27
+ * and the join names no columns (`joinRows`), the rows are chosen on their row numbers alone, and the columns read for
28
+ * the rows chosen:
29
+ *
30
+ * SELECT <the list as written> FROM (
31
+ * SELECT a.rowid AS __mason_r1, b.rowid AS __mason_r2, <each sort key> AS __mason_o<i>
32
+ * FROM … WHERE … ORDER BY <the sort keys> LIMIT … OFFSET …) AS __mason_late
33
+ * LEFT JOIN a ON a.rowid = __mason_late.__mason_r1 LEFT JOIN b ON b.rowid = __mason_late.__mason_r2
34
+ * ORDER BY __mason_late.__mason_o<i> …
35
+ *
36
+ * A row of the join is one row number per table, NULL where an outer join supplied none, and that NULL finds no row, so
37
+ * its columns are NULL as they were: a row the join multiplies is a pair of its own, and a statement of one snapshot
38
+ * reads each row number once. The rows and the order among different keys are the inner query's; rows of EQUAL keys
39
+ * are in no defined order, here as on Postgres.
40
+ *
41
+ * A LIMIT WITH NO ORDER BY is written the same way (a statement with no limit in fact, OFFSET alone or LIMIT ALL, stays
42
+ * as it was: every row comes back, and reading each again saves nothing), for the join's memory, where the last table is
43
+ * on the hash join's build side. Its keys are the row numbers of the tables read ONLY WHERE THE JOIN'S ROWS ARE PROVED
44
+ * BOUNDED (`fanOutBounded`, src/duck/distinct-key.ts: every join equates a whole primary key of a table its side holds
45
+ * once, from the analysis and the catalog's pg_index). That sort is for pages: which rows such a statement answers is
46
+ * defined by no one, but DuckDB's parallel join hands the same statement's rows back in another order each run, and a
47
+ * LIMIT over them overlaps its neighbouring pages (about a third of the rows of a join paged to its end came back twice;
48
+ * one thread, none; a Postgres of one plan, in practice none). The top N on row numbers is a full join where a streaming
49
+ * LIMIT stops early, so its cost grows with the join: over 50 M rows, a narrow `dim LEFT JOIN big LIMIT 100` takes 134 ms
50
+ * against 1 ms, 1332 ms against 10 ms at OFFSET 2,000,000, and that is accepted for stable pages. Its order is total
51
+ * over what is read: two rows of equal row numbers are the same output.
52
+ *
53
+ * WHAT IS GIVEN UP WHERE THE PROOF FAILS: the sort. A join the proof cannot bound may multiply (a self-join on a column
54
+ * of few values, 100 billion joined rows: 74 s sorted, milliseconds not; docs/DESIGN.md "A top N over joined tables"), so
55
+ * its rows are chosen on row numbers with no sort at all, inside or out: wide columns still stay out of the join, and
56
+ * the statement still stops early. ITS PAGES CAN OVERLAP AGAIN, as they could before PR 118: a many-to-many join, a cross
57
+ * join, a key in part, a table with no primary key and a join on an expression all pay this, and a tool that pages one
58
+ * can see rows twice and others never. The rows of each answer are still rows of the join, each as often as the join
59
+ * holds it: the proof decides the cost and the pages, never an answer, so the safe side of a proof that fails is no sort.
60
+ *
61
+ * `afterLimit` still needs a sort: the keys put its rows back, and a list of no key has none.
62
+ *
63
+ * Measured over a 4 M-row join with wide JSON in the last table (a 2 GB
64
+ * limit): 12.9 s and 8.6 GiB spilled before, 0.19 s and none after; LIMIT 10,000 failed for memory before and takes 2.9 s.
65
+ *
66
+ * A row number is read only of a table, so a table with a column named `rowid` in any case, which DuckDB reads instead,
67
+ * stays as it was. A `*` is written out from Postgres's own list of the table's columns, so the tables can be read
68
+ * themselves (a copy's view of its visible columns, src/duck/kept-route.ts, has no row numbers); a whole row is not
69
+ * listed, and stays as it was.
70
+ *
71
+ * EVERY FROM ITEM OF SUCH A JOIN IS ONE OF THREE (issue 134, step 2: a view, a WITH query, a tool's wrapper and a derived
72
+ * side each carried every wide column through the join and the top N: 7.3 GiB spilled to keep 100 rows, or a failure):
73
+ *
74
+ * - A TABLE, fetched by its row number as above.
75
+ * - A PLAIN DERIVED READ: a derived table, a view's definition or a WITH query read once and not MATERIALIZED, with
76
+ * only a projection and a WHERE over one table (`Derived.read`: Postgres's analysis says so, and a tree of that shape
77
+ * must agree), nothing volatile in it and nothing from outside. It exposes its row number, last so an alias's column
78
+ * names still fall on its own list, and is fetched by it, in its derived form and without its WHERE (the row
79
+ * passed it inside):
80
+ *
81
+ * SELECT <the list as written> FROM (
82
+ * SELECT a.rowid AS __mason_r1, d.__mason_rid AS __mason_r2, <carried columns>, <sort keys> AS __mason_o<i>
83
+ * FROM a JOIN (SELECT <the derived list>, t.rowid AS __mason_rid FROM t WHERE …) AS d ON … WHERE … ORDER BY … LIMIT …
84
+ * ) AS __mason_late
85
+ * LEFT JOIN a ON a.rowid = __mason_late.__mason_r1
86
+ * LEFT JOIN (SELECT <the derived list>, t.rowid AS __mason_rid FROM t) AS d ON d.__mason_rid = __mason_late.__mason_r2
87
+ *
88
+ * THE DERIVED FORM IS WHAT KEEPS A NULLABLE SIDE RIGHT: `LEFT JOIN (SELECT id, 1 AS one …)` must read NULL where nothing
89
+ * matched, and a NULL row number finds no row, so it does; flattened, it would read 1. DuckDB prunes what the join does
90
+ * not read from the inner derived list, so it reads the join's columns alone.
91
+ * - ANYTHING ELSE that is a derived read, CARRIED: an aggregate, a window, DISTINCT, a LIMIT, a set operation, a volatile
92
+ * function, a LATERAL, a WITH query read twice or MATERIALIZED, a join. No row number exists of it, so each column the
93
+ * list reads comes through the inner query as `__mason_c<i>`, unchanged, as it did before. A function or a VALUES list
94
+ * in the FROM is none of the three, and the statement stays as it was.
95
+ *
96
+ * A QUERY WHOSE ONE FROM ITEM IS A PROJECTION AND A WHERE OVER FROM ITEMS IS FOLDED INTO IT FIRST (`fold`: a view of a
97
+ * join, `SELECT * FROM (<join>) t LIMIT 100`, a WITH query over a join, a view of a view), the way Postgres pulls a
98
+ * subquery up: each column of the wrapper read as the expression it is, the WHEREs and'd. Nothing there is volatile, so
99
+ * an expression read twice is one value; a wrapper's own output name or number is read before the folding, so a sort
100
+ * means what it meant. A statement whose form this cannot write stays as it was, never half folded.
101
+ *
102
+ * MERGED COLUMNS AND COMPUTED KEYS (issue 134, step 3: `USING`, `NATURAL` and a sort by a computed output each carried every
103
+ * wide column through the join and the top N), both read from Postgres's analysis:
104
+ *
105
+ * - THE JOIN STAYS AS WRITTEN INSIDE, and a bare merged column in the keys is the join's. The fetch has no join to merge
106
+ * it, so a merged column of the list is read through Postgres's join alias variables (`mergedRead`): left for INNER and
107
+ * LEFT, right for RIGHT, `coalesce` for FULL (an inner join reads the side of the join's type where a side needs a
108
+ * cast), a merged column of a join below read through that join, with the cast the join adds. A cast to any other type
109
+ * (a timestamp's, in a session's zone) cannot be written here: that column is carried, the join's own reading of it.
110
+ * A NATURAL arrives as the USING of the columns Postgres merged, or `ON true` for none (src/duck/rewrite.ts
111
+ * `naturalWritten`, for every statement); one it could not write stays as it was. A join with a name of its own stays
112
+ * as it was.
113
+ * - A COMPUTED OUTPUT THAT IS A SORT KEY, by its name, its number or its expression repeated, is one key, and the list
114
+ * reads it from `__mason_o<i>` (`sortKeys`' `shown`): one value, never computed again, so `SELECT upper(x) AS u … ORDER
115
+ * BY u` and `ORDER BY 1` and `ORDER BY upper(x)` are one statement. A key runs on every row, a raising one raises as
116
+ * Postgres's sort does. Beside a `*` a number is read from the list written out.
117
+ * - A VOLATILE VALUE IS NEVER COMPUTED TWICE: Postgres's analysis says which sort targets hold a volatile function
118
+ * (`computedSorts`, pg_proc's provolatile). The key is sorted by its output's name, one value inside, and shown from
119
+ * it. One a written form cannot show as that value (a collation's keys repeat it) stays as it was. A volatile output
120
+ * no sort reads is computed once, after the limit.
121
+ *
122
+ * A SORT BY A COMPUTED OUTPUT OF ONE TABLE STAYS AS IT WAS: it computes that output for every row anyway (`afterLimit`).
123
+ *
124
+ * WHAT IS GIVEN UP: a raising expression of a derived list now runs on the chosen rows alone (`limit-errors`).
125
+ *
126
+ * A ROLE'S FILTERED READ IS A FOURTH KIND, WALLED (issue 134, step 4; src/duck/row-filter.ts): `(SELECT * FROM (SELECT *
127
+ * FROM t WHERE <the role's filter> LIMIT <the barrier>) AS <the wall>) AS t`, the outer `*` the statistics wall once the
128
+ * plan is done. It is no plain read (its LIMIT is the barrier), so it is never folded, flattened or carried: its row
129
+ * numbers are chosen INSIDE it, the barrier's own select exposing `t.rowid AS __mason_rid` beside its `*`, so the wall's
130
+ * `try` passes it on like any column and the statement's WHERE, join conditions and keys run where they ran before, above
131
+ * the barrier, on the rows the filter keeps. The rows are FETCHED THROUGH THE SAME WALLED READ, filter, barrier and wall,
132
+ * never the bare table: a list expression then meets only the rows the filter keeps and none of the table's statistics,
133
+ * and runs on the chosen rows alone. A plain derived read of a walled read (a view, a derived table, a WITH query over a
134
+ * filtered table) is fetched the same way, through it. Each walled read fetched is one more wall in the statement: the
135
+ * plan's count of them (`wallStatistics`) is told so by `LateContext.wall`. A walled read whose filter holds a volatile
136
+ * function stays as it was: read twice, it could keep other rows.
137
+ *
138
+ * A SUBQUERY IN THE SELECT LIST RUNS FOR THE CHOSEN ROWS (issue 134, step 5: a correlated subquery in a top N's list was
139
+ * computed for every row of the join below the sort, 57 s and 26 GiB spilled against 0.10 s). Where every subquery of
140
+ * a query stands in its select list, shown and no sort or group key (`listSubLinkReads`, from Postgres's analysis), the
141
+ * rows are chosen on keys alone as above, and the list, its subqueries as written, is computed in the fetch: each FROM
142
+ * item a subquery reads (by the analysis's columns of the query, as many levels up as the subquery sits) is fetched
143
+ * under its own alias, so the subquery reads the row's own columns, and NULL where an outer join supplied no row. One
144
+ * table alone is written so too. What stays as it was: a subquery anywhere else (a WHERE, a join's condition, a sort
145
+ * key, a FROM item's function), one that reads a carried item, a join's merged column (a FULL join's, or one of a cast),
146
+ * a whole row or a query outside its own, a wrapper folded away (its columns are names inside the subquery a fold does
147
+ * not know), and one a rewrite wrote that the analysis has not. A subquery's own SELECT is never rewritten. A subquery
148
+ * that raises on a row the limit leaves out answers here (`limit-errors`).
149
+ *
150
+ * A DISTINCT ON WITH A LIMIT IS WRITTEN SO TOO (issue 134, task E: a DISTINCT ON over a join with wide columns in its list
151
+ * carried every row's through DuckDB's DISTINCT ON and its top N, 25 GiB spilled and a failure after 20 minutes, the
152
+ * same rows written by hand 0.21 s). A DISTINCT ON keeps ONE WHOLE ROW of its FROM for each set of its ON expressions, the first in the sort's order,
153
+ * so that row's numbers identify it exactly, whatever the ON expressions are: a key or not, computed, a jsonb's equality
154
+ * key. The inner query keeps the DISTINCT ON beside the sort and the limit (its ordinals and output names read as the
155
+ * outputs' expressions, `distinctKeys`, a clone each), and the list is read from the rows kept, one table included:
156
+ * DuckDB's late materialization does not reach through the window and filter its DISTINCT ON is. Never `any_value` and
157
+ * never a primary key (`keyFirst` is a GROUP BY's, whose group may hold many rows of a table). What stays as it was: no
158
+ * LIMIT in fact, a DISTINCT ON over a GROUP BY, an aggregate, a window or a set-returning function, and an ON expression
159
+ * holding a volatile function (`Query.volatileOn`). A raising list value in a row that is not its group's first answers
160
+ * here (`limit-errors`).
161
+ */
162
+ import { FIRST_USER_OID } from "./pgtree.js";
163
+ import { distinctGrouping, fanOutBounded, keyGrouped, projectionOnly } from "./distinct-key.js";
164
+ import { constantExpression, evaluated } from "./constant-key.js";
1
165
  const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
2
166
  const kindOf = (n) => isObject(n) ? Object.keys(n)[0] ?? null : null;
3
167
  const bodyOf = (n) => n[kindOf(n)];
@@ -39,8 +203,121 @@ function rowWiseStatement(tree) {
39
203
  visit(tree);
40
204
  return ok && queries > 0;
41
205
  }
42
- /** every node of a select-list expression is row-wise, and it computes something (a plain column needs no deferring) */
43
- function rowWise(n) {
206
+ /** A QUERY'S SUBQUERIES IN ITS SELECT LIST ALONE (issue 134, step 5): the FROM items they read of `q`, by alias, from
207
+ * Postgres's analysis; [] for a query with none; null where a subquery stands anywhere else (a WHERE, a join, a sort or
208
+ * group key, a FROM item's function), or one reads a join's merged column or a query outside `q` */
209
+ function listSubLinkReads(q) {
210
+ if (q.hasSubLinks === "false")
211
+ return [];
212
+ const rtable = treeList(q.rtable);
213
+ const reads = new Set();
214
+ let ok = true, elsewhere = false;
215
+ // anything of `q` but its select list: a subquery there is elsewhere (its FROM items' own queries are their own)
216
+ const outside = (v) => {
217
+ if (elsewhere)
218
+ return;
219
+ if (Array.isArray(v)) {
220
+ for (const x of v)
221
+ outside(x);
222
+ return;
223
+ }
224
+ if (!isTreeNode(v))
225
+ return;
226
+ if (v._node === "SUBLINK") {
227
+ elsewhere = true;
228
+ return;
229
+ }
230
+ if (v._node === "QUERY")
231
+ return;
232
+ for (const [k, x] of Object.entries(v))
233
+ if (k !== "_node")
234
+ outside(x);
235
+ };
236
+ for (const [k, x] of Object.entries(q)) {
237
+ if (k === "_node" || k === "targetList" || k === "cteList")
238
+ continue;
239
+ if (k === "rtable") {
240
+ for (const e of rtable)
241
+ if (isTreeNode(e))
242
+ for (const [f, y] of Object.entries(e))
243
+ if (f !== "subquery" && f !== "_node")
244
+ outside(y);
245
+ continue;
246
+ }
247
+ outside(x);
248
+ }
249
+ // a subselect's columns of `q`: a VAR as many levels up as it sits below `q`
250
+ const inner = (v, depth) => {
251
+ if (!ok)
252
+ return;
253
+ if (Array.isArray(v)) {
254
+ for (const x of v)
255
+ inner(x, depth);
256
+ return;
257
+ }
258
+ if (!isTreeNode(v))
259
+ return;
260
+ if (v._node === "VAR") {
261
+ const up = Number(v.varlevelsup ?? 0);
262
+ if (up > depth) {
263
+ ok = false;
264
+ return;
265
+ }
266
+ if (up === depth && depth > 0) {
267
+ const e = rtable[Number(v.varno) - 1];
268
+ // a whole row of the table read itself would list what its view of visible columns does not
269
+ if (!isTreeNode(e) || e.rtekind === RTE_JOIN || v.varattno === "0" || !isTreeNode(e.eref) || typeof e.eref.aliasname !== "string") {
270
+ ok = false;
271
+ return;
272
+ }
273
+ reads.add(e.eref.aliasname);
274
+ }
275
+ }
276
+ // a WITH query of `q` read in a subselect: the rewrite moves `q`'s unread WITH queries into the inner query, below
277
+ // the list that reads it, so the statement stays as it was (one of a query above `q` stays where it is)
278
+ if (v.rtekind === RTE_CTE && Number(v.ctelevelsup ?? 0) === depth && depth > 0) {
279
+ ok = false;
280
+ return;
281
+ }
282
+ const below = v._node === "QUERY" ? depth + 1 : depth;
283
+ for (const [k, x] of Object.entries(v))
284
+ if (k !== "_node")
285
+ inner(x, below);
286
+ };
287
+ for (const t of treeList(q.targetList)) {
288
+ if (!isTreeNode(t))
289
+ continue;
290
+ let held = false;
291
+ walkTree(t.expr, (n) => { held ||= n._node === "SUBLINK"; });
292
+ if (!held)
293
+ continue;
294
+ // a sort or group key, or an entry the list does not show (a sort's own): elsewhere
295
+ if (t.resjunk === "true" || (t.ressortgroupref !== undefined && t.ressortgroupref !== "0"))
296
+ return null;
297
+ inner(t.expr, 0);
298
+ }
299
+ return ok && !elsewhere ? [...reads].sort() : null;
300
+ }
301
+ /** each row of the query is one row of its FROM, its list reading that row alone, a subquery in it as `listSubLinkReads` says */
302
+ const rowWiseQuery = (q) => ["hasAggs", "hasWindowFuncs", "hasTargetSRFs"].every((f) => q[f] === "false") && listSubLinkReads(q) !== null;
303
+ /** `rowWiseStatement`, with what a joined query carries set apart: a derived read beside another FROM item may hold
304
+ * anything, because its columns pass through unchanged. A wrapper of one item or a set operation's arm has no such side.
305
+ * A subquery in a select list is allowed here (the list is computed for the chosen rows, `joinRows`), never in it */
306
+ function rowWiseAround(tree) {
307
+ const query = (q) => {
308
+ if (!rowWiseQuery(q))
309
+ return false;
310
+ const rtable = (Array.isArray(q.rtable) ? q.rtable : []).filter((e) => isTreeNode(e));
311
+ const beside = q.setOperations === null && rtable.filter((e) => e.rtekind !== RTE_JOIN).length >= 2;
312
+ const inner = [...rtable.flatMap((e) => isTreeNode(e.subquery) ? [e.subquery] : []),
313
+ ...(Array.isArray(q.cteList) ? q.cteList : []).flatMap((c) => isTreeNode(c) && isTreeNode(c.ctequery) ? [c.ctequery] : [])];
314
+ return inner.every((c) => beside || query(c));
315
+ };
316
+ return isTreeNode(tree) && tree._node === "QUERY" && query(tree);
317
+ }
318
+ /** every node of a select-list expression is row-wise, and it computes something (a plain column needs no deferring).
319
+ * With `subLinks`, a subquery is too: its own select is not looked into, only what it tests (`x IN (…)`'s x) */
320
+ function rowWise(n, subLinks = false) {
44
321
  let ok = true, computes = false;
45
322
  const visit = (v) => {
46
323
  if (!ok)
@@ -53,11 +330,16 @@ function rowWise(n) {
53
330
  return;
54
331
  const k = kindOf(v);
55
332
  if (/^[A-Z]/.test(k) && isObject(v[k])) {
333
+ const b = bodyOf(v);
334
+ if (k === "SubLink" && subLinks) {
335
+ computes = true;
336
+ visit(b.testexpr);
337
+ return;
338
+ }
56
339
  if (!ROW_WISE.has(k)) {
57
340
  ok = false;
58
341
  return;
59
342
  }
60
- const b = bodyOf(v);
61
343
  if (k === "FuncCall") {
62
344
  const name = b.funcname.map((x) => bodyOf(x).sval).join(".").toLowerCase();
63
345
  if (b.over !== undefined || b.agg_star === true || b.agg_distinct === true || b.agg_order !== undefined || b.agg_filter !== undefined || SETS.has(name)) {
@@ -88,6 +370,10 @@ function overLate(n, columns) {
88
370
  if (!isObject(n))
89
371
  return n;
90
372
  if (kindOf(n) === "ColumnRef" && isObject(n.ColumnRef)) {
373
+ // a lambda's parameter (the rewrite's own name) is no column of the table
374
+ const names = fieldNames(n.ColumnRef);
375
+ if (names.length === 1 && typeof names[0] === "string" && ownName(names[0]))
376
+ return n;
91
377
  const key = JSON.stringify(n.ColumnRef.fields);
92
378
  let name = columns.get(key);
93
379
  if (name === undefined) {
@@ -98,19 +384,40 @@ function overLate(n, columns) {
98
384
  }
99
385
  return Object.fromEntries(Object.entries(n).map(([k, v]) => [k, overLate(v, columns)]));
100
386
  }
387
+ /** with no catalog to ask, every function is volatile: nothing is read twice */
388
+ const UNKNOWN = { volatile: async (functions) => new Set(functions), wall: null };
101
389
  /**
102
390
  * The statement with every sorted, limited SELECT written with its select list computed after its limit (see above),
103
- * or null where none is. `tree` is the rewrite's DuckDB tree, `analysis` Postgres's of the original.
391
+ * or null where none is. `tree` is the rewrite's DuckDB tree, `analysis` Postgres's of the original, `keys` reads the
392
+ * catalog's primary keys of the tables `fanOutTables` names: only for a statement this may write, so a tool's
393
+ * `SELECT count(*) … JOIN … LIMIT 2000` costs no round trip to the catalog.
104
394
  */
105
- export function selectListAfterLimit(tree, analysis) {
106
- if (!rowWiseStatement(analysis))
395
+ export async function selectListAfterLimit(tree, analysis, keys, context = UNKNOWN) {
396
+ const strict = rowWiseStatement(analysis);
397
+ const around = strict || rowWiseAround(analysis);
398
+ if (!around && !groupsWithLimit(analysis))
107
399
  return null;
400
+ const volatile = await context.volatile(derivedFunctions(analysis));
401
+ const wall = context.wall;
402
+ const relations = relationsOf(analysis, await keys(), volatile, wall?.alias ?? null);
108
403
  let changed = false;
404
+ const written = (select) => {
405
+ const grouped = keyFirst(select, relations);
406
+ if (grouped !== null || !around)
407
+ return grouped;
408
+ const rows = joinRows(select, relations, wall?.alias ?? null);
409
+ if (rows !== null) {
410
+ if (wall !== null)
411
+ wall.added += rows.walls;
412
+ return rows.select;
413
+ }
414
+ return strict ? afterLimit(select) : null;
415
+ };
109
416
  // bottom up: a SELECT's derived tables and WITH queries first, then the SELECT (never what it was written as)
110
417
  const visit = (n) => {
111
418
  if (Array.isArray(n))
112
419
  return n.map(visit);
113
- if (!isObject(n))
420
+ if (!isObject(n) || kindOf(n) === "SubLink")
114
421
  return n;
115
422
  const out = Object.fromEntries(Object.entries(n).map(([k, v]) => [k, visit(v)]));
116
423
  if (kindOf(out) !== "SelectStmt")
@@ -120,62 +427,363 @@ export function selectListAfterLimit(tree, analysis) {
120
427
  for (const arm of ["larg", "rarg"]) {
121
428
  if (!isObject(body[arm]))
122
429
  continue;
123
- const late = afterLimit({ SelectStmt: body[arm] });
430
+ const late = written({ SelectStmt: body[arm] });
124
431
  if (late !== null) {
125
432
  body[arm] = bodyOf(late);
126
433
  changed = true;
127
434
  }
128
435
  }
129
- const late = afterLimit(out);
436
+ const late = written(out);
130
437
  if (late === null)
131
438
  return out;
132
439
  changed = true;
133
440
  return late;
134
441
  };
135
- const written = visit(tree);
136
- return changed ? written : null;
442
+ const result = visit(tree);
443
+ return changed ? result : null;
137
444
  }
138
- /** one SELECT written so, or null where it is not so shaped */
139
- function afterLimit(select) {
140
- const s = bodyOf(select);
445
+ /** a SELECT with a LIMIT, each row one row of its FROM: its list and its sort, which may be none */
446
+ function limited(s, withs = false, on = false) {
141
447
  if (s.op !== undefined && s.op !== "SETOP_NONE")
142
448
  return null;
143
- for (const f of ["withClause", "distinctClause", "groupClause", "havingClause", "windowClause", "lockingClause", "intoClause", "valuesLists"]) {
449
+ if (s.withClause !== undefined && (!withs || s.withClause.recursive === true))
450
+ return null;
451
+ if (!on && distinctOnOf(s) !== null)
452
+ return null;
453
+ for (const f of [...on ? [] : ["distinctClause"], "groupClause", "havingClause", "windowClause", "lockingClause", "intoClause", "valuesLists"]) {
144
454
  if (s[f] !== undefined && !(Array.isArray(s[f]) && s[f].length === 0))
145
455
  return null;
146
456
  }
147
457
  if (s.limitCount === undefined || (s.limitOption !== undefined && s.limitOption !== "LIMIT_OPTION_COUNT" && s.limitOption !== "LIMIT_OPTION_DEFAULT"))
148
458
  return null;
149
459
  const targets = s.targetList;
150
- const sorts = s.sortClause;
151
- if (targets === undefined || sorts === undefined || sorts.length === 0)
460
+ if (targets === undefined)
152
461
  return null;
153
- let computes = false;
462
+ return { targets, sorts: s.sortClause ?? [] };
463
+ }
464
+ /** a SELECT's DISTINCT ON expressions as written, or null for none (a plain DISTINCT is one empty entry) */
465
+ function distinctOnOf(s) {
466
+ const d = s.distinctClause;
467
+ return Array.isArray(d) && d.length > 0 && d.every((x) => kindOf(x) !== null) ? d : null;
468
+ }
469
+ /** each DISTINCT ON expression over the FROM clause, as Postgres reads it (findTargetlistEntrySQL92, as an ORDER BY's):
470
+ * an ordinal or an output's bare name is that output's own expression, a clone each (the inner list holds Mason's names
471
+ * alone), written so the engine evaluates it; any other expression as written. Null where a bare output name stands
472
+ * inside an expression that is not that output's own (the rewrite's equality key of a jsonb named by its output), which
473
+ * the inner query would read as an input column */
474
+ function distinctKeys(targets, on) {
475
+ const out = [];
476
+ for (const d of on) {
477
+ let target;
478
+ if (kindOf(d) === "A_Const" && isObject(bodyOf(d).ival)) {
479
+ target = targets[Number(bodyOf(d).ival.ival ?? 0) - 1];
480
+ if (target === undefined)
481
+ return null;
482
+ }
483
+ else if (kindOf(d) === "ColumnRef" && bodyOf(d).fields.length === 1) {
484
+ const name = fieldNames(bodyOf(d))[0];
485
+ const named = targets.filter((t) => outputName(t) === name);
486
+ if (named.length > 1)
487
+ return null;
488
+ target = named[0];
489
+ }
490
+ if (target !== undefined) {
491
+ out.push(evaluated(structuredClone(bodyOf(target).val)));
492
+ continue;
493
+ }
494
+ let shadowed = false;
495
+ const walk = (n) => {
496
+ if (Array.isArray(n)) {
497
+ n.forEach(walk);
498
+ return;
499
+ }
500
+ if (!isObject(n))
501
+ return;
502
+ if (kindOf(n) === "ColumnRef" && isObject(n.ColumnRef)) {
503
+ const names = fieldNames(n.ColumnRef);
504
+ if (names.length === 1)
505
+ shadowed ||= targets.some((t) => outputName(t) === names[0] && !sameTree(bodyOf(t).val, n));
506
+ return;
507
+ }
508
+ Object.values(n).forEach(walk);
509
+ };
510
+ walk(d);
511
+ if (shadowed)
512
+ return null;
513
+ out.push(evaluated(structuredClone(d)));
514
+ }
515
+ return out;
516
+ }
517
+ /** whether a query of the statement groups, or is a DISTINCT that is a grouping, and has a LIMIT: one `keyFirst` may write */
518
+ function groupsWithLimit(tree) {
519
+ let found = false;
520
+ walkTree(tree, (n) => { found ||= n._node === "QUERY" && isTreeNode(n.limitCount) && (treeList(n.groupClause).length > 0 || distinctGrouping(n)); });
521
+ return found;
522
+ }
523
+ /** `keyGrouped`'s tables of a query by the alias its FROM reads them by, each key's columns by name */
524
+ function keyedTables(q, keys) {
525
+ const rtable = treeList(q.rtable);
526
+ return [...keyGrouped(q, keys)].flatMap(([rtindex, attnos]) => {
527
+ const e = rtable[rtindex - 1];
528
+ if (!isTreeNode(e) || !isTreeNode(e.eref) || typeof e.eref.aliasname !== "string")
529
+ return [];
530
+ const names = treeList(e.eref.colnames);
531
+ const key = attnos.map((a) => names[a - 1]);
532
+ return key.every((k) => typeof k === "string" && k !== "") ? [{ alias: e.eref.aliasname, key: key }] : [];
533
+ }).sort((a, b) => a.alias.localeCompare(b.alias));
534
+ }
535
+ const anyValue = (arg) => ({ FuncCall: { funcname: [S("any_value")], args: [arg], funcformat: "COERCE_EXPLICIT_CALL" } });
536
+ /**
537
+ * KEY-FIRST GROUPING (issue 134, step 6: `GROUP BY` a table's primary key with its wide columns in the list and a LIMIT
538
+ * carried every wide value through the hash aggregate, the rewrite's `any_value` of each (src/duck/rewrite.ts
539
+ * `decidedByKey`), for every group before the limit kept a hundred: 29 GiB spilled and a failure, against 0.76 s). Where
540
+ * the analysis proves a table's WHOLE primary key grouped (`keyGrouped`, src/duck/distinct-key.ts: pg_index's primary
541
+ * key, never a UNIQUE index), a group holds one row of that table, so its columns are read by that row's number after
542
+ * the limit:
543
+ *
544
+ * SELECT <the list, each column of the keyed table read of it> FROM (
545
+ * SELECT any_value(t.rowid) AS __mason_r1, <every other output> AS __mason_c<i>, <each sort key> AS __mason_o<i>
546
+ * FROM … WHERE … GROUP BY <as written, less a column of t only the list reads> HAVING … ORDER BY … LIMIT … OFFSET …
547
+ * ) AS __mason_late LEFT JOIN t ON t.rowid = __mason_late.__mason_r1
548
+ * ORDER BY __mason_late.__mason_o<i> …
549
+ *
550
+ * The groups are grouped as written, by the key, never by a row number: a key the copy holds twice for a moment is one
551
+ * group, as on the source, and `any_value` reads one of its rows. A column of the table grouped beside its key and read
552
+ * nowhere but as an output is left out of the grouping, being decided by the key. An output fetched is a column of a
553
+ * keyed table, bare or as the rewrite's `any_value` of it, under casts; every other output (an aggregate, a key of
554
+ * another table, a grouped expression) is computed inside as it was, and a NULL row number (an outer join's group of
555
+ * no row) reads NULL, as the group's `any_value` did. What stays as it was: grouping sets, a DISTINCT beside a GROUP BY,
556
+ * a row lock, no LIMIT in fact, a FROM item a join names, and a table with a column named rowid.
557
+ *
558
+ * A SELECT DISTINCT whose list holds a table's whole primary key (task D) is the same shape: a DISTINCT is a grouping on
559
+ * its list (`distinctGrouping`, from the analysis: never a DISTINCT ON, nor one over an aggregate, a window or a
560
+ * set-returning function), so it is written as a GROUP BY of each output, and on as above. Where the rewrite keyed a
561
+ * jsonb output's equality (`keyedDistinct`: its DISTINCT ON of the outputs' keys), the grouping is those keys, and an
562
+ * output so keyed must be one the fetch reads, so no raw jsonb is shown from a group it does not decide.
563
+ */
564
+ function keyFirst(select, relations) {
565
+ const s = bodyOf(select);
566
+ if (s.op !== undefined && s.op !== "SETOP_NONE")
567
+ return null;
568
+ for (const f of ["lockingClause", "intoClause", "valuesLists"]) {
569
+ if (s[f] !== undefined && !(Array.isArray(s[f]) && s[f].length === 0))
570
+ return null;
571
+ }
572
+ const distinct = Array.isArray(s.distinctClause) && s.distinctClause.length > 0 ? s.distinctClause : null;
573
+ const groupClause = s.groupClause;
574
+ if (distinct !== null && Array.isArray(groupClause) && groupClause.length > 0)
575
+ return null;
576
+ if (distinct === null && (!Array.isArray(groupClause) || groupClause.length === 0 || groupClause.some((g) => kindOf(g) === "GroupingSet")))
577
+ return null;
578
+ if (s.limitCount === undefined || noLimit(s.limitCount) || (s.limitOption !== undefined && s.limitOption !== "LIMIT_OPTION_COUNT" && s.limitOption !== "LIMIT_OPTION_DEFAULT"))
579
+ return null;
580
+ const items = fromItems(s.fromClause);
581
+ if (items === null)
582
+ return null;
583
+ const aliases = items.map(aliasOfItem);
584
+ if (aliases.some((a) => a === null) || new Set(aliases).size !== aliases.length)
585
+ return null;
586
+ const named = relations.filter((q) => q.items.length === aliases.length && q.items.every((t, i) => t?.alias === aliases[i]));
587
+ const first = named[0];
588
+ if (first === undefined || named.some((q) => JSON.stringify([q.items, q.keyed, q.distinct]) !== JSON.stringify([first.items, first.keyed, first.distinct])))
589
+ return null;
590
+ // a DISTINCT is the analysis's grouping on its list, never a DISTINCT ON
591
+ if (distinct !== null && !first.distinct)
592
+ return null;
593
+ const keyed = new Map(first.keyed.flatMap((k) => {
594
+ const at = aliases.indexOf(k.alias), r = first.items[at];
595
+ const ok = r?.kind === "table" && kindOf(items[at]) === "RangeVar" && !r.columns.some((c) => c === "" || c.toLowerCase() === "rowid");
596
+ return ok ? [[k.alias, { ...k, at, columns: r.columns }]] : [];
597
+ }));
598
+ if (keyed.size === 0)
599
+ return null;
600
+ const targets = s.targetList;
601
+ if (targets === undefined || targets.some((t) => kindOf(t) !== "ResTarget" || hasStarTarget(t)))
602
+ return null;
603
+ // a DISTINCT's grouping: each output, or the rewrite's key of each where it keyed a jsonb's equality, one per output
604
+ const plainDistinct = distinct !== null && distinct.length === 1 && (!isObject(distinct[0]) || Object.keys(distinct[0]).length === 0);
605
+ if (distinct !== null && !plainDistinct && (distinct.length !== targets.length || !distinct.every((d) => isObject(d) && kindOf(d) !== null)))
606
+ return null;
607
+ const groups = distinct === null ? groupClause : plainDistinct ? targets.map((t) => structuredClone(bodyOf(t).val)) : distinct;
608
+ // a column of a keyed table, as a ColumnRef names it: its alias and its column, or null
609
+ const columnOf = (n) => {
610
+ if (kindOf(n) !== "ColumnRef")
611
+ return null;
612
+ const names = fieldNames(bodyOf(n));
613
+ const column = names.at(-1);
614
+ if (typeof column !== "string" || names.length > 2)
615
+ return null;
616
+ if (names.length === 2)
617
+ return typeof names[0] === "string" && keyed.get(names[0])?.columns.includes(column) ? { alias: names[0], column } : null;
618
+ const owners = first.items.filter((r) => r?.columns.includes(column));
619
+ return owners.length === 1 && keyed.has(owners[0].alias) ? { alias: owners[0].alias, column } : null;
620
+ };
621
+ // an output read of a keyed table's row: its column, bare or as any_value of it, under casts
622
+ const fetchOf = (val) => {
623
+ if (kindOf(val) === "TypeCast") {
624
+ const inner = fetchOf(bodyOf(val).arg);
625
+ return inner === null ? null : { alias: inner.alias, outer: { TypeCast: { ...structuredClone(bodyOf(val)), arg: inner.outer } } };
626
+ }
627
+ let column = val;
628
+ if (kindOf(val) === "FuncCall") {
629
+ const b = bodyOf(val), args = b.args;
630
+ const plain = Object.keys(b).every((k) => ["funcname", "args", "funcformat", "location"].includes(k));
631
+ if (!plain || fieldNames({ fields: b.funcname }).join(".").toLowerCase() !== "any_value" || args?.length !== 1)
632
+ return null;
633
+ column = args[0];
634
+ }
635
+ const c = columnOf(column);
636
+ return c === null ? null : { alias: c.alias, outer: ref(c.alias, c.column) };
637
+ };
638
+ // an ordinal or an output's name in the GROUP BY is that output's expression: the inner list is another list
639
+ const outputs = new Map();
154
640
  for (const t of targets) {
155
- if (kindOf(t) !== "ResTarget")
641
+ const name = bodyOf(t).name;
642
+ if (typeof name === "string")
643
+ outputs.set(name, [...outputs.get(name) ?? [], bodyOf(t).val]);
644
+ }
645
+ const inputs = new Set(first.items.flatMap((r) => r?.columns ?? []));
646
+ const grouping = [];
647
+ for (const g of groups) {
648
+ // a DISTINCT's grouping is its outputs' values: a constant among them groups nothing (and DuckDB reads an integer
649
+ // in a GROUP BY as an ordinal)
650
+ if (distinct !== null) {
651
+ if (kindOf(g) !== "A_Const")
652
+ grouping.push(structuredClone(g));
653
+ continue;
654
+ }
655
+ if (kindOf(g) === "A_Const" && isObject(bodyOf(g).ival)) {
656
+ const t = targets[Number(bodyOf(g).ival.ival ?? 0) - 1];
657
+ if (t === undefined)
658
+ return null;
659
+ grouping.push(structuredClone(bodyOf(t).val));
660
+ continue;
661
+ }
662
+ if (kindOf(g) === "A_Const")
156
663
  return null;
157
- const r = rowWise(bodyOf(t).val);
158
- if (!r.ok)
664
+ const bare = kindOf(g) === "ColumnRef" ? fieldNames(bodyOf(g)) : [];
665
+ const named1 = bare.length === 1 && typeof bare[0] === "string" && !inputs.has(bare[0]) ? outputs.get(bare[0]) : undefined;
666
+ if (named1 !== undefined && named1.length !== 1)
159
667
  return null;
160
- computes ||= r.computes;
668
+ grouping.push(structuredClone(named1?.[0] ?? g));
161
669
  }
162
- if (!computes)
670
+ const shown = new Map();
671
+ const sorted = sortKeys(targets, s.sortClause ?? [], shown);
672
+ if (sorted === null)
163
673
  return null;
164
- // each sort key as an expression over the FROM clause: an ordinal or an output column's name is that output's own
165
- // expression, as Postgres reads it (an output name before an input column's)
166
- const outputName = (t) => {
167
- const b = bodyOf(t);
168
- if (typeof b.name === "string")
169
- return b.name;
170
- const v = b.val;
171
- if (kindOf(v) === "ColumnRef") {
172
- const last = bodyOf(v).fields.at(-1);
173
- return kindOf(last) === "String" ? bodyOf(last).sval : null;
674
+ const { keys, sorts } = sorted;
675
+ const fetched = [];
676
+ const carried = [];
677
+ const list = targets.map((t, i) => {
678
+ const b = bodyOf(t), at = shown.get(i);
679
+ if (at !== undefined)
680
+ return { ResTarget: { ...b, val: ref(LATE, `__mason_o${at + 1}`) } };
681
+ const f = fetchOf(b.val);
682
+ if (f !== null) {
683
+ if (!fetched.includes(f.alias))
684
+ fetched.push(f.alias);
685
+ return { ResTarget: { ...b, val: f.outer } };
174
686
  }
687
+ // an output a DISTINCT groups by the rewrite's key of it, not by itself, is not computed inside
688
+ if (distinct !== null && JSON.stringify(groups[i]) !== JSON.stringify(b.val))
689
+ return null;
690
+ const name = `__mason_c${carried.length + 1}`;
691
+ carried.push({ ResTarget: { name, val: b.val } });
692
+ return { ResTarget: { ...b, val: ref(LATE, name) } };
693
+ });
694
+ if (fetched.length === 0 || list.some((t) => t === null))
175
695
  return null;
696
+ // a grouped column of a keyed table, not its key, read nowhere inside but where it is grouped: decided by the key
697
+ const mentions = (n, c) => {
698
+ if (Array.isArray(n))
699
+ return n.some((x) => mentions(x, c));
700
+ if (!isObject(n))
701
+ return false;
702
+ const own = columnOf(n);
703
+ if (own !== null)
704
+ return own.alias === c.alias && own.column === c.column;
705
+ return Object.values(n).some((x) => mentions(x, c));
176
706
  };
177
- const keys = [];
178
- for (const sort of sorts) {
707
+ // (or the rewrite's key of one, a jsonb's: its columns alone, through Mason's own functions, nothing volatile)
708
+ const decided = (g) => {
709
+ const out = [];
710
+ let ok = true;
711
+ const walk = (n) => {
712
+ if (!ok)
713
+ return;
714
+ if (Array.isArray(n)) {
715
+ n.forEach(walk);
716
+ return;
717
+ }
718
+ if (!isObject(n))
719
+ return;
720
+ const k = kindOf(n);
721
+ if (k === "ColumnRef") {
722
+ const c = columnOf(n);
723
+ if (c === null || keyed.get(c.alias).key.includes(c.column))
724
+ ok = false;
725
+ else
726
+ out.push(c);
727
+ return;
728
+ }
729
+ if (k === "SubLink") {
730
+ ok = false;
731
+ return;
732
+ }
733
+ if (k === "FuncCall") {
734
+ const name = fieldNames({ fields: bodyOf(n).funcname }).join(".");
735
+ if (!name.startsWith("__mason_") && name !== "getvariable") {
736
+ ok = false;
737
+ return;
738
+ }
739
+ }
740
+ Object.values(n).forEach(walk);
741
+ };
742
+ walk(g);
743
+ return ok && out.length > 0 ? out : null;
744
+ };
745
+ const kept = grouping.filter((g, i) => {
746
+ const columns = decided(g);
747
+ if (columns === null)
748
+ return true;
749
+ return columns.some((c) => mentions([carried, keys, s.havingClause, s.windowClause, grouping.filter((_x, j) => j !== i)], c));
750
+ });
751
+ const fromInner = mapFrom(s.fromClause, (n, i) => keyed.has(aliases[i]) ? ownTable(bodyOf(n)) : n);
752
+ const { sortClause: _sorts, ...unsorted } = s;
753
+ const inner = { ...unsorted, fromClause: fromInner, groupClause: kept, distinctClause: undefined, targetList: [
754
+ ...fetched.map((alias, i) => ({ ResTarget: { name: `__mason_r${i + 1}`, val: anyValue(rowNumber(alias)) } })),
755
+ ...carried,
756
+ ...keys.map((k, i) => ({ ResTarget: { name: `__mason_o${i + 1}`, val: k } })),
757
+ ] };
758
+ if (sorts.length > 0)
759
+ inner.sortClause = sorts.map((sort, i) => ({ SortBy: { ...bodyOf(sort), node: structuredClone(keys[i]) } }));
760
+ let joined = { RangeSubselect: { subquery: { SelectStmt: inner }, alias: { aliasname: LATE } } };
761
+ fetched.forEach((alias, i) => {
762
+ joined = { JoinExpr: { jointype: "JOIN_LEFT", larg: joined, rarg: ownTable(bodyOf(items[keyed.get(alias).at])), quals: { A_Expr: {
763
+ kind: "AEXPR_OP", name: [S("=")], lexpr: rowNumber(alias), rexpr: ref(LATE, `__mason_r${i + 1}`)
764
+ } } } };
765
+ });
766
+ return { SelectStmt: {
767
+ targetList: list,
768
+ fromClause: [joined],
769
+ ...(sorts.length > 0 ? { sortClause: sorts.map((sort, i) => ({ SortBy: { ...bodyOf(sort), node: ref(LATE, `__mason_o${i + 1}`) } })) } : {}),
770
+ limitOption: "LIMIT_OPTION_DEFAULT", op: "SETOP_NONE",
771
+ } };
772
+ }
773
+ /** a limited SELECT that sorts: only one the keys alone put back in order may compute its list after the limit */
774
+ function sortedLimited(s) {
775
+ const shape = limited(s);
776
+ return shape === null || shape.sorts.length === 0 ? null : shape;
777
+ }
778
+ /** LIMIT ALL and LIMIT NULL, which limit nothing: the parser's constant NULL, cast or not */
779
+ const noLimit = (n) => kindOf(n) === "A_Const" ? bodyOf(n).isnull === true : kindOf(n) === "TypeCast" && noLimit(bodyOf(n).arg);
780
+ /** each sort key as an expression over the FROM clause, or null: an ordinal or an output column's name is that
781
+ * output's own expression, as Postgres reads it (an output name before an input column's). Without `shown`, a sort by a
782
+ * computed output is left alone: over one table it computes that output for every row anyway, so nothing is deferred.
783
+ * With it, the key is that output, and `shown` says which key each computed output is, by the output's place: one value */
784
+ function sortKeys(targets, written, shown = null) {
785
+ const keys = [], sorts = [];
786
+ for (const sort of written) {
179
787
  if (kindOf(sort) !== "SortBy")
180
788
  return null;
181
789
  const b = bodyOf(sort);
@@ -195,17 +803,52 @@ function afterLimit(select) {
195
803
  return null;
196
804
  target = named[0];
197
805
  }
806
+ const computed = (t) => rowWise(bodyOf(t).val).computes;
807
+ sorts.push(sort);
198
808
  if (target !== undefined) {
199
- // a sort by a computed output computes it for every row anyway: nothing to defer
200
- if (rowWise(bodyOf(target).val).computes)
201
- return null;
202
- keys.push(structuredClone(bodyOf(target).val));
809
+ if (computed(target)) {
810
+ if (shown === null)
811
+ return null;
812
+ if (!shown.has(targets.indexOf(target)))
813
+ shown.set(targets.indexOf(target), keys.length);
814
+ }
815
+ // (an output a rewrite wrote as a bare literal: a key the engine evaluates, never an ordinal of the query it is in)
816
+ keys.push(evaluated(structuredClone(bodyOf(target).val)));
203
817
  }
204
818
  else if (kindOf(node) === "A_Const")
205
819
  return null;
206
- else
820
+ else {
821
+ // the expression repeated: Postgres's first output equal to it is this key (findTargetlistEntrySQL99)
822
+ const at = shown === null ? -1 : targets.findIndex((t) => computed(t) && sameTree(bodyOf(t).val, node));
823
+ if (at >= 0 && !shown.has(at))
824
+ shown.set(at, keys.length);
207
825
  keys.push(structuredClone(node));
826
+ }
208
827
  }
828
+ return { keys, sorts };
829
+ }
830
+ /** one SELECT written so, or null where it is not so shaped */
831
+ function afterLimit(select) {
832
+ const s = bodyOf(select);
833
+ const shape = sortedLimited(s);
834
+ if (shape === null)
835
+ return null;
836
+ const { targets } = shape;
837
+ let computes = false;
838
+ for (const t of targets) {
839
+ if (kindOf(t) !== "ResTarget")
840
+ return null;
841
+ const r = rowWise(bodyOf(t).val);
842
+ if (!r.ok)
843
+ return null;
844
+ computes ||= r.computes;
845
+ }
846
+ if (!computes)
847
+ return null;
848
+ const sorted = sortKeys(targets, shape.sorts);
849
+ if (sorted === null || sorted.sorts.length === 0)
850
+ return null;
851
+ const { keys, sorts } = sorted;
209
852
  const columns = new Map();
210
853
  const list = targets.map((t) => ({ ResTarget: { ...bodyOf(t), val: overLate(bodyOf(t).val, columns) } }));
211
854
  const inner = { ...s, targetList: [
@@ -219,3 +862,789 @@ function afterLimit(select) {
219
862
  limitOption: "LIMIT_OPTION_DEFAULT", op: "SETOP_NONE",
220
863
  } };
221
864
  }
865
+ /** two expressions the same but for where they were written */
866
+ const sameTree = (a, b) => {
867
+ const placeless = (_k, v) => _k === "location" ? undefined : v;
868
+ return JSON.stringify(a, placeless) === JSON.stringify(b, placeless);
869
+ };
870
+ const RTE_RELATION = "0", RTE_SUBQUERY = "1", RTE_JOIN = "2", RTE_CTE = "6", RTE_GROUP = "9";
871
+ /** ctematerialized: CTEMaterializeAlways */
872
+ const MATERIALIZED = "1";
873
+ function isTreeNode(v) { return typeof v === "object" && v !== null && !Array.isArray(v); }
874
+ const treeList = (v) => Array.isArray(v) ? v : [];
875
+ /** every node of an analysed tree, its query's own and its subqueries' */
876
+ function walkTree(v, at) {
877
+ if (Array.isArray(v)) {
878
+ for (const x of v)
879
+ walkTree(x, at);
880
+ return;
881
+ }
882
+ if (!isTreeNode(v))
883
+ return;
884
+ at(v);
885
+ for (const [k, x] of Object.entries(v))
886
+ if (k !== "_node")
887
+ walkTree(x, at);
888
+ }
889
+ /** what a query sorts by that Postgres's analysis says is no bare column: each such target, and its place among the
890
+ * outputs (null for one the list does not show). An ordinal, an output's name and a repeated expression are one target */
891
+ function computedSorts(q) {
892
+ const targets = treeList(q.targetList).filter((t) => isTreeNode(t));
893
+ return treeList(q.sortClause).flatMap((c) => {
894
+ const target = isTreeNode(c) ? targets.find((t) => t.ressortgroupref === c.tleSortGroupRef) : undefined;
895
+ if (target === undefined || !isTreeNode(target.expr) || target.expr._node === "VAR")
896
+ return [];
897
+ return [{ expr: target.expr, output: target.resjunk === "true" ? null : targets.indexOf(target) }];
898
+ });
899
+ }
900
+ /** the targets a query's DISTINCT ON names, by its clause's references */
901
+ function onTargets(q) {
902
+ const refs = new Set(treeList(q.distinctClause).flatMap((c) => isTreeNode(c) ? [c.tleSortGroupRef] : []));
903
+ return treeList(q.targetList).filter((t) => isTreeNode(t) && refs.has(t.ressortgroupref));
904
+ }
905
+ /** the functions a statement calls, and the operators', by OID, where a query with a LIMIT holds a derived read itself or
906
+ * sorts by something computed: none for any other (`SELECT … FROM (SELECT … LIMIT 10) s`), so it asks the catalog nothing */
907
+ function derivedFunctions(tree) {
908
+ const out = new Set();
909
+ let derived = false;
910
+ walkTree(tree, (n) => {
911
+ if (n._node === "QUERY" && n.limitCount !== null && n.limitCount !== undefined) {
912
+ derived ||= treeList(n.rtable).some((e) => isTreeNode(e) && (e.rtekind === RTE_SUBQUERY || e.rtekind === RTE_CTE)) || computedSorts(n).length > 0;
913
+ }
914
+ for (const id of [n._node === "FUNCEXPR" ? n.funcid : undefined, ["OPEXPR", "DISTINCTEXPR", "NULLIFEXPR", "SCALARARRAYOPEXPR"].includes(n._node) ? n.opfuncid : undefined]) {
915
+ if (typeof id === "string" && Number(id) > 0)
916
+ out.add(Number(id));
917
+ }
918
+ });
919
+ return derived ? [...out].sort((a, b) => a - b) : [];
920
+ }
921
+ /** whether an analysed tree calls a volatile function */
922
+ function holdsVolatile(v, volatile) {
923
+ let held = false;
924
+ walkTree(v, (n) => { held ||= volatile.has(Number(n.funcid ?? 0)) || volatile.has(Number(n.opfuncid ?? 0)); });
925
+ return held;
926
+ }
927
+ /** a query that may be read twice: a projection and a WHERE, nothing volatile, nothing of a query outside it */
928
+ function readTwice(q, volatile) {
929
+ if (!projectionOnly(q) || holdsVolatile(q, volatile))
930
+ return false;
931
+ // a column of a query outside it: a VAR whose levels up pass the queries it sits in below `q` (a role's `through:`
932
+ // filter reads its own row from its EXISTS, one level up, and that is inside)
933
+ let ok = true;
934
+ const walk = (v, depth) => {
935
+ if (!ok)
936
+ return;
937
+ if (Array.isArray(v)) {
938
+ for (const x of v)
939
+ walk(x, depth);
940
+ return;
941
+ }
942
+ if (!isTreeNode(v))
943
+ return;
944
+ if (v._node === "VAR" && Number(v.varlevelsup ?? 0) > depth) {
945
+ ok = false;
946
+ return;
947
+ }
948
+ const below = v._node === "QUERY" && v !== q ? depth + 1 : depth;
949
+ for (const [k, x] of Object.entries(v))
950
+ if (k !== "_node")
951
+ walk(x, below);
952
+ };
953
+ walk(q, 0);
954
+ return ok;
955
+ }
956
+ /** the casts a merged column's join adds that the fetch writes too: each to the type DuckDB reads the same value by */
957
+ const MERGE_CASTS = { "21": "int2", "23": "int4", "20": "int8", "700": "float4", "701": "float8" };
958
+ const NUMERIC = "1700";
959
+ /** COERCE_IMPLICIT_CAST: what a join's merged column is coerced by */
960
+ const IMPLICIT_CAST = "2";
961
+ /** a join's merged column as the fetch reads it, through Postgres's join alias variables (left for INNER and LEFT, right
962
+ * for RIGHT, coalesce for FULL, a merged column of a join below read through it), each as the FROM item's own column;
963
+ * null where it holds what this cannot write, a cast to another type: only the join reads that */
964
+ function mergedRead(v, rtable, within = false) {
965
+ if (!isTreeNode(v))
966
+ return null;
967
+ if (v._node === "RELABELTYPE")
968
+ return mergedRead(v.arg, rtable, within);
969
+ if (v._node === "VAR") {
970
+ const e = rtable[Number(v.varno) - 1];
971
+ if (!isTreeNode(e) || v.varlevelsup !== "0")
972
+ return null;
973
+ const at = Number(v.varattno) - 1;
974
+ if (e.rtekind === RTE_JOIN)
975
+ return mergedRead(treeList(e.joinaliasvars)[at], rtable, within);
976
+ const name = isTreeNode(e.eref) ? treeList(e.eref.colnames)[at] : null;
977
+ return isTreeNode(e.eref) && typeof e.eref.aliasname === "string" && typeof name === "string" ? ref(e.eref.aliasname, name) : null;
978
+ }
979
+ if (v._node === "COALESCEEXPR") {
980
+ const args = treeList(v.args).map((a) => mergedRead(a, rtable, true));
981
+ return args.some((a) => a === null) ? null : { CoalesceExpr: { args } };
982
+ }
983
+ if (v._node !== "FUNCEXPR" || v.funcformat !== IMPLICIT_CAST || treeList(v.args).length !== 1)
984
+ return null;
985
+ const arg = mergedRead(treeList(v.args)[0], rtable, within);
986
+ // an integer's text is its numeric's, alone; beside a numeric of decimals, coalesce would write it with them
987
+ if (v.funcresulttype === NUMERIC)
988
+ return within ? null : arg;
989
+ const type = MERGE_CASTS[String(v.funcresulttype)];
990
+ return arg === null || type === undefined ? null : { TypeCast: { arg, typeName: { names: [S(type)], typemod: -1 } } };
991
+ }
992
+ /** the columns the JOINs of a query merge (`Query.merged`, `Query.joins`). A name two joins merged apart, neither over the
993
+ * other, is read by no bare name: a `*` of it would be, so it is given no read and stays with the join */
994
+ function mergedColumns(q) {
995
+ const rtable = treeList(q.rtable);
996
+ const below = new Map();
997
+ walkTree(q.jointree, (n) => {
998
+ if (n._node !== "JOINEXPR")
999
+ return;
1000
+ const held = new Set();
1001
+ walkTree(n, (x) => { if (x._node === "JOINEXPR" && x !== n)
1002
+ held.add(Number(x.rtindex)); });
1003
+ below.set(Number(n.rtindex), held);
1004
+ });
1005
+ const merged = new Map();
1006
+ const joins = [];
1007
+ rtable.forEach((e, i) => {
1008
+ if (!isTreeNode(e) || e.rtekind !== RTE_JOIN)
1009
+ return;
1010
+ const names = (isTreeNode(e.eref) ? treeList(e.eref.colnames) : []).slice(0, Number(e.joinmergedcols)).filter((c) => typeof c === "string");
1011
+ joins.push(names);
1012
+ names.forEach((name, c) => {
1013
+ const had = merged.get(name);
1014
+ const over = had === undefined || below.get(i + 1)?.has(had.at) === true;
1015
+ merged.set(name, { at: i + 1, read: over ? mergedRead(treeList(e.joinaliasvars)[c], rtable) : null });
1016
+ });
1017
+ });
1018
+ return { merged: [...merged].map(([name, m]) => [name, m.read]), joins };
1019
+ }
1020
+ /** a query that is a role's walled read, as Postgres analysed it: one item, the barrier's select by the wall's name */
1021
+ function walledQuery(q, wall) {
1022
+ const rtable = treeList(q.rtable);
1023
+ const [e] = rtable;
1024
+ return rtable.length === 1 && isTreeNode(e) && e.rtekind === RTE_SUBQUERY && isTreeNode(e.eref) && e.eref.aliasname === wall;
1025
+ }
1026
+ /** each output column of an analysed query, whether the analysis made it a Const (a set operation's are its own VARs) */
1027
+ function constantColumns(q) {
1028
+ return treeList(q.targetList).filter((t) => isTreeNode(t) && t.resjunk !== "true").map((t) => constantExpression(t.expr));
1029
+ }
1030
+ function relationsOf(analysis, keys, volatile, wall) {
1031
+ const out = [];
1032
+ const queryOf = (q, enclosing) => {
1033
+ const scopes = [treeList(q.cteList), ...enclosing];
1034
+ const relation = (e) => {
1035
+ const ref = e.eref;
1036
+ if (typeof ref?.aliasname !== "string" || !Array.isArray(ref.colnames))
1037
+ return null;
1038
+ const alias = ref.aliasname, columns = ref.colnames.filter((c) => typeof c === "string");
1039
+ if (e.rtekind === RTE_RELATION)
1040
+ return e.relkind === "r" && Number(e.relid) >= FIRST_USER_OID ? { kind: "table", alias, columns } : null;
1041
+ if (wall !== null && e.rtekind === RTE_SUBQUERY && isTreeNode(e.subquery) && walledQuery(e.subquery, wall)) {
1042
+ return holdsVolatile(e.subquery, volatile) ? null : { kind: "walled", alias, columns };
1043
+ }
1044
+ if (e.rtekind === RTE_SUBQUERY && isTreeNode(e.subquery)) {
1045
+ return { kind: "derived", alias, columns, cte: null, read: e.lateral !== "true" && readTwice(e.subquery, volatile) ? queryOf(e.subquery, scopes) : null, constants: constantColumns(e.subquery) };
1046
+ }
1047
+ if (e.rtekind !== RTE_CTE)
1048
+ return null;
1049
+ const level = scopes[Number(e.ctelevelsup)] ?? [];
1050
+ const cte = level.find((c) => isTreeNode(c) && c.ctename === e.ctename);
1051
+ const once = cte !== undefined && cte.cterefcount === "1" && cte.ctematerialized !== MATERIALIZED && isTreeNode(cte.ctequery) && readTwice(cte.ctequery, volatile);
1052
+ return { kind: "derived", alias, columns, cte: once ? e.ctename : null, read: once ? queryOf(cte.ctequery, scopes.slice(Number(e.ctelevelsup))) : null,
1053
+ constants: cte !== undefined && isTreeNode(cte.ctequery) ? constantColumns(cte.ctequery) : [] };
1054
+ };
1055
+ // (a grouped query's RTE_GROUP entry, Postgres 17+, is no FROM item: it holds what the GROUP BY groups)
1056
+ const items = treeList(q.rtable).filter((e) => isTreeNode(e) && e.rtekind !== RTE_JOIN && e.rtekind !== RTE_GROUP).map(relation);
1057
+ const computed = computedSorts(q).filter((c) => holdsVolatile(c.expr, volatile));
1058
+ return { items, bounded: fanOutBounded(q, keys), rowWise: rowWiseQuery(q), listReads: listSubLinkReads(q) ?? [], listSubLinks: subLinksIn(treeList(q.targetList)), ...mergedColumns(q),
1059
+ volatileOutputs: computed.flatMap((c) => c.output === null ? [] : [c.output]), volatileKeys: computed.length > 0, sorts: treeList(q.sortClause).length, keyed: keyedTables(q, keys), distinct: distinctGrouping(q),
1060
+ distinctOn: q.hasDistinctOn === "true", volatileOn: onTargets(q).some((t) => holdsVolatile(t.expr, volatile)) };
1061
+ };
1062
+ const visit = (v, enclosing) => {
1063
+ if (Array.isArray(v)) {
1064
+ for (const x of v)
1065
+ visit(x, enclosing);
1066
+ return;
1067
+ }
1068
+ // a subquery's own SELECTs are never written (`selectListAfterLimit` leaves a SubLink as it is)
1069
+ if (!isTreeNode(v) || v._node === "SUBLINK")
1070
+ return;
1071
+ if (v._node === "QUERY" && Array.isArray(v.rtable))
1072
+ out.push(queryOf(v, enclosing));
1073
+ const inner = v._node === "QUERY" ? [treeList(v.cteList), ...enclosing] : enclosing;
1074
+ for (const [k, x] of Object.entries(v))
1075
+ if (k !== "_node")
1076
+ visit(x, inner);
1077
+ };
1078
+ visit(analysis, []);
1079
+ return out;
1080
+ }
1081
+ /** the FROM clause's items in order, or null where it holds anything but tables and derived tables joined, by no name */
1082
+ function fromItems(from) {
1083
+ const items = [];
1084
+ const walk = (n) => {
1085
+ const k = kindOf(n);
1086
+ if (k === "RangeVar" || k === "RangeSubselect") {
1087
+ items.push(n);
1088
+ return true;
1089
+ }
1090
+ if (k !== "JoinExpr")
1091
+ return false;
1092
+ const j = bodyOf(n);
1093
+ if (j.alias !== undefined || j.join_using_alias !== undefined)
1094
+ return false;
1095
+ return walk(j.larg) && walk(j.rarg);
1096
+ };
1097
+ return Array.isArray(from) && from.every(walk) ? items : null;
1098
+ }
1099
+ /** the FROM clause with each item replaced, by its place in `fromItems`'s order, and each JOIN by `join`, in the order
1100
+ * Postgres's range table holds them: its sides first */
1101
+ function mapFrom(from, f, join = (b) => b) {
1102
+ let at = 0, joins = 0;
1103
+ const walk = (n) => {
1104
+ if (kindOf(n) !== "JoinExpr")
1105
+ return f(n, at++);
1106
+ const larg = walk(bodyOf(n).larg), rarg = walk(bodyOf(n).rarg);
1107
+ return { JoinExpr: join({ ...bodyOf(n), larg, rarg }, joins++) };
1108
+ };
1109
+ return from.map(walk);
1110
+ }
1111
+ /** how many JOINs a FROM clause holds */
1112
+ function joinCount(from) {
1113
+ let joins = 0;
1114
+ mapFrom(from, (n) => n, (b) => { joins++; return b; });
1115
+ return joins;
1116
+ }
1117
+ /** whether a FROM clause holds a NATURAL JOIN the rewrite left as written (src/duck/rewrite.ts `naturalWritten`) */
1118
+ function natural(n) {
1119
+ if (Array.isArray(n))
1120
+ return n.some(natural);
1121
+ if (kindOf(n) !== "JoinExpr")
1122
+ return false;
1123
+ const b = bodyOf(n);
1124
+ return b.isNatural === true || natural(b.larg) || natural(b.rarg);
1125
+ }
1126
+ /** a select list with each bare reference to a merged column read as `merged` says, or carried where it says null */
1127
+ function mergedOver(n, merged, carried) {
1128
+ if (Array.isArray(n))
1129
+ return n.map((x) => mergedOver(x, merged, carried));
1130
+ if (!isObject(n))
1131
+ return n;
1132
+ if (kindOf(n) === "ColumnRef" && isObject(n.ColumnRef)) {
1133
+ const names = fieldNames(n.ColumnRef), name = names[0];
1134
+ if (names.length !== 1 || typeof name !== "string")
1135
+ return n;
1136
+ const read = merged.get(name);
1137
+ return read === undefined ? n : read === null ? carried(name) : structuredClone(read);
1138
+ }
1139
+ return Object.fromEntries(Object.entries(n).map(([k, v]) => [k, k === "subselect" ? v : mergedOver(v, merged, carried)]));
1140
+ }
1141
+ const aliasOfTable = (t) => isObject(t.alias) ? String(t.alias.aliasname) : String(t.relname);
1142
+ const aliasOfItem = (n) => {
1143
+ const b = bodyOf(n);
1144
+ return kindOf(n) === "RangeVar" ? aliasOfTable(b) : isObject(b.alias) ? String(b.alias.aliasname) : null;
1145
+ };
1146
+ const rowNumber = (alias) => ref(alias, "rowid");
1147
+ const ROW_ID = "__mason_rid";
1148
+ const hasStarTarget = (t) => {
1149
+ const val = kindOf(t) === "ResTarget" ? bodyOf(t).val : null;
1150
+ return kindOf(val) === "ColumnRef" && kindOf(bodyOf(val).fields.at(-1)) === "A_Star";
1151
+ };
1152
+ /** a column's name in a ColumnRef's fields, each field a name; null for a star */
1153
+ const fieldNames = (b) => b.fields.map((f) => kindOf(f) === "String" ? bodyOf(f).sval : null);
1154
+ /** a name no column of a statement has: what a rewrite binds inside an expression (a lambda's parameter) */
1155
+ const ownName = (name) => name.startsWith("__mason_");
1156
+ /** the select list with every `*` written out; the tables it reads by name; null where it reads a whole row, or holds what the list after the limit may not.
1157
+ * With `subLinks` (the items its subqueries read, from Postgres's analysis: `listSubLinkReads`), a subquery may stand in
1158
+ * it, its own select not read here */
1159
+ function writtenOut(targets, tables, tablesAlone = true, subLinks = null) {
1160
+ const aliases = new Set(tables.map((t) => t.alias));
1161
+ const read = new Set();
1162
+ const list = [];
1163
+ const reads = (n) => {
1164
+ if (Array.isArray(n))
1165
+ return n.every(reads);
1166
+ if (!isObject(n))
1167
+ return true;
1168
+ if (kindOf(n) === "ColumnRef" && isObject(n.ColumnRef)) {
1169
+ const names = fieldNames(n.ColumnRef);
1170
+ if (names.length === 1) {
1171
+ // a bare name: of a table it does not say, unless it is a table's own name, a whole row
1172
+ const name = names[0];
1173
+ // (a lambda's parameter, the rewrite's own name, reads no table)
1174
+ if (typeof name === "string" && ownName(name))
1175
+ return true;
1176
+ if (name === null || name === undefined || aliases.has(name))
1177
+ return false;
1178
+ const owners = tablesAlone ? [] : tables.filter((t) => t.columns.includes(name));
1179
+ for (const t of owners.length > 0 ? owners : tables)
1180
+ read.add(t.alias);
1181
+ return true;
1182
+ }
1183
+ const owner = names.at(-2);
1184
+ if (owner === LATE)
1185
+ return true;
1186
+ if (typeof owner !== "string" || !aliases.has(owner))
1187
+ return false;
1188
+ read.add(owner);
1189
+ return true;
1190
+ }
1191
+ if (kindOf(n) === "SubLink")
1192
+ return subLinks !== null && reads(bodyOf(n).testexpr);
1193
+ return Object.values(n).every(reads);
1194
+ };
1195
+ for (const t of targets) {
1196
+ if (kindOf(t) !== "ResTarget")
1197
+ return null;
1198
+ const val = bodyOf(t).val;
1199
+ const fields = kindOf(val) === "ColumnRef" ? bodyOf(val).fields : [];
1200
+ if (kindOf(fields.at(-1)) === "A_Star") {
1201
+ const owner = fields.length >= 2 ? bodyOf(fields.at(-2)).sval : null;
1202
+ for (const table of tables) {
1203
+ if (owner !== null && table.alias !== owner)
1204
+ continue;
1205
+ read.add(table.alias);
1206
+ for (const c of table.columns)
1207
+ list.push({ ResTarget: { name: c, val: ref(table.alias, c) } });
1208
+ }
1209
+ continue;
1210
+ }
1211
+ if (!rowWise(val, subLinks !== null).ok || !reads(val))
1212
+ return null;
1213
+ list.push(t);
1214
+ }
1215
+ for (const alias of subLinks ?? []) {
1216
+ if (!aliases.has(alias))
1217
+ return null;
1218
+ read.add(alias);
1219
+ }
1220
+ return { list, read };
1221
+ }
1222
+ /** how many subquery expressions a tree holds, not counting those inside one */
1223
+ function holdsSubLink(n) {
1224
+ if (Array.isArray(n))
1225
+ return n.reduce((sum, x) => sum + holdsSubLink(x), 0);
1226
+ if (!isObject(n))
1227
+ return 0;
1228
+ return kindOf(n) === "SubLink" ? 1 : holdsSubLink(Object.values(n));
1229
+ }
1230
+ /** how many subquery expressions an analysed tree holds, not counting those inside one */
1231
+ function subLinksIn(v) {
1232
+ if (Array.isArray(v))
1233
+ return v.reduce((sum, x) => sum + subLinksIn(x), 0);
1234
+ if (!isTreeNode(v))
1235
+ return 0;
1236
+ return v._node === "SUBLINK" ? 1 : subLinksIn(Object.entries(v).flatMap(([k, x]) => k === "_node" ? [] : [x]));
1237
+ }
1238
+ /** the name a target is read by from outside its SELECT */
1239
+ function outputName(t) {
1240
+ const b = bodyOf(t);
1241
+ if (typeof b.name === "string")
1242
+ return b.name;
1243
+ if (kindOf(b.val) !== "ColumnRef")
1244
+ return null;
1245
+ const last = bodyOf(b.val).fields.at(-1);
1246
+ return kindOf(last) === "String" ? bodyOf(last).sval : null;
1247
+ }
1248
+ /** a SELECT that is a projection and a WHERE, as the rewrite left it */
1249
+ function projection(b) {
1250
+ if (b.op !== undefined && b.op !== "SETOP_NONE")
1251
+ return false;
1252
+ return ["withClause", "distinctClause", "groupClause", "havingClause", "windowClause", "lockingClause", "intoClause", "valuesLists", "sortClause", "limitCount", "limitOffset"]
1253
+ .every((f) => b[f] === undefined || (Array.isArray(b[f]) && b[f].length === 0));
1254
+ }
1255
+ function withsOf(s) {
1256
+ const ctes = isObject(s.withClause) && Array.isArray(s.withClause.ctes) ? s.withClause.ctes : [];
1257
+ return { byName: new Map(ctes.map((c) => [String(bodyOf(c).ctename), bodyOf(c)])), used: new Set() };
1258
+ }
1259
+ /** a read of a WITH query, as the derived table it may be: the only reference to it, so it stands in the query's place */
1260
+ function inlined(item, relation, withs) {
1261
+ if (kindOf(item) !== "RangeVar" || relation?.kind !== "derived" || relation.cte === null)
1262
+ return item;
1263
+ const cte = withs.byName.get(relation.cte), b = bodyOf(item);
1264
+ if (cte === undefined || b.schemaname !== undefined || b.relname !== relation.cte || kindOf(cte.ctequery) !== "SelectStmt")
1265
+ return item;
1266
+ withs.used.add(relation.cte);
1267
+ const named = isObject(b.alias) ? b.alias : { aliasname: relation.cte };
1268
+ const columns = named.colnames ?? cte.aliascolnames;
1269
+ return { RangeSubselect: { subquery: structuredClone(cte.ctequery), alias: { ...named, ...(columns !== undefined ? { colnames: columns } : {}) } } };
1270
+ }
1271
+ /** the conjunction of two WHEREs, either of which may be none, flat as the parser reads it back */
1272
+ function both(a, b) {
1273
+ if (a === undefined)
1274
+ return b;
1275
+ if (b === undefined)
1276
+ return a;
1277
+ const args = (n) => kindOf(n) === "BoolExpr" && bodyOf(n).boolop === "AND_EXPR" ? bodyOf(n).args : [n];
1278
+ return { BoolExpr: { boolop: "AND_EXPR", args: [...args(a), ...args(b)] } };
1279
+ }
1280
+ /** one SELECT of one FROM item, a projection and a WHERE over FROM items, folded into it (see above): each column of the
1281
+ * wrapper read as the expression it is; null where it cannot be */
1282
+ function fold(s, sub, item) {
1283
+ const inner = item.read;
1284
+ if (inner === null || !projection(sub))
1285
+ return null;
1286
+ const items = inner.items.filter((r) => r !== null);
1287
+ if (items.length !== inner.items.length)
1288
+ return null;
1289
+ const expanded = writtenOut(sub.targetList, items);
1290
+ if (expanded === null || expanded.list.length !== item.columns.length)
1291
+ return null;
1292
+ const values = expanded.list.map((t) => bodyOf(t).val);
1293
+ const byName = new Map();
1294
+ item.columns.forEach((c, i) => byName.set(c, byName.has(c) ? null : values[i]));
1295
+ let whole = true;
1296
+ // the item's column a reference reads, by the name the wrapper reads it by
1297
+ const columnOf = (n) => {
1298
+ const names = fieldNames(n.ColumnRef);
1299
+ const column = names.length === 2 && names[0] === item.alias ? names[1] : names.length === 1 ? names[0] : null;
1300
+ return typeof column === "string" ? column : null;
1301
+ };
1302
+ // the item's columns the ANALYSIS made a Const (src/duck/constant-key.ts), by a name it has once: a key reading one
1303
+ // orders nothing
1304
+ const constant = new Set(item.columns.filter((c, i) => item.constants[i] === true && item.columns.indexOf(c) === item.columns.lastIndexOf(c)));
1305
+ const readsConstant = (n) => {
1306
+ if (!isObject(n) || kindOf(n) !== "ColumnRef" || !isObject(n.ColumnRef))
1307
+ return false;
1308
+ const column = columnOf(n);
1309
+ return column !== null && !ownName(column) && constant.has(column);
1310
+ };
1311
+ const over = (n) => {
1312
+ if (Array.isArray(n))
1313
+ return n.map(over);
1314
+ if (!isObject(n))
1315
+ return n;
1316
+ if (kindOf(n) === "ColumnRef" && isObject(n.ColumnRef)) {
1317
+ const column = columnOf(n);
1318
+ if (column !== null && ownName(column))
1319
+ return n;
1320
+ const value = typeof column === "string" ? byName.get(column) : undefined;
1321
+ if (value === undefined || value === null) {
1322
+ whole = false;
1323
+ return n;
1324
+ }
1325
+ return structuredClone(value);
1326
+ }
1327
+ return Object.fromEntries(Object.entries(n).map(([k, v]) => [k, over(v)]));
1328
+ };
1329
+ const targets = [];
1330
+ // which of them is a constant: a column of the item the analysis made a Const, read as it is
1331
+ const constantOutputs = [];
1332
+ for (const t of s.targetList) {
1333
+ if (kindOf(t) !== "ResTarget")
1334
+ return null;
1335
+ if (hasStarTarget(t)) {
1336
+ const fields = bodyOf(bodyOf(t).val).fields;
1337
+ if (fields.length > 2 || (fields.length === 2 && bodyOf(fields[0]).sval !== item.alias))
1338
+ return null;
1339
+ item.columns.forEach((c, i) => { targets.push({ ResTarget: { name: c, val: structuredClone(values[i]) } }); constantOutputs.push(constant.has(c)); });
1340
+ continue;
1341
+ }
1342
+ const name = outputName(t);
1343
+ constantOutputs.push(readsConstant(bodyOf(t).val));
1344
+ targets.push({ ResTarget: { ...bodyOf(t), ...(name !== null ? { name } : {}), val: over(bodyOf(t).val) } });
1345
+ }
1346
+ // an output's number or bare name is read by `sortKeys` from the list; any other key is over the wrapper's columns
1347
+ const outputs = new Set(targets.map(outputName));
1348
+ // A CONSTANT SORT KEY (the class, issue 134): a key reading the derived table's column the analysis made a Const
1349
+ // (`s.one`, the subquery's `1 AS one`), by itself or by the wrapper's output's number or name, orders nothing, and
1350
+ // folded it would be the literal, an ordinal to the engine: it is no key. Any other key folded is written so the
1351
+ // engine evaluates it (`evaluated`), never as a bare literal
1352
+ const sorts = (s.sortClause ?? []).flatMap((sort) => {
1353
+ const node = bodyOf(sort).node;
1354
+ const bare = kindOf(node) === "ColumnRef" && bodyOf(node).fields.length === 1 ? fieldNames(bodyOf(node))[0] : null;
1355
+ const named = bare !== null && bare !== undefined && outputs.has(bare);
1356
+ const at = kindOf(node) === "A_Const" && isObject(bodyOf(node).ival) ? Number(bodyOf(node).ival.ival ?? 0) - 1
1357
+ : named ? targets.findIndex((t) => outputName(t) === bare) : -1;
1358
+ if (at >= 0 && constantOutputs[at] === true)
1359
+ return [];
1360
+ if (kindOf(node) === "A_Const" || named)
1361
+ return [sort];
1362
+ if (readsConstant(node))
1363
+ return [];
1364
+ return [{ SortBy: { ...bodyOf(sort), node: evaluated(over(node)) } }];
1365
+ });
1366
+ // a DISTINCT ON's expressions alike: an output's number or bare name is read from the list, any other over the
1367
+ // wrapper's columns. One reading a constant column groups nothing, and is left as it was
1368
+ const on = distinctOnOf(s);
1369
+ const distinct = [];
1370
+ for (const d of on ?? []) {
1371
+ const bare = kindOf(d) === "ColumnRef" && bodyOf(d).fields.length === 1 ? fieldNames(bodyOf(d))[0] : null;
1372
+ if (kindOf(d) === "A_Const" || (bare !== null && bare !== undefined && outputs.has(bare))) {
1373
+ distinct.push(d);
1374
+ continue;
1375
+ }
1376
+ if (readsConstant(d))
1377
+ return null;
1378
+ distinct.push(evaluated(over(d)));
1379
+ }
1380
+ const where = over(s.whereClause);
1381
+ if (!whole)
1382
+ return null;
1383
+ const { whereClause: _w, sortClause: _s, ...rest } = s;
1384
+ const joined = both(sub.whereClause, where);
1385
+ return { ...rest, ...(on !== null ? { distinctClause: distinct } : {}), targetList: targets, fromClause: sub.fromClause, ...(joined !== undefined ? { whereClause: joined } : {}), ...(sorts.length > 0 ? { sortClause: sorts } : {}) };
1386
+ }
1387
+ /** a SELECT's FROM read as its query's items: its WITH queries in place of their one read and, while its one item is a
1388
+ * derived read, folded into it; null where the tree and the analysis do not agree or a wrapper does not fold */
1389
+ function unwrapped(s, query, withs) {
1390
+ for (let body = s, q = query;;) {
1391
+ const items = fromItems(body.fromClause);
1392
+ if (items === null || items.length !== q.items.length || items.some((n, i) => aliasOfItem(n) !== q.items[i]?.alias))
1393
+ return null;
1394
+ const placed = { ...body, fromClause: mapFrom(body.fromClause, (n, i) => inlined(n, q.items[i], withs)) };
1395
+ const one = items.length === 1 ? q.items[0] : null;
1396
+ const item = fromItems(placed.fromClause)[0];
1397
+ if (one?.kind !== "derived" || one.read === null || kindOf(item) !== "RangeSubselect")
1398
+ return { s: placed, query: q };
1399
+ const folded = fold(placed, bodyOf(bodyOf(item).subquery), one);
1400
+ if (folded === null)
1401
+ return null;
1402
+ body = folded;
1403
+ q = one.read;
1404
+ }
1405
+ }
1406
+ /** the select behind a walled read's barrier, and the table it reads: null where the tree is not the walled form */
1407
+ function barrierOf(item, wall) {
1408
+ if (wall === null || kindOf(item) !== "RangeSubselect")
1409
+ return null;
1410
+ const outer = bodyOf(item).subquery;
1411
+ if (kindOf(outer) !== "SelectStmt")
1412
+ return null;
1413
+ const o = bodyOf(outer), from = o.fromClause, list = o.targetList;
1414
+ if (from?.length !== 1 || kindOf(from[0]) !== "RangeSubselect" || !isObject(bodyOf(from[0]).alias) || bodyOf(from[0]).alias.aliasname !== wall)
1415
+ return null;
1416
+ if (list?.length !== 1 || !hasStarTarget(list[0]) || bodyOf(bodyOf(list[0]).val).fields.length !== 1 || o.whereClause !== undefined)
1417
+ return null;
1418
+ const inner = bodyOf(from[0]).subquery;
1419
+ if (kindOf(inner) !== "SelectStmt")
1420
+ return null;
1421
+ const b = bodyOf(inner), tables = b.fromClause, own = b.targetList;
1422
+ if (tables?.length !== 1 || kindOf(tables[0]) !== "RangeVar" || b.limitCount === undefined || own?.length !== 1 || !hasStarTarget(own[0]))
1423
+ return null;
1424
+ return { barrier: b, table: bodyOf(tables[0]) };
1425
+ }
1426
+ /** a walled read exposing its row number as `__mason_rid`: chosen behind its barrier, passed on by its wall's `*` */
1427
+ function walledExposed(item, wall) {
1428
+ const copy = structuredClone(item);
1429
+ const { barrier, table } = barrierOf(copy, wall);
1430
+ barrier.targetList = [...barrier.targetList, { ResTarget: { name: ROW_ID, val: rowNumber(aliasOfTable(table)) } }];
1431
+ barrier.fromClause = [ownTable(table)];
1432
+ return copy;
1433
+ }
1434
+ /** a derived item that is a plain read of one table or one walled read in the tree too (the rewrite may have written it
1435
+ * otherwise), or null */
1436
+ function plainSide(item, relation, withs, wall) {
1437
+ if (relation.read === null || kindOf(item) !== "RangeSubselect")
1438
+ return null;
1439
+ const sub = bodyOf(item).subquery;
1440
+ if (kindOf(sub) !== "SelectStmt" || bodyOf(item).lateral === true)
1441
+ return null;
1442
+ // a WITH query taken only if this read is: one that stays carried reads what it always read
1443
+ const trial = { byName: withs.byName, used: new Set() };
1444
+ const opened = unwrapped(bodyOf(sub), relation.read, trial);
1445
+ if (opened === null || !projection(opened.s))
1446
+ return null;
1447
+ const [table] = opened.query.items, from = opened.s.fromClause;
1448
+ if (opened.query.items.length !== 1 || from.length !== 1 || table === null || table === undefined)
1449
+ return null;
1450
+ const walled = table.kind === "walled" && barrierOf(from[0], wall) !== null && aliasOfItem(from[0]) === table.alias;
1451
+ if (!walled && (table.kind !== "table" || kindOf(from[0]) !== "RangeVar"))
1452
+ return null;
1453
+ if (table.columns.some((c) => c === "" || c.toLowerCase() === "rowid"))
1454
+ return null;
1455
+ const written = writtenOut(opened.s.targetList, [table]);
1456
+ // a whole row in the WHERE lists what a read of the table itself shows and its view of visible columns does not
1457
+ if (written === null || !writtenOut([{ ResTarget: { val: opened.s.whereClause ?? { A_Const: { isnull: true } } } }], [table]))
1458
+ return null;
1459
+ for (const name of trial.used)
1460
+ withs.used.add(name);
1461
+ return { body: opened.s, source: from[0], id: walled ? ref(table.alias, ROW_ID) : rowNumber(aliasOfTable(bodyOf(from[0]))), walled, list: written.list };
1462
+ }
1463
+ /** a table read itself: a row number is no column of a copy's view, and nothing here lists a column that is not named */
1464
+ function ownTable(t) { const { catalogname: _c, ...rest } = t; return { RangeVar: structuredClone(rest) }; }
1465
+ /** a plain side as a FROM item: the query it was, each row exposing its row number; a walled read it reads keeps its
1466
+ * filter, its barrier and its wall, whether or not the side keeps its own WHERE */
1467
+ function exposed(side, alias, where, wall) {
1468
+ const { whereClause, ...rest } = side.body;
1469
+ return { RangeSubselect: { alias: structuredClone(alias), subquery: { SelectStmt: structuredClone({ ...rest,
1470
+ targetList: [...side.list, { ResTarget: { name: ROW_ID, val: side.id } }],
1471
+ fromClause: [side.walled ? walledExposed(side.source, wall) : ownTable(bodyOf(side.source))],
1472
+ ...(where && whereClause !== undefined ? { whereClause } : {}) }) } } };
1473
+ }
1474
+ /** the same tree with every column of a carried item replaced by `__mason_late.__mason_c<i>`, which `columns` names */
1475
+ function overCarried(n, carried, columns) {
1476
+ if (Array.isArray(n))
1477
+ return n.map((x) => overCarried(x, carried, columns));
1478
+ if (!isObject(n))
1479
+ return n;
1480
+ if (kindOf(n) === "ColumnRef" && isObject(n.ColumnRef)) {
1481
+ const names = fieldNames(n.ColumnRef);
1482
+ const column = names.at(-1);
1483
+ // a lambda's parameter (the rewrite's own name) is no column of any item
1484
+ if (names.length === 1 && typeof column === "string" && ownName(column))
1485
+ return n;
1486
+ const owner = names.length === 1 ? carried.find((c) => c.columns.includes(String(column))) : carried.find((c) => c.alias === names.at(-2));
1487
+ if (owner === undefined || typeof column !== "string")
1488
+ return n;
1489
+ const key = `${owner.alias}.${column}`;
1490
+ let at = columns.get(key);
1491
+ if (at === undefined) {
1492
+ at = { name: `__mason_c${columns.size + 1}`, val: ref(owner.alias, column) };
1493
+ columns.set(key, at);
1494
+ }
1495
+ return ref(LATE, at.name);
1496
+ }
1497
+ return Object.fromEntries(Object.entries(n).map(([k, v]) => [k, k === "subselect" ? v : overCarried(v, carried, columns)]));
1498
+ }
1499
+ /** one SELECT over joined FROM items written with its rows chosen by row number (see above), or null where it is not so shaped */
1500
+ function joinRows(select, relations, wall) {
1501
+ const written0 = bodyOf(select);
1502
+ // a DISTINCT ON keeps one whole row of its FROM per group: that row's numbers fetch it, whatever the ON expressions
1503
+ const on0 = distinctOnOf(written0);
1504
+ const shape0 = limited(written0, true, on0 !== null);
1505
+ if (shape0 === null)
1506
+ return null;
1507
+ // no sort and in fact no limit: every row comes back, and reading each one again saves nothing. Nor under a DISTINCT
1508
+ // ON, whose every group comes back
1509
+ if ((shape0.sorts.length === 0 || on0 !== null) && noLimit(written0.limitCount))
1510
+ return null;
1511
+ const items0 = fromItems(written0.fromClause);
1512
+ if (items0 === null)
1513
+ return null;
1514
+ const aliases0 = items0.map(aliasOfItem);
1515
+ // the query of the analysis this SELECT is: its FROM items, in order. Two queries alike must agree, or which is unsure
1516
+ const named = relations.filter((q) => q.items.length === aliases0.length && q.items.every((t, i) => t?.alias === aliases0[i]));
1517
+ const first = named[0];
1518
+ if (first === undefined || named.some((q) => JSON.stringify([q.items, q.merged, q.joins]) !== JSON.stringify([first.items, first.merged, first.joins]) || !q.rowWise))
1519
+ return null;
1520
+ // the analysis's DISTINCT ON is the tree's, and none of its expressions is volatile: computed inside, once per row
1521
+ if (named.some((q) => q.distinctOn !== (on0 !== null) || q.volatileOn))
1522
+ return null;
1523
+ const withs = withsOf(written0);
1524
+ const opened = unwrapped(written0, first, withs);
1525
+ if (opened === null)
1526
+ return null;
1527
+ const { s, query } = opened;
1528
+ const on = distinctOnOf(s);
1529
+ if ((on === null) !== (on0 === null))
1530
+ return null;
1531
+ const items = fromItems(s.fromClause);
1532
+ // a subquery in the list (issue 134, step 5): run for the chosen rows alone, a table's own too
1533
+ const subLinks = [...new Set(named.flatMap((q) => q.listReads))].sort();
1534
+ const listed0 = s.targetList ?? [];
1535
+ const subqueried = holdsSubLink(listed0) > 0;
1536
+ // a wrapper folded away is an item its subqueries may read, by names inside them a fold does not know
1537
+ if (subqueried && query !== first)
1538
+ return null;
1539
+ // each the analysis's own, never one a rewrite wrote: what it reads of the FROM items is what the analysis says
1540
+ if (subqueried && named.some((q) => q.listSubLinks !== holdsSubLink(listed0)))
1541
+ return null;
1542
+ if (items.length < (subqueried || on !== null ? 1 : 2))
1543
+ return null;
1544
+ const aliases = items.map(aliasOfItem);
1545
+ if (new Set(aliases).size !== aliases.length)
1546
+ return null;
1547
+ const relation = query.items;
1548
+ if (relation.some((r) => r === null))
1549
+ return null;
1550
+ const listed = relation;
1551
+ // DuckDB binds a name without regard to case: "RowId" hides the row number as rowid does
1552
+ if (listed.some((t) => t.kind !== "derived" && t.columns.some((c) => c === "" || c.toLowerCase() === "rowid")))
1553
+ return null;
1554
+ // a walled read is the walled form in the tree too, or nothing is written
1555
+ if (listed.some((t, i) => t.kind === "walled" && barrierOf(items[i], wall) === null))
1556
+ return null;
1557
+ const sides = items.map((n, i) => { const r = listed[i]; return r.kind === "derived" ? plainSide(n, r, withs, wall) : null; });
1558
+ // what a list reads of a derived item is read by name: two of one name (in any case) cannot be told apart
1559
+ const carried = listed.filter((r, i) => r.kind === "derived" && sides[i] === null);
1560
+ if (listed.some((r) => r.kind === "derived" && new Set(r.columns.map((c) => c.toLowerCase())).size !== r.columns.length))
1561
+ return null;
1562
+ const tablesAlone = listed.every((r) => r.kind === "table");
1563
+ // a subquery reads a carried item's columns by the names inside it, which the fetch has not
1564
+ if (subLinks.some((a) => { const i = aliases.indexOf(a); return i < 0 || (listed[i].kind === "derived" && sides[i] === null); }))
1565
+ return null;
1566
+ const outputs = writtenOut(s.targetList, listed, tablesAlone, subqueried ? subLinks : null);
1567
+ if (outputs === null)
1568
+ return null;
1569
+ const writtenSorts = s.sortClause ?? [];
1570
+ // the keys are read from the outputs as the join reads them: a bare merged column is the join's, inside
1571
+ const shown = new Map();
1572
+ const sorted = sortKeys(outputs.list, writtenSorts, shown);
1573
+ if (sorted === null)
1574
+ return null;
1575
+ const { keys, sorts } = sorted;
1576
+ const distinct = on === null ? null : distinctKeys(outputs.list, on);
1577
+ if (on !== null && distinct === null)
1578
+ return null;
1579
+ // ONE VALUE: a volatile output a sort reads is read from its key, and a key a written form cannot show stays as it was
1580
+ if (named.some((q) => q.volatileOutputs.some((o) => !shown?.has(o)) || (q.volatileKeys && q.sorts !== writtenSorts.length)))
1581
+ return null;
1582
+ const volatileKeys = named.some((q) => q.volatileKeys);
1583
+ if (query.joins.length !== joinCount(s.fromClause) || natural(s.fromClause))
1584
+ return null;
1585
+ const columns = new Map();
1586
+ const carriedMerge = (name) => {
1587
+ const key = `.${name}`;
1588
+ let at = columns.get(key);
1589
+ if (at === undefined) {
1590
+ at = { name: `__mason_c${columns.size + 1}`, val: ref(name) };
1591
+ columns.set(key, at);
1592
+ }
1593
+ return ref(LATE, at.name);
1594
+ };
1595
+ const merged = new Map(query.merged);
1596
+ const written = writtenOut(outputs.list.map((t, i) => {
1597
+ const at = shown?.get(i);
1598
+ return { ResTarget: { ...bodyOf(t), val: at !== undefined ? ref(LATE, `__mason_o${at + 1}`) : mergedOver(bodyOf(t).val, merged, carriedMerge) } };
1599
+ }), listed, tablesAlone, subqueried ? subLinks : null);
1600
+ if (written === null)
1601
+ return null;
1602
+ const isFetched = (i) => written.read.has(aliases[i]) && (listed[i].kind !== "derived" || sides[i] !== null);
1603
+ const fetched = items.flatMap((n, i) => isFetched(i) ? [{ node: n, at: i, alias: aliases[i], id: listed[i].kind === "table" ? rowNumber(aliases[i]) : ref(aliases[i], ROW_ID) }] : []);
1604
+ if (fetched.length === 0)
1605
+ return null;
1606
+ const list = written.list.map((t) => ({ ResTarget: { ...bodyOf(t), val: overCarried(bodyOf(t).val, carried, columns) } }));
1607
+ const from = mapFrom(s.fromClause, (n, i) => {
1608
+ const side = sides[i];
1609
+ if (side !== null && side !== undefined)
1610
+ return isFetched(i) ? exposed(side, bodyOf(n).alias, true, wall) : { RangeSubselect: { ...bodyOf(n), subquery: { SelectStmt: side.body } } };
1611
+ if (listed[i].kind === "walled")
1612
+ return isFetched(i) ? walledExposed(n, wall) : n;
1613
+ return kindOf(n) === "RangeVar" && listed[i].kind === "table" ? ownTable(bodyOf(n)) : n;
1614
+ });
1615
+ // no sort of its own: the row numbers are the order where the join is proved bounded, so a page of the statement is
1616
+ // one page of one list; otherwise none, or a join that multiplies is run whole. Two queries alike must both be proved.
1617
+ // What is carried has no row number, so with one (a keyed WITH query read twice) they would be no total order
1618
+ const pagedInOrder = sorts.length === 0 && carried.length === 0 && (query === first ? named.every((q) => q.bounded) : query.bounded);
1619
+ const order = (node) => ({ SortBy: { node, sortby_dir: "SORTBY_DEFAULT", sortby_nulls: "SORTBY_NULLS_DEFAULT" } });
1620
+ // (the written sort is never the inner's: the outer sorts the rows the inner fetched)
1621
+ const { withClause: _with, sortClause: _sorts, ...body } = s;
1622
+ const left = isObject(s.withClause) ? s.withClause.ctes.filter((c) => !withs.used.has(String(bodyOf(c).ctename))) : [];
1623
+ const inner = { ...body, ...(distinct !== null ? { distinctClause: distinct } : {}), ...(left.length > 0 ? { withClause: { ...s.withClause, ctes: left } } : {}), fromClause: from, targetList: [
1624
+ ...fetched.map((t, i) => ({ ResTarget: { name: `__mason_r${i + 1}`, val: t.id } })),
1625
+ ...[...columns.values()].map((c) => ({ ResTarget: { name: c.name, val: c.val } })),
1626
+ ...keys.map((k, i) => ({ ResTarget: { name: `__mason_o${i + 1}`, val: k } })),
1627
+ ],
1628
+ // (a volatile key is sorted by the output that holds it: said twice, it would be two values)
1629
+ ...(sorts.length > 0 ? { sortClause: sorts.map((sort, i) => ({ SortBy: { ...bodyOf(sort), node: volatileKeys ? ref(`__mason_o${i + 1}`) : keys[i] } })) } : pagedInOrder ? { sortClause: fetched.map((t) => order(structuredClone(t.id))) } : {}) };
1630
+ let joined = { RangeSubselect: { subquery: { SelectStmt: inner }, alias: { aliasname: LATE } } };
1631
+ // the fetch reads a walled read through itself again, filter, barrier and wall: one more wall each
1632
+ let walls = 0;
1633
+ fetched.forEach((t, i) => {
1634
+ const side = sides[t.at];
1635
+ const walled = listed[t.at].kind === "walled";
1636
+ if (walled || side?.walled === true)
1637
+ walls++;
1638
+ const rarg = side !== null && side !== undefined ? exposed(side, bodyOf(t.node).alias, false, wall) : walled ? walledExposed(t.node, wall) : ownTable(bodyOf(t.node));
1639
+ joined = { JoinExpr: { jointype: "JOIN_LEFT", larg: joined, rarg, quals: { A_Expr: {
1640
+ kind: "AEXPR_OP", name: [S("=")], lexpr: structuredClone(t.id), rexpr: ref(LATE, `__mason_r${i + 1}`)
1641
+ } } } };
1642
+ });
1643
+ return { walls, select: { SelectStmt: {
1644
+ targetList: list,
1645
+ fromClause: [joined],
1646
+ ...(sorts.length > 0 ? { sortClause: sorts.map((sort, i) => ({ SortBy: { ...bodyOf(sort), node: ref(LATE, `__mason_o${i + 1}`) } })) }
1647
+ : pagedInOrder ? { sortClause: fetched.map((_t, i) => order(ref(LATE, `__mason_r${i + 1}`))) } : {}),
1648
+ limitOption: "LIMIT_OPTION_DEFAULT", op: "SETOP_NONE",
1649
+ } } };
1650
+ }