@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.
- package/README.md +13 -4
- package/dist/src/api/postgres/catalog-worker.js +49 -2
- package/dist/src/api/postgres/catalog.js +166 -32
- package/dist/src/api/postgres/listener.js +11 -3
- package/dist/src/api/postgres/proxy.js +283 -0
- package/dist/src/api/postgres/schema.js +6 -0
- package/dist/src/api/postgres/server.js +505 -99
- package/dist/src/api/postgres/translate.js +47 -2
- package/dist/src/cli/background.js +18 -19
- package/dist/src/cli/bridge-stdin.js +101 -0
- package/dist/src/cli/command.js +169 -11
- package/dist/src/cli/commands.js +207 -51
- package/dist/src/cli/credentials.js +182 -0
- package/dist/src/cli/demo.js +2 -2
- package/dist/src/cli/deploy.js +574 -0
- package/dist/src/cli/dev.js +148 -27
- package/dist/src/cli/doctor.js +116 -33
- package/dist/src/cli/generated-files.js +143 -0
- package/dist/src/cli/guides.js +97 -6
- package/dist/src/cli/hosted.js +850 -0
- package/dist/src/cli/init.js +47 -11
- package/dist/src/cli/login.js +289 -0
- package/dist/src/cli/machine.js +1 -1
- package/dist/src/cli/main.js +8 -2
- package/dist/src/cli/mason.cjs +17 -0
- package/dist/src/cli/mcp.js +2 -2
- package/dist/src/cli/output-gate.js +176 -0
- package/dist/src/cli/page-share.js +96 -0
- package/dist/src/cli/pg-tls.js +1 -1
- package/dist/src/cli/pin.js +6 -2
- package/dist/src/cli/pool-size.cjs +85 -0
- package/dist/src/cli/ports.js +2 -0
- package/dist/src/cli/preflight.js +130 -39
- package/dist/src/cli/project.js +99 -0
- package/dist/src/cli/readiness.js +42 -6
- package/dist/src/cli/registry.js +12 -4
- package/dist/src/cli/replica-hosted.js +665 -0
- package/dist/src/cli/replica.js +76 -0
- package/dist/src/cli/roles.js +2 -1
- package/dist/src/cli/run-server.js +154 -7
- package/dist/src/cli/security.js +57 -10
- package/dist/src/cli/serve.js +63 -7
- package/dist/src/cli/share-run.js +51 -8
- package/dist/src/cli/share.js +3 -3
- package/dist/src/cli/source-edit.js +10 -4
- package/dist/src/cli/source-role.js +344 -0
- package/dist/src/cli/sources.js +30 -20
- package/dist/src/cli/style.js +2 -1
- package/dist/src/cli/supervise.js +15 -23
- package/dist/src/cli/token-hosted.js +337 -0
- package/dist/src/cli/ui-ssh.js +460 -0
- package/dist/src/cli/ui.js +115 -11
- package/dist/src/cli/user-hosted.js +299 -0
- package/dist/src/cli/user.js +126 -74
- package/dist/src/demo/seed.js +26 -0
- package/dist/src/duck/analyze.js +36 -20
- package/dist/src/duck/collation.js +266 -0
- package/dist/src/duck/constant-key.js +94 -0
- package/dist/src/duck/distinct-key.js +283 -26
- package/dist/src/duck/events.js +39 -8
- package/dist/src/duck/executor-worker.js +2 -2
- package/dist/src/duck/executor.js +6 -5
- package/dist/src/duck/explain.js +4 -0
- package/dist/src/duck/functions.js +448 -49
- package/dist/src/duck/gaps.js +34 -16
- package/dist/src/duck/generated-plan.js +1070 -0
- package/dist/src/duck/generated.js +906 -0
- package/dist/src/duck/grants.js +1 -1
- package/dist/src/duck/held-types.js +64 -0
- package/dist/src/duck/history.js +4 -0
- package/dist/src/duck/jsonb-order.js +9 -12
- package/dist/src/duck/kept-route.js +6 -5
- package/dist/src/duck/kept.js +9 -7
- package/dist/src/duck/late.js +1469 -40
- package/dist/src/duck/libc-bound.js +341 -0
- package/dist/src/duck/libc-order.js +892 -126
- package/dist/src/duck/pg-eval-worker.js +7 -1
- package/dist/src/duck/pg-eval.js +6 -4
- package/dist/src/duck/pgtree.js +774 -23
- package/dist/src/duck/plan.js +275 -30
- package/dist/src/duck/publication.js +14 -0
- package/dist/src/duck/requests.js +9 -5
- package/dist/src/duck/resolved.js +117 -2
- package/dist/src/duck/rewrite.js +4567 -583
- package/dist/src/duck/row-filter.js +12 -6
- package/dist/src/duck/statement.js +19 -0
- package/dist/src/duck/store.js +294 -68
- package/dist/src/duck/stream.js +9 -5
- package/dist/src/duck/sync.js +602 -126
- package/dist/src/duck/token.js +4 -1
- package/dist/src/duck/types.js +63 -7
- package/dist/src/duck/usage.js +6 -4
- package/dist/src/duck/users.js +5 -0
- package/dist/src/duck/verified.js +159 -2
- package/dist/src/duck/views.js +4 -3
- package/dist/src/lib/database-url.js +55 -10
- package/dist/src/lib/instant.js +50 -0
- package/dist/src/lib/log-buffer.js +13 -4
- package/dist/src/lib/log-read.js +196 -0
- package/dist/src/lib/pg-numeric.js +387 -49
- package/dist/src/lib/secret-mask.js +142 -0
- package/dist/src/lib/table-sync.js +24 -9
- package/dist/src/web/app.js +9 -4
- package/dist/src/web/pages/status.js +178 -13
- package/dist/src/web/pages/syncs.js +1 -1
- package/dist/src/web/pages/tables.js +8 -3
- package/dist/src/web/pages/users.js +4 -3
- package/dist/src/web/shared.js +19 -3
- package/package.json +8 -7
package/dist/src/duck/late.js
CHANGED
|
@@ -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
|
-
/**
|
|
43
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
136
|
-
return changed ?
|
|
442
|
+
const result = visit(tree);
|
|
443
|
+
return changed ? result : null;
|
|
137
444
|
}
|
|
138
|
-
/**
|
|
139
|
-
function
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
if (targets === undefined || sorts === undefined || sorts.length === 0)
|
|
460
|
+
if (targets === undefined)
|
|
152
461
|
return null;
|
|
153
|
-
|
|
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
|
-
|
|
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
|
|
158
|
-
|
|
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
|
-
|
|
668
|
+
grouping.push(structuredClone(named1?.[0] ?? g));
|
|
161
669
|
}
|
|
162
|
-
|
|
670
|
+
const shown = new Map();
|
|
671
|
+
const sorted = sortKeys(targets, s.sortClause ?? [], shown);
|
|
672
|
+
if (sorted === null)
|
|
163
673
|
return null;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
+
}
|