@mason-data/cli 0.6.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +147 -69
  2. package/dist/src/api/error.js +3 -0
  3. package/dist/src/api/postgres/catalog.js +258 -14
  4. package/dist/src/api/postgres/encode.js +3 -1
  5. package/dist/src/api/postgres/explain.js +3 -2
  6. package/dist/src/api/postgres/protocol.js +39 -23
  7. package/dist/src/api/postgres/server.js +1290 -223
  8. package/dist/src/api/postgres/translate.js +81 -9
  9. package/dist/src/api/request-log.js +18 -5
  10. package/dist/src/cdc/pg-wire.js +144 -38
  11. package/dist/src/cdc/poll.js +5 -5
  12. package/dist/src/cli/background.js +55 -8
  13. package/dist/src/cli/cancel.js +75 -0
  14. package/dist/src/cli/command.js +5 -3
  15. package/dist/src/cli/commands.js +843 -272
  16. package/dist/src/cli/control.js +59 -0
  17. package/dist/src/cli/demo.js +158 -0
  18. package/dist/src/cli/dev.js +458 -125
  19. package/dist/src/cli/doctor.js +25 -12
  20. package/dist/src/cli/guides.js +103 -31
  21. package/dist/src/cli/init.js +159 -58
  22. package/dist/src/cli/machine.js +18 -12
  23. package/dist/src/cli/main.js +5 -5
  24. package/dist/src/cli/mcp.js +735 -0
  25. package/dist/src/cli/metrics.js +79 -0
  26. package/dist/src/cli/pg-tls.js +13 -3
  27. package/dist/src/cli/pg-url.js +4 -0
  28. package/dist/src/cli/pin.js +13 -12
  29. package/dist/src/cli/preflight.js +24 -14
  30. package/dist/src/cli/project.js +48 -7
  31. package/dist/src/cli/readiness.js +38 -4
  32. package/dist/src/cli/registry.js +57 -35
  33. package/dist/src/cli/replication-checks.js +7 -5
  34. package/dist/src/cli/replication-mode.js +105 -26
  35. package/dist/src/cli/request-filter.js +134 -0
  36. package/dist/src/cli/roles.js +584 -0
  37. package/dist/src/cli/run-server.js +498 -133
  38. package/dist/src/cli/runs.js +1 -1
  39. package/dist/src/cli/security.js +10 -8
  40. package/dist/src/cli/serve.js +147 -40
  41. package/dist/src/cli/share-run.js +286 -0
  42. package/dist/src/cli/share.js +286 -0
  43. package/dist/src/cli/source-edit.js +168 -0
  44. package/dist/src/cli/sources.js +290 -39
  45. package/dist/src/cli/supervise.js +12 -1
  46. package/dist/src/cli/sync.js +55 -25
  47. package/dist/src/cli/tunnel.js +200 -0
  48. package/dist/src/cli/ui.js +9 -6
  49. package/dist/src/cli/usage.js +128 -0
  50. package/dist/src/cli/user.js +496 -40
  51. package/dist/src/cli/version.js +9 -7
  52. package/dist/src/demo/seed.js +240 -0
  53. package/dist/src/demo/source.js +655 -0
  54. package/dist/src/duck/cancel.js +10 -1
  55. package/dist/src/duck/distinct-key.js +228 -0
  56. package/dist/src/duck/events.js +134 -0
  57. package/dist/src/duck/executor-worker.js +25 -11
  58. package/dist/src/duck/executor.js +107 -53
  59. package/dist/src/duck/explain.js +158 -29
  60. package/dist/src/duck/extensions.js +160 -0
  61. package/dist/src/duck/functions.js +73 -19
  62. package/dist/src/duck/gaps.js +25 -11
  63. package/dist/src/duck/grants.js +176 -0
  64. package/dist/src/duck/history.js +123 -0
  65. package/dist/src/duck/jsonb-order.js +175 -0
  66. package/dist/src/duck/jsonb.js +10 -8
  67. package/dist/src/duck/kept-route.js +82 -2
  68. package/dist/src/duck/kept.js +211 -44
  69. package/dist/src/duck/pgtree.js +40 -0
  70. package/dist/src/duck/plan.js +120 -30
  71. package/dist/src/duck/publication.js +6 -6
  72. package/dist/src/duck/requests.js +265 -93
  73. package/dist/src/duck/rewrite.js +1193 -73
  74. package/dist/src/duck/row-filter.js +649 -0
  75. package/dist/src/duck/store.js +271 -159
  76. package/dist/src/duck/stream.js +10 -5
  77. package/dist/src/duck/sync.js +907 -275
  78. package/dist/src/duck/types.js +6 -5
  79. package/dist/src/duck/usage.js +324 -0
  80. package/dist/src/duck/users.js +64 -15
  81. package/dist/src/duck/verified.js +9 -0
  82. package/dist/src/duck/views.js +2 -3
  83. package/dist/src/lib/database-name.js +62 -0
  84. package/dist/src/lib/dotenv.js +17 -4
  85. package/dist/src/lib/log-buffer.js +68 -0
  86. package/dist/src/lib/pg-numeric.js +5 -2
  87. package/dist/src/lib/project-version.js +23 -23
  88. package/dist/src/lib/table-sync.js +267 -0
  89. package/dist/src/lib/timezone.js +3 -2
  90. package/dist/src/web/app.css +706 -55
  91. package/dist/src/web/app.js +330 -258
  92. package/dist/src/web/charts.js +229 -0
  93. package/dist/src/web/index.html +84 -15
  94. package/dist/src/web/pages/queries.js +192 -0
  95. package/dist/src/web/pages/status.js +446 -0
  96. package/dist/src/web/pages/syncs.js +155 -0
  97. package/dist/src/web/pages/tables.js +434 -0
  98. package/dist/src/web/pages/usage.js +397 -0
  99. package/dist/src/web/pages/users.js +218 -0
  100. package/dist/src/web/shared.js +568 -0
  101. package/dist/src/web/vendor/README.md +25 -0
  102. package/dist/src/web/vendor/geist-1.7.2/Geist-Variable.woff2 +0 -0
  103. package/dist/src/web/vendor/geist-1.7.2/GeistMono-Variable.woff2 +0 -0
  104. package/dist/src/web/vendor/geist-1.7.2/LICENSE.txt +92 -0
  105. package/dist/src/web/vendor/lucide-1.56.0/LICENSE.txt +43 -0
  106. package/dist/src/web/vendor/lucide-1.56.0/chart-column.mjs +29 -0
  107. package/dist/src/web/vendor/lucide-1.56.0/layout-dashboard.mjs +29 -0
  108. package/dist/src/web/vendor/lucide-1.56.0/logs.mjs +34 -0
  109. package/dist/src/web/vendor/lucide-1.56.0/menu.mjs +28 -0
  110. package/dist/src/web/vendor/lucide-1.56.0/square-terminal.mjs +28 -0
  111. package/dist/src/web/vendor/lucide-1.56.0/table.mjs +29 -0
  112. package/dist/src/web/vendor/lucide-1.56.0/users.mjs +29 -0
  113. package/dist/src/web/vendor/lucide-1.56.0/x.mjs +27 -0
  114. package/dist/src/web/vendor/uplot-1.6.32/LICENSE.txt +21 -0
  115. package/dist/src/web/vendor/uplot-1.6.32/uPlot.esm.js +6140 -0
  116. package/dist/src/web/vendor/uplot-1.6.32/uPlot.min.css +1 -0
  117. package/dist/src/web/vendor/zilla-slab-5.3.0/LICENSE.txt +93 -0
  118. package/dist/src/web/vendor/zilla-slab-5.3.0/zilla-slab-latin-700-normal.woff2 +0 -0
  119. package/package.json +10 -3
@@ -1,8 +1,12 @@
1
- import { pathIndex, SESSION_VARIABLE } from "./functions.js";
1
+ import { pathIndex, SESSION_VARIABLE, SETTINGS_VARIABLE } from "./functions.js";
2
2
  import { cellsOf as cellsOfProgram, numericLiteral } from "../lib/pg-numeric.js";
3
- import { caseTwins } from "./pgtree.js";
4
- import { canonicalRead, readKey } from "./kept-route.js";
5
- import { atTimeZoneOf } from "../lib/timezone.js";
3
+ import { caseTwins, joinsUsing } from "./pgtree.js";
4
+ import { canonicalRead, EQUAL_STEPS, KEY_NULL_HEX, KEY_RECOMPUTE_HEX, MARKED_FUNCTIONS, markedError, ORDER_STEPS, readKey } from "./kept-route.js";
5
+ import { JSONB_KEY_FUNCTIONS } from "./jsonb-order.js";
6
+ import { atTimeZoneOf, isUtc, UTC_NAMES } from "../lib/timezone.js";
7
+ /** the DuckDB ICU collation a collation's strings are keyed by (`icu_sort_key`): ICU's own, or the one a libc order's
8
+ * accent and case levels are read with */
9
+ export const icuNameOf = (c) => c.kind === "icu" ? c.name : c.icu;
6
10
  /** Whether pg_database's collation is a libc locale's (not C or POSIX), whose order is read from the database
7
11
  * (src/duck/libc-order.ts) for `duckCollation`'s `libc` */
8
12
  export const isLibcCollation = (provider, collate) => provider !== "b" && provider !== "i" && !/^(C|POSIX)([.].*)?$/i.test(collate);
@@ -22,12 +26,12 @@ export function duckCollation(provider, collate, locale, known) {
22
26
  if (provider === "i") {
23
27
  const name = icuName(locale ?? collate);
24
28
  if (name === null)
25
- throw new Error(`the source database's ICU locale ${JSON.stringify(locale ?? collate)} has no DuckDB collation: Mason cannot sort text as it does`);
29
+ throw new Error(`the source database's ICU locale ${JSON.stringify(locale ?? collate)} has no collation in the engine: Mason cannot sort text as it does`);
26
30
  return { kind: "icu", name };
27
31
  }
28
32
  const name = icuName(collate);
29
33
  if (name === null)
30
- throw new Error(`the source database's collation ${JSON.stringify(collate)} has no DuckDB collation to follow: Mason cannot sort text as it does`);
34
+ throw new Error(`the source database's collation ${JSON.stringify(collate)} has no collation in the engine to follow: Mason cannot sort text as it does`);
31
35
  return { kind: "libc", icu: name };
32
36
  }
33
37
  export class Unrewritable extends Error {
@@ -49,9 +53,16 @@ const NUMBER_TYPES = new Set([INT2, INT4, INT8, FLOAT4, FLOAT8, NUMERIC]);
49
53
  const NUMBER_ELEMENT = { 1005: INT2, 1007: INT4, 1016: INT8, 1021: FLOAT4, 1022: FLOAT8, 1231: NUMERIC };
50
54
  const TEXTS = new Set([TEXT, VARCHAR, BPCHAR, NAME, UNKNOWN]);
51
55
  const JSONS = new Set([JSON_OID, JSONB]);
56
+ const JSONB_ARRAY = 3807;
57
+ /** the way out of a jsonb comparison Mason does not key (gap jsonb-compare) */
58
+ const JSONB_WAY_OUT = "compare a member (j ->> 'k'), or j::text for the canonical text";
52
59
  const BOOL = 16, UUID = 2950, TIME = 1083, BYTEA = 17;
53
60
  /** pg_type's array types of the element types above */
54
61
  const ARRAYS = new Set([1000, 1001, 1005, 1007, 1009, 1014, 1015, 1016, 1021, 1022, 1028, 1115, 1182, 1183, 1185, 1187, 1231, 2951, 199, 3807]);
62
+ /** inet[], which the copy holds as a list of its text (src/duck/sync.ts KEPT_ARRAYS) */
63
+ const INET_ARRAY = 1041;
64
+ /** numeric[] and record[] */
65
+ const NUMERIC_ARRAY = 1231, RECORD_ARRAY = 2287;
55
66
  /** pg_collation's C and POSIX: byte order */
56
67
  const BYTE_COLLATIONS = new Set([950, 951]);
57
68
  /** The DuckDB type a Postgres type is cast to (a typmod's numeric(p, s) included), or null where DuckDB has none. */
@@ -111,6 +122,8 @@ const arrayCast = (arg, element) => ({ TypeCast: { arg, typeName: { names: [S(el
111
122
  const op = (name, l, r) => ({ A_Expr: { kind: "AEXPR_OP", name: [S(name)], ...(l === null ? {} : { lexpr: l }), rexpr: r } });
112
123
  const not = (n) => ({ BoolExpr: { boolop: "NOT_EXPR", args: [n] } });
113
124
  const and = (args) => ({ BoolExpr: { boolop: "AND_EXPR", args } });
125
+ /** the call that raises a marked row's error (src/duck/kept-route.ts `markedError`), over the column read */
126
+ const markedRaise = (name, column, e) => fn(name, [column, strConst(e.code), strConst(e.message)]);
114
127
  const caseWhen = (arms, otherwise) => ({
115
128
  CaseExpr: { args: arms.map(([expr, result]) => ({ CaseWhen: { expr, result } })), ...(otherwise === null ? {} : { defresult: otherwise }) },
116
129
  });
@@ -173,6 +186,24 @@ class Rewriter {
173
186
  answering = new Set();
174
187
  /** the function calls a FROM reads (a set-returning function there is a table, not unnest's) */
175
188
  inFrom = new WeakSet();
189
+ /** the set-returning functions in FROM a LEFT JOIN … ON TRUE reads, written as a list never empty (leftLateral) */
190
+ nonEmpty = new WeakSet();
191
+ /** such a function whose argument a step of its own before the lateral computes: the step's column (leftLateral) */
192
+ stepArg = new WeakMap();
193
+ /** the steps leftLateral wrote, each named for its number in the statement */
194
+ steps = 0;
195
+ /** the joins whose every column a `*` of the SELECT that holds them, or a join's own name, reads (markWholeReads):
196
+ * a step written into one would be a column of the answer (leftLateral) */
197
+ readWhole = new WeakSet();
198
+ /** the SELECTs and joins whose rows' order decides the answer (a LIMIT keeps the first of them, an aggregate or a
199
+ * window reads them in order): a LEFT lateral of a list is streamed there alone (markBounds, leftLateral) */
200
+ bounded = new WeakSet();
201
+ /** the same, were the statement's own rows bounded by a row count on the wire (an Execute's, markBounds): null when
202
+ * they are (the plan made for that Execute) */
203
+ boundedOnWire = null;
204
+ /** a LEFT lateral of a list left DuckDB's join that a row count on the wire would stream (leftLateral): the
205
+ * listener plans the statement again for an Execute that sets one (PlannedSql.unboundedLateral) */
206
+ unboundedLateral = false;
176
207
  /** how the database changes a letter's case */
177
208
  caseMode;
178
209
  /** the ANY / ALL comparisons already guarded against a NULL array (the guard holds the comparison, walked again) */
@@ -190,7 +221,7 @@ class Rewriter {
190
221
  /** every read of a copied table's JSON column by literal steps the statement makes, stored or not: the request log's
191
222
  * `json_reads`, which the keeper's policy reads (src/duck/kept.ts `review`) */
192
223
  jsonReads = new Map();
193
- constructor(analysis, base, collation, userTypes = new Map(), rowTypes = new Map(), timeZone = "UTC", enums = { labels: new Map(), arrays: new Map() }, kept = null) {
224
+ constructor(analysis, base, collation, userTypes = new Map(), rowTypes = new Map(), timeZone = "UTC", enums = { labels: new Map(), arrays: new Map() }, kept = null, keyCounts = new Set()) {
194
225
  this.types = analysis.types;
195
226
  this.base = base;
196
227
  this.collation = collation;
@@ -200,11 +231,83 @@ class Rewriter {
200
231
  this.timeZone = timeZone;
201
232
  this.enums = enums;
202
233
  this.kept = kept;
234
+ for (const at of keyCounts)
235
+ this.keyCounts.add(at + base);
203
236
  this.caseMode = collation.kind === "icu" ? "icu" : collation.kind === "bytes" && collation.ascii === true ? "ascii" : "libc";
204
237
  this.twins = caseTwins(analysis.tree);
205
238
  }
206
239
  /** the subqueries and CTEs whose columns have twins in another case, by the name the query calls them (caseTwins) */
207
240
  twins;
241
+ /**
242
+ * THE count(DISTINCT x) WHOSE DISTINCT CHANGES NOTHING, by the raw node's offset: x a column unique on the source and
243
+ * its table's rows not repeated by the FROM, decided over Postgres's analysis (src/duck/distinct-key.ts), and the
244
+ * same call repeated in its own SELECT's ORDER BY (`repeatedKeyCounts`). Each is counted without its DISTINCT
245
+ * (`funcCall`): DuckDB's hash aggregate of every value, which finds no duplicate, is what that saves.
246
+ */
247
+ keyCounts = new Set();
248
+ /**
249
+ * An ORDER BY (or a window's) that repeats a count(DISTINCT x) of its SELECT list is analysed as THAT target
250
+ * (findTargetlistEntrySQL99): its own offset is in no AGGREF, so the decision never saw it. It is the same call over
251
+ * the same FROM, so it drops its DISTINCT where its twin in the SAME SELECT did — never by a twin in another SELECT,
252
+ * whose `x` may be another table's column.
253
+ */
254
+ repeatedKeyCounts(root) {
255
+ if (this.keyCounts.size === 0)
256
+ return;
257
+ const select = (b) => {
258
+ const own = [];
259
+ const collect = (v) => {
260
+ if (Array.isArray(v)) {
261
+ for (const x of v)
262
+ collect(x);
263
+ return;
264
+ }
265
+ if (v === null || typeof v !== "object")
266
+ return;
267
+ if (isNode(v) && kindOf(v) === "SelectStmt") {
268
+ select(bodyOf(v));
269
+ return;
270
+ }
271
+ if (isNode(v) && kindOf(v) === "FuncCall" && bodyOf(v).agg_distinct === true && lastName(bodyOf(v).funcname) === "count")
272
+ own.push(bodyOf(v));
273
+ for (const [k, x] of Object.entries(isNode(v) ? bodyOf(v) : v)) {
274
+ const arm = armOf(k, x);
275
+ if (arm !== null)
276
+ select(bodyOf(arm));
277
+ else
278
+ collect(x);
279
+ }
280
+ };
281
+ for (const [k, x] of Object.entries(b)) {
282
+ const arm = armOf(k, x);
283
+ if (arm !== null)
284
+ select(bodyOf(arm));
285
+ else
286
+ collect(x);
287
+ }
288
+ const dropped = new Set(own.filter((f) => this.keyCounts.has(f.location)).map((f) => shapeOf({ FuncCall: f })));
289
+ for (const f of own) {
290
+ if (typeof f.location === "number" && !this.keyCounts.has(f.location) && this.at(f, ["AGGREF"]) === null && dropped.has(shapeOf({ FuncCall: f })))
291
+ this.keyCounts.add(f.location);
292
+ }
293
+ };
294
+ const visit = (v) => {
295
+ if (Array.isArray(v)) {
296
+ for (const x of v)
297
+ visit(x);
298
+ return;
299
+ }
300
+ if (v === null || typeof v !== "object")
301
+ return;
302
+ if (isNode(v) && kindOf(v) === "SelectStmt") {
303
+ select(bodyOf(v));
304
+ return;
305
+ }
306
+ for (const x of Object.values(isNode(v) ? bodyOf(v) : v))
307
+ visit(x);
308
+ };
309
+ visit(root);
310
+ }
208
311
  /**
209
312
  * A STORED JSON PATH READ FROM ITS COLUMN (docs/DESIGN.md "Stored JSON paths"): `l` a copied table's
210
313
  * json or jsonb column (Typed.relation) read by literal `steps` — recorded as one of the statement's reads (as its
@@ -238,11 +341,13 @@ class Rewriter {
238
341
  const prefix = fields.length === 1 && t.qualifier !== null ? [S(t.qualifier)] : fields.slice(0, -1).map((f) => structuredClone(f));
239
342
  const column = () => ({ ColumnRef: { fields: [...prefix.map((f) => structuredClone(f)), S(ready.name)] } });
240
343
  const nul = () => fn("chr", [intConst(0)]);
344
+ const recompute = () => op("||", nul(), strConst("!"));
241
345
  const value = asText ? keptText(column, canonical) : caseWhen([[op("=", column(), nul()), nullConst()]], column());
346
+ // a row marked since the path was counted exact is never read as its value: it raises (kept-route.ts markedError)
242
347
  if (ready.exact)
243
- return value;
348
+ return caseWhen([[op("=", column(), recompute()), markedRaise(MARKED_FUNCTIONS.value, column(), markedError(read, false))]], value);
244
349
  // (a copied table's column, checked above: a jsonb's text is the copy's canonical one)
245
- return caseWhen([[or([isNull(column()), op("=", column(), op("||", nul(), strConst("!")))]), jsonGet(this.cp, l, steps, either, canonical, asText, canonical)]], value);
350
+ return caseWhen([[or([isNull(column()), op("=", column(), recompute())]), jsonGet(this.cp, l, steps, either, canonical, asText, canonical)]], value);
246
351
  }
247
352
  /**
248
353
  * A JSON READ OFF `base` BY `steps`: from its stored column where every row holds it (keptRead), else DuckDB's own
@@ -370,6 +475,10 @@ class Rewriter {
370
475
  const own = isNode(v) ? this.synthetic.get(bodyOf(v)) : undefined;
371
476
  if (own !== undefined)
372
477
  this.synthetic.set(bodyOf(out), own);
478
+ // and one typed as another (decidedByKey's any_value): a grouped ORDER BY 1 over a jsonb is keyed over its copy
479
+ const like = isNode(v) ? this.typedLike.get(bodyOf(v)) : undefined;
480
+ if (like !== undefined)
481
+ this.typedLike.set(bodyOf(out), like);
373
482
  return out;
374
483
  }
375
484
  walk(v) {
@@ -449,9 +558,33 @@ class Rewriter {
449
558
  case "ParamRef": return this.param(b);
450
559
  case "SortBy": return this.sortBy(b, null, null);
451
560
  case "RangeFunction": return this.rangeFunction(b);
561
+ case "JoinExpr": return this.leftLateral(b);
452
562
  case "CollateClause": return this.collateClause(b);
453
563
  case "MinMaxExpr": return this.minMax(b);
454
564
  case "SQLValueFunction": return this.sqlValueFunction(b);
565
+ // CASE x WHEN v …: each WHEN is x = v, jsonb's equality for a jsonb (its keys compared)
566
+ case "CaseExpr": {
567
+ if (!isNode(b.arg) || (this.typeOf(b.arg) !== JSONB && this.typeOf(b.arg) !== JSONB_ARRAY))
568
+ return null;
569
+ b.arg = this.equated(b.arg, "CASE … WHEN of");
570
+ for (const w of (b.args ?? []))
571
+ if (isNode(bodyOf(w).expr))
572
+ bodyOf(w).expr = this.equalKey(bodyOf(w).expr);
573
+ return null;
574
+ }
575
+ // x IN (SELECT …), x = ANY / ALL (SELECT …): the subquery's values compared with x by jsonb's equality
576
+ case "SubLink": {
577
+ if (b.subLinkType === "ARRAY_SUBLINK") {
578
+ this.arraySubquery(b);
579
+ return null;
580
+ }
581
+ if (!isNode(b.testexpr) || !["ANY_SUBLINK", "ALL_SUBLINK", "ROWCOMPARE_SUBLINK"].includes(String(b.subLinkType)))
582
+ return null;
583
+ const type = this.typeOf(b.testexpr);
584
+ if (type === JSONB || type === JSONB_ARRAY || this.rowHoldsJsonb(b.testexpr))
585
+ throw new Unrewritable("a jsonb compared with a subquery's values (IN (SELECT …), = ANY (SELECT …)) is not supported by Mason (Postgres equates jsonb by its own rules)", "join the subquery instead: … JOIN (SELECT …) s ON s.v = t.j", "jsonb-compare");
586
+ return null;
587
+ }
455
588
  // a row as a value anywhere but the SELECT list, a cast to text and the json functions (rowValues) is a struct
456
589
  // to DuckDB, compared, tested and grouped by its rules
457
590
  case "ColumnRef":
@@ -465,6 +598,11 @@ class Rewriter {
465
598
  // and a column its subquery has twice in two cases (`"T"`, `t`) is that subquery's renamed one (caseTwins)
466
599
  const fields = (b.fields ?? []);
467
600
  const last = fields.at(-1);
601
+ // a function's whole row by its FROM name alone, where the function is one column and no row (`e` of
602
+ // `jsonb_array_elements(x) e`, whose column is `value`): that column, which DuckDB would read as a struct
603
+ const whole = k === "ColumnRef" && t?.node === "VAR" && fields.length === 1 && last !== undefined && kindOf(last) === "String" ? t.functionColumn : undefined;
604
+ if (whole !== undefined && String(bodyOf(last).sval) !== whole.column)
605
+ return { ColumnRef: { ...b, fields: [S(whole.alias), S(t.renamed ?? whole.column)] } };
468
606
  if (k === "ColumnRef" && t?.node === "VAR" && last !== undefined && kindOf(last) === "String" && (t.renamed !== null || (t.qualifier !== null && fields.length === 1))) {
469
607
  const prefix = fields.length === 1 && t.qualifier !== null ? [S(t.qualifier)] : fields.slice(0, -1);
470
608
  return { ColumnRef: { ...b, fields: [...prefix, t.renamed !== null ? S(t.renamed) : last] } };
@@ -535,6 +673,42 @@ class Rewriter {
535
673
  default: return null;
536
674
  }
537
675
  }
676
+ /**
677
+ * ARRAY(SELECT x …) answers as Postgres's only where x is a scalar DuckDB holds as Postgres does (text, an integer,
678
+ * jsonb, an enum, a bool, a date, …): a numeric is DuckDB's double there (Postgres's numeric[] is exact), an array
679
+ * is a list of lists (Postgres accumulates a multi-dimensional array, and raises 2202E on an empty one, 22004 on a
680
+ * NULL), a row a struct (record[] to Postgres). Those, and an element whose type the analysis does not say, are
681
+ * refused by name.
682
+ */
683
+ arraySubquery(b) {
684
+ let q = isNode(b.subselect) ? bodyOf(b.subselect) : undefined;
685
+ while (q !== undefined && q.op !== undefined && q.op !== "SETOP_NONE" && q.larg !== undefined)
686
+ q = q.larg;
687
+ const target = (q?.targetList ?? [])[0];
688
+ const val = target !== undefined ? bodyOf(target).val : undefined;
689
+ let t = isNode(val) ? this.typeOf(val) : null;
690
+ // an array's element (`a[1]`: no offset of its own to the analysis) is a scalar of the array's element type, a
691
+ // numeric where the array is a numeric[]; a slice is an array
692
+ if (t === null && isNode(val) && kindOf(val) === "A_Indirection") {
693
+ const ind = (bodyOf(val).indirection ?? []);
694
+ const at = isNode(bodyOf(val).arg) ? this.typeOf(bodyOf(val).arg) : null;
695
+ // (a user type's array other than an enum's may be a table's rows: an element whose type is not told)
696
+ const told = at !== null && at !== RECORD_ARRAY && (this.userTypes.get(at)?.startsWith("_") !== true || this.enums.arrays.has(at));
697
+ if (told && ind.length > 0 && ind.every((x) => kindOf(x) === "A_Indices" && bodyOf(x).is_slice !== true))
698
+ t = at === NUMERIC_ARRAY ? NUMERIC : TEXT;
699
+ }
700
+ const refuse = (what) => {
701
+ throw new Unrewritable(`ARRAY(SELECT …) of ${what} is not supported by Mason`, "array_agg(x ORDER BY …) in a subquery, or ARRAY(SELECT x::text …)", "array-subquery-element");
702
+ };
703
+ if (t === null)
704
+ refuse("a value whose type Mason cannot tell");
705
+ if (t === NUMERIC)
706
+ refuse("a numeric (Postgres's numeric[] is exact)");
707
+ if (ARRAYS.has(t) || this.enums.arrays.has(t) || t === INET_ARRAY || this.userTypes.get(t)?.startsWith("_") === true)
708
+ refuse("an array (Postgres accumulates a multi-dimensional array)");
709
+ if (this.isRowType(t))
710
+ refuse("a row (Postgres's record[])");
711
+ }
538
712
  isRowType(oid) { return oid === RECORD || this.userTypes.get(oid)?.startsWith("rowtype:") === true; }
539
713
  /** a typed reference to a FROM item's column (a row's, read as its columns) */
540
714
  typedRef(names, type, typmod) {
@@ -565,7 +739,7 @@ class Rewriter {
565
739
  if (table !== undefined)
566
740
  return table.map((c) => ({ name: c.name, value: this.typedRef([alias, c.name], c.type, c.typmod), type: c.type }));
567
741
  if (t.type !== RECORD)
568
- throw new Unrewritable(`a row of ${this.userTypes.get(t.type)?.replace(/^rowtype:/, "") ?? "a table"} is not supported by Mason: Mason copied the table in part`, "name its columns instead", "row-value");
742
+ throw new Unrewritable(`a row of ${this.userTypes.get(t.type)?.replace(/^rowtype:/, "") ?? "a table"} is not supported by Mason: Mason synced the table in part`, "name its columns instead", "row-value");
569
743
  const source = this.fromSources(select).filter((q) => q.alias === alias);
570
744
  if (source.length !== 1)
571
745
  throw new Unrewritable("a row of this FROM item is not supported by Mason", "name its columns instead", "row-value");
@@ -662,6 +836,9 @@ class Rewriter {
662
836
  if (isNode(c) && typeof bodyOf(c).ctename === "string" && isNode(bodyOf(c).ctequery))
663
837
  this.ctes.set(bodyOf(c).ctename, bodyOf(c).ctequery);
664
838
  this.rowValues(b);
839
+ for (const w of (b.windowClause ?? []))
840
+ if (isNode(w))
841
+ this.keyedPartition(bodyOf(w));
665
842
  // a GROUP BY of an output name is grouped by its ordinal: DuckDB binds a name without regard to case, so `"Name"`
666
843
  // would be another table's `name` (the corpus run of fbbf8f08), where Postgres finds no input column of that name
667
844
  if (Array.isArray(b.groupClause) && Array.isArray(b.targetList)) {
@@ -674,22 +851,33 @@ class Rewriter {
674
851
  return at >= 0 && targets.filter((t) => bodyOf(t).name === f[0]).length === 1 ? intConst(at + 1) : g;
675
852
  });
676
853
  }
677
- for (const g of (b.groupClause ?? []))
678
- if (isNode(g) && kindOf(g) !== "GroupingSet")
679
- this.comparable(this.groupedExpression(g, b), "GROUP BY");
680
- if (Array.isArray(b.distinctClause))
681
- for (const t of (b.targetList ?? []))
682
- if (isNode(bodyOf(t).val))
683
- this.comparable(bodyOf(t).val, "SELECT DISTINCT");
684
- if (b.op !== undefined && b.op !== "SETOP_NONE" && b.all !== true) {
854
+ this.keyedGroups(b);
855
+ // a set operation compares its rows (without ALL, and INTERSECT ALL / EXCEPT ALL count them by equality)
856
+ if (b.op !== undefined && b.op !== "SETOP_NONE" && (b.all !== true || b.op !== "SETOP_UNION")) {
685
857
  let first = b;
686
858
  while (first.larg !== undefined)
687
859
  first = first.larg;
688
- for (const t of (first.targetList ?? []))
689
- if (isNode(bodyOf(t).val))
690
- this.comparable(bodyOf(t).val, `${String(b.op).replace("SETOP_", "")} (without ALL) of`);
860
+ const what = `${String(b.op).replace("SETOP_", "")}${b.all === true ? " ALL" : " (without ALL)"} of`;
861
+ for (const t of (first.targetList ?? [])) {
862
+ const v = bodyOf(t).val;
863
+ if (!isNode(v))
864
+ continue;
865
+ if (this.starHoldsJsonb(v))
866
+ throw new Unrewritable(`${what} a * holding a jsonb column is not supported by Mason (Postgres equates jsonb by its own rules)`, JSONB_WAY_OUT, "jsonb-compare");
867
+ // (INTERSECT ALL and EXCEPT ALL: jsonb's equality alone, which is not its text's; the rest as before)
868
+ if (b.all !== true || this.typeOf(v) === JSONB || this.typeOf(v) === JSONB_ARRAY)
869
+ this.comparable(v, what);
870
+ }
691
871
  }
692
872
  this.decidedByKey(b);
873
+ // `x IS NULL, x DESC` as `x DESC NULLS LAST` (nullsKeyFolded): the folded key is the statement's own, its rules to run
874
+ if (Array.isArray(b.sortClause)) {
875
+ b.sortClause = nullsKeyFolded(b.sortClause, b);
876
+ for (const s of b.sortClause)
877
+ this.original.add(bodyOf(s));
878
+ }
879
+ // (after decidedByKey: a DISTINCT ON a target names it as the SELECT list reads it, through any_value where it must)
880
+ this.keyedDistinct(b);
693
881
  // the statement's own ORDER BY: an ordinal or an output name sorts by that target's expression, so its key is
694
882
  // built over the expression (DuckDB's `ORDER BY 1 COLLATE …` would sort by the constant)
695
883
  if (Array.isArray(b.sortClause) && Array.isArray(b.targetList) && (b.op === undefined || b.op === "SETOP_NONE")) {
@@ -697,6 +885,12 @@ class Rewriter {
697
885
  b.sortClause = b.sortClause.flatMap((s) => {
698
886
  const sb = bodyOf(s);
699
887
  this.done.add(sb);
888
+ // an ordinal that names a jsonb a `*` lists: DuckDB would sort that column's text
889
+ const n = constInt(sb.node);
890
+ const starred = n === null ? null : this.starColumnType(targets, n);
891
+ if (starred === JSONB || starred === JSONB_ARRAY) {
892
+ throw new Unrewritable("ORDER BY the number of a jsonb column a * lists is not supported by Mason (Postgres orders jsonb by its own rules)", "ORDER BY the column's name", "jsonb-compare");
893
+ }
700
894
  return this.sortBy(sb, targets, b) ?? [s];
701
895
  });
702
896
  }
@@ -740,7 +934,9 @@ class Rewriter {
740
934
  const target = bodyOf(targets[n - 1]);
741
935
  const value = target.val;
742
936
  const c = this.textCollation(value);
743
- if (c === null || c.kind === "bytes")
937
+ // (a jsonb is sorted by its key whatever the collation: the SortBy rule keys the output column)
938
+ const keyed = this.typeOf(value) === JSONB || this.typeOf(value) === JSONB_ARRAY;
939
+ if (!keyed && (c === null || c.kind === "bytes"))
744
940
  continue;
745
941
  if (target.name === undefined)
746
942
  target.name = `__mason_sort_${n}`;
@@ -849,7 +1045,7 @@ class Rewriter {
849
1045
  */
850
1046
  withTies(b) {
851
1047
  if (b.distinctClause !== undefined)
852
- throw new Unrewritable("FETCH … WITH TIES over a SELECT DISTINCT is not supported by Mason (DuckDB has no WITH TIES)", "select the DISTINCT rows in a subquery and FETCH … WITH TIES from it", "with-ties-distinct");
1048
+ throw new Unrewritable("FETCH … WITH TIES over a SELECT DISTINCT is not supported by Mason (the engine has no WITH TIES)", "select the DISTINCT rows in a subquery and FETCH … WITH TIES from it", "with-ties-distinct");
853
1049
  const order = b.sortClause ?? [];
854
1050
  const count = b.limitCount;
855
1051
  delete b.limitCount;
@@ -1637,6 +1833,9 @@ class Rewriter {
1637
1833
  e = this.foldCitext(e);
1638
1834
  b.node = e;
1639
1835
  }
1836
+ // a jsonb sorts by its order key (jsonb's own order: src/duck/jsonb-order.ts), its NULLs where the SortBy puts them
1837
+ if (type === JSONB)
1838
+ return [{ SortBy: { ...b, node: this.jsonbKey(e) } }];
1640
1839
  this.comparable(e, "ORDER BY");
1641
1840
  const c = this.textCollation(e);
1642
1841
  // a char(n) sorts without its padding (bpcharcmp)
@@ -1707,17 +1906,217 @@ class Rewriter {
1707
1906
  }
1708
1907
  return g;
1709
1908
  }
1710
- /** a value Mason cannot sort, group or compare as Postgres does: jsonb (Postgres's own order and equality), a text
1711
- * array under the database's collation (each element under it) — refused, where a statement would need it */
1909
+ /** a value Mason cannot sort, group or compare as Postgres does: a jsonb where it is not keyed (`compared` keys it),
1910
+ * a jsonb array (each element by jsonb's rules), a text array under the database's collation (each element under
1911
+ * it) — refused, where a statement would need it */
1712
1912
  comparable(n, where) {
1713
1913
  const type = this.typeOf(n);
1714
1914
  if (this.isCitext(type))
1715
1915
  throw new Unrewritable(`${where} a citext value is not supported by Mason (Postgres groups and sorts it without regard to case)`, "GROUP BY lower(x), or x::text where case matters", "citext-forms");
1716
1916
  if (type === JSONB)
1717
- throw new Unrewritable(`${where} a jsonb value is not supported by Mason (Postgres orders and equates jsonb by its own rules)`, "compare a member (j ->> 'k'), or j::text", "jsonb-compare");
1917
+ throw new Unrewritable(`${where} a jsonb value is not supported by Mason (Postgres orders and equates jsonb by its own rules)`, JSONB_WAY_OUT, "jsonb-compare");
1918
+ if (type === JSONB_ARRAY)
1919
+ throw new Unrewritable(`${where} a jsonb array is not supported by Mason (Postgres compares its elements by jsonb's rules)`, "compare an element (a[1]), or a::text", "jsonb-compare");
1920
+ if (this.rowHoldsJsonb(n))
1921
+ throw new Unrewritable(`${where} a row holding a jsonb is not supported by Mason (Postgres compares the jsonb by its own rules)`, "list its members one by one: GROUP BY a, j", "jsonb-compare");
1718
1922
  if ((type === 1009 || type === 1015) && this.collation.kind !== "bytes")
1719
1923
  throw new Unrewritable(`${where} a text array is not supported by Mason under the database's collation`, "sort by an element (a[1]) or by array_to_string(a, ',')", "text-array-order");
1720
1924
  }
1925
+ /**
1926
+ * A JSONB AS ITS ORDER KEY (src/duck/jsonb-order.ts): bytes whose order and equality are Postgres's for jsonb
1927
+ * (`compareJsonbContainers`: by type, then size, then member by member; numbers as numerics; strings under the
1928
+ * database's collation). Under C the key is one call; under ICU or a libc locale each of the document's strings
1929
+ * needs DuckDB's ICU key, which a JavaScript function cannot ask for, so DuckDB makes them over the strings the
1930
+ * function lists (`list_transform` with a lambda, src/duck/store.ts `lambda_syntax`) and hands them to the key.
1931
+ */
1932
+ jsonbKey(e) {
1933
+ return this.keptKey(e) ?? this.jsonbKeyCall(e);
1934
+ }
1935
+ /** the key computed per row (`jsonbKey`): the functions src/duck/jsonb-order.ts `JSONB_KEY_FUNCTIONS` names */
1936
+ jsonbKeyCall(e) {
1937
+ const c = this.collation;
1938
+ if (c.kind === "bytes")
1939
+ return fn(JSONB_KEY_FUNCTIONS.bytes.key, [e]);
1940
+ const each = () => ({ ColumnRef: { fields: [S("__mason_s")] } });
1941
+ const f = JSONB_KEY_FUNCTIONS[c.kind];
1942
+ const keys = fn("list_transform", [fn(f.strings, [this.clone(e)]), op("->", each(), fn("icu_sort_key", [each(), strConst(icuNameOf(c))]))]);
1943
+ return fn(f.key, [e, keys]);
1944
+ }
1945
+ /**
1946
+ * A JSONB'S STORED KEY (src/duck/kept-route.ts `ORDER_STEPS`): `e` a copied table's jsonb column — recorded as one of
1947
+ * the statement's reads, the order's, so the keeper weighs storing its key as it weighs a path — read from the key's
1948
+ * hidden column once every row of it is stored (READY). The column holds the key, `KEY_NULL` for a NULL document, or
1949
+ * `KEY_RECOMPUTE` where the key raised; read alone while no row is marked, else through a CASE that computes the
1950
+ * marked rows (and raises the key's own error). null: not such a column, or not stored — the caller computes the key.
1951
+ * `steps` `EQUAL_STEPS` is the C key under a libc collation (`equalKey`), recorded and read the same way, computed by
1952
+ * the C key's function where a row is marked.
1953
+ */
1954
+ keptKey(e, steps = ORDER_STEPS) {
1955
+ // a column of a derived table typed as the subquery's target (sortBy's `s.*` wrapper, `typedLike`) is the
1956
+ // subquery's column, whose FROM item has no hidden column (the oracle's eager run: DuckDB's 42703)
1957
+ if (this.kept === null || kindOf(e) !== "ColumnRef" || this.typedLike.has(bodyOf(e)))
1958
+ return null;
1959
+ const fields = (bodyOf(e).fields ?? []);
1960
+ const last = fields.at(-1);
1961
+ const t = this.typed(e);
1962
+ if (last === undefined || kindOf(last) !== "String" || t === null || t.node !== "VAR" || t.relation === undefined || t.renamed !== null)
1963
+ return null;
1964
+ const at = this.kept.at(t.relation.relid, t.relation.attno);
1965
+ if (at === null)
1966
+ return null;
1967
+ const read = { source: at.source, column: at.column, steps, either: false, canonical: true, asText: false };
1968
+ this.jsonReads.set(readKey(read), read);
1969
+ const ready = this.kept.ready(read);
1970
+ if (ready === null)
1971
+ return null;
1972
+ const computed = () => steps === EQUAL_STEPS ? fn(JSONB_KEY_FUNCTIONS.bytes.key, [e]) : this.jsonbKeyCall(e);
1973
+ const prefix = fields.length === 1 && t.qualifier !== null ? [S(t.qualifier)] : fields.slice(0, -1).map((f) => structuredClone(f));
1974
+ const column = () => ({ ColumnRef: { fields: [...prefix.map((f) => structuredClone(f)), S(ready.name)] } });
1975
+ const recompute = () => fn("unhex", [strConst(KEY_RECOMPUTE_HEX)]);
1976
+ const isNullKey = () => op("=", column(), fn("unhex", [strConst(KEY_NULL_HEX)]));
1977
+ // a row marked since the key was counted exact is never read as a key: it raises (kept-route.ts markedError)
1978
+ if (ready.exact) {
1979
+ const raise = markedRaise(MARKED_FUNCTIONS.key, fn("hex", [column()]), markedError(read, this.collation.kind === "libc"));
1980
+ return caseWhen([[isNullKey(), nullConst()], [op("=", column(), recompute()), raise]], column());
1981
+ }
1982
+ return caseWhen([[or([isNull(column()), op("=", column(), recompute())]), computed()]], caseWhen([[isNullKey(), nullConst()]], column()));
1983
+ }
1984
+ /**
1985
+ * A JSONB AS ITS EQUALITY KEY: the key under C, whatever the database's collation. Postgres's default collation is
1986
+ * deterministic, so two jsonb strings are equal only when their bytes are (varstr_cmp breaks a collation's tie by
1987
+ * strcmp), and the C key's equality is exactly jsonb's — where the collation's key would refuse a value its order
1988
+ * does not cover (a libc collation's `libc-uncovered`: an emoji's variation selector, an NFD accent) that equality
1989
+ * never needed to order. Every context that only equates (=, <>, IN, = ANY, IS DISTINCT FROM, NULLIF, CASE x WHEN,
1990
+ * GROUP BY, DISTINCT [ON], count(DISTINCT), PARTITION BY) reads this one; ORDER BY, <, >, BETWEEN read `jsonbKey`.
1991
+ */
1992
+ equalKey(e) {
1993
+ // under C and ICU the order's key is an equality key too (a string's collation key, then its bytes), which never
1994
+ // refuses, so a stored one serves (keptKey) — both sides of an equality read the order's key; only a libc key
1995
+ // refuses a string (libc-uncovered), so under libc equality reads the C key: its own stored column where it is
1996
+ // (`EQUAL_STEPS`, src/duck/kept.ts), else computed
1997
+ if (this.collation.kind !== "libc")
1998
+ return this.jsonbKey(e);
1999
+ return this.keptKey(e, EQUAL_STEPS) ?? fn(JSONB_KEY_FUNCTIONS.bytes.key, [e]);
2000
+ }
2001
+ /** a value compared for EQUALITY only (PARTITION BY, IS DISTINCT FROM, NULLIF, CASE x WHEN, = ANY, DISTINCT ON): a
2002
+ * jsonb as its equality key, a jsonb array refused; any other value as it is (its equality is DuckDB's) */
2003
+ equated(n, where) {
2004
+ const type = this.typeOf(n);
2005
+ if (type === JSONB)
2006
+ return this.equalKey(n);
2007
+ if (type === JSONB_ARRAY)
2008
+ this.comparable(n, where);
2009
+ return n;
2010
+ }
2011
+ /** a row value (`(a, j)`, `ROW(…)`, a list of IN's left) one of whose members, at any depth, is a jsonb or a jsonb array */
2012
+ rowHoldsJsonb(n) {
2013
+ if (kindOf(n) !== "RowExpr")
2014
+ return false;
2015
+ return (bodyOf(n).args ?? []).some((a) => isNode(a) && (this.typeOf(a) === JSONB || this.typeOf(a) === JSONB_ARRAY || this.rowHoldsJsonb(a)));
2016
+ }
2017
+ /** the type of the `n`th output column where a `*` (or `t.*`) lists it, as Postgres expanded the stars (each column's
2018
+ * VAR at its star's offset, in order); null where the column is a target of its own, or not known */
2019
+ starColumnType(targets, n) {
2020
+ let at = 0;
2021
+ for (const t of targets) {
2022
+ const v = bodyOf(t).val;
2023
+ if (v === undefined || !hasStar(v) || kindOf(v) !== "ColumnRef") {
2024
+ at++;
2025
+ if (at === n)
2026
+ return null;
2027
+ continue;
2028
+ }
2029
+ const loc = bodyOf(v).location;
2030
+ const cols = typeof loc === "number" ? (this.types.get(loc - this.base) ?? []).filter((x) => x.node === "VAR") : [];
2031
+ if (cols.length === 0)
2032
+ return null;
2033
+ if (n <= at + cols.length)
2034
+ return cols[n - at - 1].type;
2035
+ at += cols.length;
2036
+ }
2037
+ return null;
2038
+ }
2039
+ /** a `*` (or `t.*`) whose columns hold a jsonb or a jsonb array, as Postgres expanded it (each column's VAR at the
2040
+ * star's own offset) */
2041
+ starHoldsJsonb(n) {
2042
+ if (!hasStar(n) || kindOf(n) !== "ColumnRef")
2043
+ return false;
2044
+ const at = bodyOf(n).location;
2045
+ return typeof at === "number" && (this.types.get(at - this.base) ?? []).some((t) => t.node === "VAR" && (t.type === JSONB || t.type === JSONB_ARRAY));
2046
+ }
2047
+ /**
2048
+ * GROUP BY A JSONB (Postgres groups by jsonb's equality: `1.0` and `1.00` are one group): grouped by its order key,
2049
+ * an ordinal or an output name by its target's. The value itself is then no group key, so `decidedByKey` reads it
2050
+ * through any_value — the group's value, which is one of the texts Postgres's group holds, as Postgres's answer is
2051
+ * the one its plan meets first (gap jsonb-equal-text). Anything else Mason cannot group as Postgres does is refused,
2052
+ * in a grouping set too.
2053
+ */
2054
+ keyedGroups(b) {
2055
+ if (!Array.isArray(b.groupClause))
2056
+ return;
2057
+ const refuseIn = (v) => {
2058
+ if (Array.isArray(v)) {
2059
+ for (const x of v)
2060
+ refuseIn(x);
2061
+ return;
2062
+ }
2063
+ if (!isNode(v))
2064
+ return;
2065
+ if (kindOf(v) === "GroupingSet")
2066
+ refuseIn(bodyOf(v).content);
2067
+ else
2068
+ this.comparable(this.groupedExpression(v, b), "GROUP BY a grouping set of");
2069
+ };
2070
+ b.groupClause = b.groupClause.map((g) => {
2071
+ if (!isNode(g))
2072
+ return g;
2073
+ if (kindOf(g) === "GroupingSet") {
2074
+ refuseIn(bodyOf(g).content);
2075
+ return g;
2076
+ }
2077
+ const e = this.groupedExpression(g, b);
2078
+ if (this.typeOf(e) !== JSONB) {
2079
+ this.comparable(e, "GROUP BY");
2080
+ return g;
2081
+ }
2082
+ return this.equalKey(e === g ? g : this.clone(e));
2083
+ });
2084
+ }
2085
+ /**
2086
+ * SELECT DISTINCT OVER A JSONB (Postgres's DISTINCT is jsonb's equality): DuckDB's DISTINCT ON the targets, each
2087
+ * jsonb by its order key — the first row of each set of equal ones, in the ORDER BY's order, as DuckDB's DISTINCT ON
2088
+ * keeps (one of the texts Postgres's answer may be: gap jsonb-equal-text). A DISTINCT ON's own jsonb expression is
2089
+ * keyed alike. A `*` holding a jsonb column is refused (its columns are not written out here).
2090
+ */
2091
+ keyedDistinct(b) {
2092
+ if (!Array.isArray(b.distinctClause))
2093
+ return;
2094
+ const targets = (b.targetList ?? []);
2095
+ const plain = b.distinctClause.length === 1 && !isNode(b.distinctClause[0]);
2096
+ const values = targets.map((t) => bodyOf(t).val).filter(isNode);
2097
+ for (const v of values)
2098
+ if (this.typeOf(v) !== JSONB)
2099
+ this.comparable(v, "SELECT DISTINCT");
2100
+ if (!plain) {
2101
+ b.distinctClause = b.distinctClause.map((d) => {
2102
+ const n = constInt(d);
2103
+ const ordinal = n !== null && n >= 1 && n <= targets.length;
2104
+ const e = ordinal ? bodyOf(targets[n - 1]).val : d;
2105
+ if (this.typeOf(e) !== JSONB) {
2106
+ this.comparable(e, "SELECT DISTINCT ON");
2107
+ return d;
2108
+ }
2109
+ return this.equalKey(ordinal ? this.clone(e) : d);
2110
+ });
2111
+ return;
2112
+ }
2113
+ for (const v of values)
2114
+ if (this.starHoldsJsonb(v))
2115
+ throw new Unrewritable("SELECT DISTINCT * over a jsonb column is not supported by Mason (Postgres equates jsonb by its own rules)", "name the columns: SELECT DISTINCT a, b, …", "jsonb-compare");
2116
+ if (!values.some((v) => this.typeOf(v) === JSONB))
2117
+ return;
2118
+ b.distinctClause = values.map((v) => this.equated(this.clone(v), "SELECT DISTINCT"));
2119
+ }
1721
2120
  collateClause(b) {
1722
2121
  // an expression's own COLLATE outside a sort: a comparison's is handled at the comparison; C needs nothing
1723
2122
  const c = this.namedCollation(lastName(b.collname));
@@ -1740,6 +2139,9 @@ class Rewriter {
1740
2139
  minMax(b) {
1741
2140
  // GREATEST / LEAST of text compare under the collation
1742
2141
  const t = this.at(b, ["MINMAXEXPR"]);
2142
+ if (t !== null && (t.type === JSONB || t.type === JSONB_ARRAY)) {
2143
+ throw new Unrewritable(`${b.op === "IS_LEAST" ? "LEAST" : "GREATEST"} of jsonb values is not supported by Mason (Postgres orders jsonb by its own rules)`, "ORDER BY the value … LIMIT 1 in a subquery", "jsonb-compare");
2144
+ }
1743
2145
  if (t !== null)
1744
2146
  b.args = b.args.map((a, i) => this.widen(a, t.args[i]?.type));
1745
2147
  if (t === null || !TEXTS.has(t.type) || BYTE_COLLATIONS.has(t.inputCollation) || this.collation.kind === "bytes")
@@ -1766,9 +2168,74 @@ class Rewriter {
1766
2168
  // jsonb's jsonpath operators: Postgres's answer for each row (src/duck/pg-eval.ts)
1767
2169
  if (b.kind === "AEXPR_OP" && (name === "@?" || name === "@@") && a0 === JSONB && l !== undefined && r !== undefined)
1768
2170
  return pgCall("bool", `a1::jsonb ${name} a2::jsonpath`, [l, r]);
2171
+ // x = ANY (ARRAY(SELECT …)): DuckDB binds the array's subquery as a comparison of its own and refuses the types
2172
+ if ((b.kind === "AEXPR_OP_ANY" || b.kind === "AEXPR_OP_ALL") && r !== undefined && kindOf(r) === "SubLink" && bodyOf(r).subLinkType === "ARRAY_SUBLINK") {
2173
+ throw new Unrewritable(`${b.kind === "AEXPR_OP_ANY" ? "ANY" : "ALL"} of ARRAY(SELECT …) is not supported by Mason`, `x ${name} ${b.kind === "AEXPR_OP_ANY" ? "ANY" : "ALL"} (SELECT …), without the ARRAY`, "array-subquery-element");
2174
+ }
1769
2175
  if ((b.kind === "AEXPR_OP_ANY" || b.kind === "AEXPR_OP_ALL") && this.isCitext(a0)) {
1770
2176
  throw new Unrewritable("a citext compared with ANY or ALL of an array is not supported by Mason", "x IN (…) with the values written out, or lower(x) = ANY (…)", "citext-forms");
1771
2177
  }
2178
+ // A JSONB COMPARED (jsonb's =, <>, <, … and its btree's order: src/duck/jsonb-order.ts), each side as its order key
2179
+ // (an operand of a jsonb operator is a jsonb, a literal included: each is keyed, whatever its own analysis says)
2180
+ const lType = l === undefined ? null : this.typeOf(l);
2181
+ const comparing = ["=", "<>", "!=", "<", "<=", ">", ">="].includes(name);
2182
+ // rows compared member by member (`(id, j) = (1, '…')`: Postgres's per-member operators; a struct to DuckDB,
2183
+ // compared by its text)
2184
+ if ((l !== undefined && this.rowHoldsJsonb(l)) || (r !== undefined && this.rowHoldsJsonb(r))) {
2185
+ throw new Unrewritable("a row holding a jsonb compared with another is not supported by Mason (Postgres compares the jsonb by its own rules)", "compare the members one by one: a = x AND j = y", "jsonb-compare");
2186
+ }
2187
+ // a jsonb array's containment and overlap compare its elements by jsonb's equality, DuckDB's list operators by text
2188
+ if (b.kind === "AEXPR_OP" && ["@>", "<@", "&&"].includes(name) && (a0 === JSONB_ARRAY || a1 === JSONB_ARRAY)) {
2189
+ throw new Unrewritable(`a jsonb array's ${name} is not supported by Mason (Postgres compares its elements by jsonb's equality)`, "compare an element: a[1] = '…'::jsonb, or a::text", "jsonb-compare");
2190
+ }
2191
+ // a jsonb's containment with ANY / ALL of an array written out: each element's containment, OR'd (ALL: AND'd), as
2192
+ // SQL's three values are ANY's and ALL's (DuckDB has no `@> ANY`)
2193
+ if ((b.kind === "AEXPR_OP_ANY" || b.kind === "AEXPR_OP_ALL") && (name === "@>" || name === "<@") && (a0 === JSONB || lType === JSONB) && l !== undefined && r !== undefined) {
2194
+ if (kindOf(r) !== "A_ArrayExpr" || (bodyOf(r).elements ?? []).length === 0) {
2195
+ throw new Unrewritable(`a jsonb's ${name} with ${b.kind === "AEXPR_OP_ANY" ? "ANY" : "ALL"} of an array that is not written out is not supported by Mason`, `write the values out: j ${name} '…' OR j ${name} '…'`, "jsonb-compare");
2196
+ }
2197
+ const each = (bodyOf(r).elements ?? []).map((e, i) => {
2198
+ const x = i === 0 ? l : this.clone(l);
2199
+ return name === "@>" ? fn("__mason_jsonb_contains", [jsonArg(x), jsonArg(e)]) : fn("__mason_jsonb_contains", [jsonArg(e), jsonArg(x)]);
2200
+ });
2201
+ return b.kind === "AEXPR_OP_ANY" ? or(each) : and(each);
2202
+ }
2203
+ // an operator's ANY / ALL compares only for a comparison (`j @> ANY (…)` is containment, above)
2204
+ const compares = b.kind === "AEXPR_OP" || b.kind === "AEXPR_OP_ANY" || b.kind === "AEXPR_OP_ALL" ? comparing : true;
2205
+ if (l !== undefined && r !== undefined && (a0 === JSONB || a0 === JSONB_ARRAY || lType === JSONB || lType === JSONB_ARRAY) && compares) {
2206
+ const what = `comparing (${b.kind === "AEXPR_OP" ? name : b.kind.replace("AEXPR_", "").replace(/_/g, " ").toLowerCase()})`;
2207
+ if (a0 === JSONB_ARRAY || lType === JSONB_ARRAY)
2208
+ this.comparable(l, what);
2209
+ // equality by the equality key (no collation: equalKey), an order by the collation's
2210
+ const equating = ["=", "<>", "!="].includes(name) || ["AEXPR_DISTINCT", "AEXPR_NOT_DISTINCT", "AEXPR_NULLIF"].includes(String(b.kind));
2211
+ const key = (x) => equating ? this.equalKey(x) : this.jsonbKey(x);
2212
+ switch (b.kind) {
2213
+ case "AEXPR_OP": return op(name, key(l), key(r));
2214
+ case "AEXPR_DISTINCT":
2215
+ case "AEXPR_NOT_DISTINCT": return { A_Expr: { ...b, lexpr: key(l), rexpr: key(r) } };
2216
+ // NULLIF(a, b): NULL where a = b, else a (the value, never its key)
2217
+ case "AEXPR_NULLIF": return caseWhen([[op("=", key(l), key(r)), nullConst()]], this.clone(l));
2218
+ case "AEXPR_IN":
2219
+ if (kindOf(r) === "List")
2220
+ return { A_Expr: { ...b, lexpr: key(l), rexpr: { List: { items: (bodyOf(r).items ?? []).map(key) } } } };
2221
+ break;
2222
+ case "AEXPR_OP_ANY":
2223
+ case "AEXPR_OP_ALL":
2224
+ if (kindOf(r) === "A_ArrayExpr" && comparing)
2225
+ return { A_Expr: { ...b, lexpr: key(l), rexpr: { A_ArrayExpr: { ...bodyOf(r), elements: (bodyOf(r).elements ?? []).map(key) } } } };
2226
+ throw new Unrewritable(`a jsonb compared with ${b.kind === "AEXPR_OP_ANY" ? "ANY" : "ALL"} of an array that is not written out is not supported by Mason`, "write the values out: x IN ('…'::jsonb, …), or ARRAY['…'::jsonb, …]", "jsonb-compare");
2227
+ case "AEXPR_BETWEEN":
2228
+ case "AEXPR_NOT_BETWEEN": {
2229
+ const [lo, hi] = (bodyOf(r).items ?? []);
2230
+ if (lo === undefined || hi === undefined)
2231
+ break;
2232
+ const inside = and([op(">=", key(l), key(lo)), op("<=", key(this.clone(l)), key(hi))]);
2233
+ return b.kind === "AEXPR_NOT_BETWEEN" ? not(inside) : inside;
2234
+ }
2235
+ default: break;
2236
+ }
2237
+ throw new Unrewritable(`${what} a jsonb value is not supported by Mason (Postgres orders and equates jsonb by its own rules)`, JSONB_WAY_OUT, "jsonb-compare");
2238
+ }
1772
2239
  switch (b.kind) {
1773
2240
  case "AEXPR_LIKE":
1774
2241
  case "AEXPR_ILIKE": {
@@ -2083,8 +2550,14 @@ class Rewriter {
2083
2550
  b.args = b.args.map((a, i) => this.widen(a, t.args[i]?.type));
2084
2551
  }
2085
2552
  // --------------------------------------------------------------------------------------------- functions
2553
+ /** a window's PARTITION BY: a jsonb by its order key (jsonb's equality), a jsonb array refused */
2554
+ keyedPartition(w) {
2555
+ if (w !== undefined && Array.isArray(w.partitionClause))
2556
+ w.partitionClause = w.partitionClause.map((p) => isNode(p) ? this.equated(p, "PARTITION BY") : p);
2557
+ }
2086
2558
  funcCall(b) {
2087
2559
  const name = lastName(b.funcname);
2560
+ this.keyedPartition(b.over);
2088
2561
  const t = this.typed({ FuncCall: b });
2089
2562
  const args = (b.args ?? []);
2090
2563
  const argType = (i) => t?.args[i]?.type ?? (args[i] !== undefined ? this.typeOf(args[i]) : null);
@@ -2109,8 +2582,20 @@ class Rewriter {
2109
2582
  throw new Unrewritable(`${name}() of an interval is not supported by Mason`, "sum(extract(epoch FROM x)) for the seconds", "interval-aggregates");
2110
2583
  if (name === "sum" && this.typeOf(args[0] ?? { A_Const: {} }) === FLOAT4 && argType(0) === FLOAT4)
2111
2584
  throw new Unrewritable("sum() of a real is not supported by Mason (Postgres adds in single precision)", "sum(x::double precision)", "sum-real");
2112
- if (b.agg_distinct === true && args[0] !== undefined)
2585
+ // count(DISTINCT x) of a column unique on the source, over a FROM that repeats none of its table's rows: no value is
2586
+ // there twice, so it is count(x), its FILTER kept (src/duck/distinct-key.ts, `keyCounts`)
2587
+ if (b.agg_distinct === true && name === "count" && typeof b.location === "number" && this.keyCounts.has(b.location))
2588
+ delete b.agg_distinct;
2589
+ // count(DISTINCT j) counts jsonb's equal values once: their keys, counted (another aggregate's DISTINCT would hand
2590
+ // the key itself to the aggregate)
2591
+ if (b.agg_distinct === true && args[0] !== undefined && name === "count" && args.length === 1 && this.typeOf(args[0]) === JSONB)
2592
+ args[0] = this.equalKey(args[0]);
2593
+ else if (b.agg_distinct === true && args[0] !== undefined)
2113
2594
  this.comparable(args[0], `${name}(DISTINCT …) of`);
2595
+ // a jsonb array searched for an element: jsonb's equality, where DuckDB's list functions compare the text
2596
+ if (["array_position", "array_positions", "array_remove", "array_replace"].includes(name) && args[0] !== undefined && this.typeOf(args[0]) === JSONB_ARRAY) {
2597
+ throw new Unrewritable(`${name}() of a jsonb array is not supported by Mason (Postgres compares its elements by jsonb's equality)`, "compare an element: a[1] = '…'::jsonb", "jsonb-compare");
2598
+ }
2114
2599
  // a function-style cast (`date(x)`, `text(x)`, `numeric(x)`) is the cast
2115
2600
  if (t !== null && (t.node === "COERCEVIAIO" || t.node === "RELABELTYPE" || (t.node === "FUNCEXPR" && t.format !== null && t.format !== 0 && t.format !== 3)) && args.length === 1) {
2116
2601
  if (t.type === TEXT || t.type === VARCHAR) {
@@ -2188,10 +2673,18 @@ class Rewriter {
2188
2673
  return null;
2189
2674
  case "date_trunc":
2190
2675
  if (args.length === 3) {
2191
- // in a zone: the wall clock there truncated, read back as an instant in that zone
2676
+ // in a zone: the instant's own arithmetic where that is Postgres's answer (truncInstant), else the wall clock
2677
+ // there truncated, read back as an instant in that zone
2192
2678
  const [unit, ts] = args;
2193
2679
  const zone = this.zoneArg(args[2]);
2194
- return fn("timezone", [zone, fn("date_trunc", [unit, fn("timezone", [this.clone(zone), this.asInstant(ts, argType(1))])])]);
2680
+ const instant = this.asInstant(ts, argType(1));
2681
+ const fast = this.truncInstant((constString(unit) ?? "").toLowerCase(), instant, zone, constString(zone));
2682
+ // date_trunc is strict and reads its zone first: a zone not a constant name (a row's, a NULL) is read here
2683
+ // (`__mason_zone`, which refuses a name Postgres would) and a NULL one is NULL, whatever the part — a second
2684
+ // or less never names the zone, and a minute's or an hour's ±infinity is the instant's own
2685
+ if (fast !== null)
2686
+ return constString(zone) !== null ? fast : caseWhen([[isNull(zone), nullConst()]], fast);
2687
+ return fn("timezone", [zone, this.truncWall(unit, fn("timezone", [this.clone(zone), instant]), TIMESTAMP)]);
2195
2688
  }
2196
2689
  if (args.length !== 2)
2197
2690
  return null;
@@ -2203,20 +2696,25 @@ class Rewriter {
2203
2696
  if (argType(1) === INTERVAL && unit === "week")
2204
2697
  throw new Unrewritable(`unit "week" not supported for type interval`, "", { postgres: "0A000" });
2205
2698
  const x = argType(1) === TIMESTAMPTZ && own === DATE ? cast(args[1], ["timestamptz"]) : args[1];
2206
- // Postgres's centuries and millennia begin in a year 1 (2001-01-01), DuckDB's in a year 0 (DT_CENTURY)
2207
- // (and a decade before year 1 is counted back as Postgres counts it: timestamp_trunc's DTK_DECADE)
2208
- if ((unit === "century" || unit === "millennium" || unit === "decade") && argType(1) !== INTERVAL) {
2209
- const y = fn("year", [this.clone(x)]);
2210
- const first = unit === "decade"
2211
- ? caseWhen([[op(">", y, intConst(0)), op("*", op("//", this.clone(y), intConst(10)), intConst(10))]], op("*", op("-", intConst(0), op("//", op("-", intConst(8), op("-", this.clone(y), intConst(1))), intConst(10))), intConst(10)))
2212
- : (() => {
2213
- const n = unit === "century" ? 100 : 1000;
2214
- return caseWhen([[op(">", y, intConst(0)), op("-", op("*", op("//", op("+", this.clone(y), intConst(n - 1)), intConst(n)), intConst(n)), intConst(n - 1))]], op("+", op("*", op("-", intConst(0), op("//", op("-", intConst(n - 1), op("-", this.clone(y), intConst(1))), intConst(n))), intConst(n)), intConst(1)));
2215
- })();
2216
- const truncated = op("+", fn("date_trunc", [strConst("year"), this.clone(x)]), fn("to_years", [cast(op("-", first, this.clone(y)), ["integer"])]));
2217
- return caseWhen([[fn("isfinite", [this.clone(x)]), truncated]], argType(1) === TIMESTAMPTZ ? cast(x, ["timestamptz"]) : cast(x, ["timestamp"]));
2699
+ const wall = (v) => (unit === "century" || unit === "millennium" || unit === "decade") && argType(1) !== INTERVAL
2700
+ ? this.truncWall(args[0], v, argType(1) === TIMESTAMPTZ ? TIMESTAMPTZ : TIMESTAMP)
2701
+ : v === args[1] ? null : fn("date_trunc", [args[0], v]);
2702
+ // a timestamptz in the session's zone: the instant's own arithmetic where that is Postgres's answer. The
2703
+ // zone is the one the statement EXECUTES in, never the one it was planned in (a prepared statement is
2704
+ // planned at its Parse and run after any later SET TimeZone): `current_setting('TimeZone')`, which DuckDB
2705
+ // folds to a constant when it plans the statement it runs, so the CASE costs no row anything
2706
+ if (argType(1) === TIMESTAMPTZ) {
2707
+ const session = () => fn("current_setting", [strConst("TimeZone")]);
2708
+ const utc = this.truncInstant(unit, this.clone(x), session(), "UTC");
2709
+ if (utc !== null && ["microseconds", "milliseconds", "second"].includes(unit))
2710
+ return utc;
2711
+ if (utc !== null) {
2712
+ const inUtc = or(UTC_NAMES.map((z) => op("=", session(), strConst(z))));
2713
+ const elsewhere = this.truncInstant(unit, x, session(), null) ?? wall(x) ?? fn("date_trunc", [args[0], x]);
2714
+ return caseWhen([[inUtc, utc]], elsewhere);
2715
+ }
2218
2716
  }
2219
- return x === args[1] ? null : fn("date_trunc", [args[0], x]);
2717
+ return wall(x);
2220
2718
  }
2221
2719
  case "make_interval": return makeInterval(args);
2222
2720
  // isfinite of a time or an interval: DuckDB has none, and neither is ever infinite here
@@ -2355,7 +2853,7 @@ class Rewriter {
2355
2853
  case "json_object_keys":
2356
2854
  if (this.inFrom.has(b))
2357
2855
  return null;
2358
- return fn("unnest", [fn("__mason_json_keys", [args[0], boolConst(name.startsWith("jsonb"))])]);
2856
+ return fn("unnest", [fn("__mason_json_keys", [args[0], boolConst(name.startsWith("jsonb")), boolConst(false)])]);
2359
2857
  case "jsonb_array_elements":
2360
2858
  case "json_array_elements":
2361
2859
  case "jsonb_array_elements_text":
@@ -2424,6 +2922,8 @@ class Rewriter {
2424
2922
  case "family": return argType(0) === INET && args.length === 1
2425
2923
  ? caseWhen([[{ NullTest: { arg: this.cp(args[0]), nulltesttype: "IS_NULL" } }, nullConst()], [fn("contains", [args[0], strConst(":")]), intConst(6)]], intConst(4)) : null;
2426
2924
  case "abbrev": return argType(0) === INET && args.length === 1 ? inetOut(args[0]) : null;
2925
+ // the session's setting, never DuckDB's own (`settingRead`)
2926
+ case "current_setting": return args.length === 1 || args.length === 2 ? settingRead(this.cp, args) : null;
2427
2927
  // the transaction's start, as of the execution (sqlValueFunction says why)
2428
2928
  case "now":
2429
2929
  case "transaction_timestamp": return args.length === 0 ? this.now("exec") : null;
@@ -2493,6 +2993,72 @@ class Rewriter {
2493
2993
  const own = this.typeOf(n);
2494
2994
  return wanted === TIMESTAMPTZ && own !== null && own !== TIMESTAMPTZ && own !== UNKNOWN ? cast(n, ["timestamptz"]) : n;
2495
2995
  }
2996
+ /**
2997
+ * A TIMESTAMPTZ TRUNCATED AS THE INSTANT'S OWN ARITHMETIC (docs/DESIGN.md "date_trunc of a timestamptz"), where
2998
+ * that is exactly Postgres's `timestamptz_trunc_internal` and cheaper than DuckDB's: DuckDB's `date_trunc(part,
2999
+ * TIMESTAMPTZ)` is ICU's calendar, ~45 ns a row a thread even in UTC (and its sub-day parts in another zone re-read the
3000
+ * wall clock, so an instant in a repeated hour lands on the other side of the transition, where Postgres keeps its
3001
+ * offset), while the instant's microseconds (`epoch_us`, `make_timestamptz`) are ~7. Postgres truncates the wall clock
3002
+ * in `zone` and, for a day or longer, reads the truncated wall clock back in that zone (`redotz`), else keeps the
3003
+ * instant's own offset. So, the instant's microseconds `u`, floored by integer arithmetic (`%` is truncating, hence
3004
+ * `((u % k) + k) % k`):
3005
+ * - microseconds: the instant itself;
3006
+ * - milliseconds, second: `u` floored, in every zone (an offset is whole seconds);
3007
+ * - minute, hour: in UTC `u` floored; elsewhere `u` less the wall clock's remainder (one zone conversion, the
3008
+ * offset kept);
3009
+ * - day, week (Monday's, ISO: 1970-01-01 was a Thursday), month and up: in UTC only — `u` floored by a day or a
3010
+ * week, or the naive calendar (`make_timestamp(u)`, no zone) truncated as a timestamp is (`truncWall`); in
3011
+ * another zone the wall clock read back is a zone lookup DuckDB's own call makes as cheaply, so null (the caller's
3012
+ * path).
3013
+ * `zoneName` is the zone as a constant (UTC and its aliases: `isUtc`), null for a zone a row holds. ±infinity is
3014
+ * itself (Postgres 14+) and NULL is NULL: the CASE's ELSE. null where the unit is not a constant this handles.
3015
+ */
3016
+ truncInstant(unit, value, zone, zoneName) {
3017
+ // a literal Postgres read as a timestamptz is one to DuckDB too (in the session's zone, as Postgres reads it)
3018
+ const x = this.typeOf(value) === UNKNOWN ? cast(value, ["timestamptz"]) : value;
3019
+ const utc = zoneName !== null && isUtc(zoneName);
3020
+ const DAY = 86400000000n;
3021
+ const k = { milliseconds: 1000n, second: 1000000n, minute: 60000000n, hour: 3600000000n, day: DAY }[unit];
3022
+ const big = (n) => n <= 2147483647n ? intConst(Number(n)) : numConst(String(n));
3023
+ const u = () => fn("epoch_us", [this.clone(x)]);
3024
+ const floorMod = (v, by, plus = 0n) => op("%", op("+", op("%", v(), big(by)), big(by + plus)), big(by));
3025
+ let micros;
3026
+ if (unit === "microseconds")
3027
+ return x;
3028
+ if (k !== undefined && (utc || unit === "milliseconds" || unit === "second"))
3029
+ micros = op("-", u(), floorMod(u, k));
3030
+ else if (k !== undefined && unit !== "day")
3031
+ micros = op("-", u(), floorMod(() => fn("epoch_us", [fn("timezone", [this.clone(zone), this.clone(x)])]), k));
3032
+ else if (!utc)
3033
+ return null;
3034
+ // Monday's: u - ((u + 3 days) mod 7 days), as ((u % 7d) + 10d) % 7d, which no instant overflows
3035
+ else if (unit === "week")
3036
+ micros = op("-", u(), floorMod(u, 7n * DAY, 3n * DAY));
3037
+ else if (["month", "quarter", "year", "decade", "century", "millennium"].includes(unit))
3038
+ micros = fn("epoch_us", [this.truncWall(strConst(unit), fn("make_timestamp", [u()]), TIMESTAMP)]);
3039
+ else
3040
+ return null;
3041
+ return caseWhen([[fn("isfinite", [this.clone(x)]), fn("make_timestamptz", [micros])]], x);
3042
+ }
3043
+ /**
3044
+ * `date_trunc(unit, x)` of a wall clock (a timestamp, or a timestamptz read in the session's zone: `type`), DuckDB's
3045
+ * except where Postgres counts otherwise: its centuries and millennia begin in a year 1 (2001-01-01), DuckDB's in a
3046
+ * year 0 (DT_CENTURY), and a decade before year 1 is counted back as timestamp_trunc's DTK_DECADE counts it.
3047
+ */
3048
+ truncWall(unitNode, x, type) {
3049
+ const unit = (constString(unitNode) ?? "").toLowerCase();
3050
+ if (unit !== "century" && unit !== "millennium" && unit !== "decade")
3051
+ return fn("date_trunc", [unitNode, x]);
3052
+ const y = fn("year", [this.clone(x)]);
3053
+ const first = unit === "decade"
3054
+ ? caseWhen([[op(">", y, intConst(0)), op("*", op("//", this.clone(y), intConst(10)), intConst(10))]], op("*", op("-", intConst(0), op("//", op("-", intConst(8), op("-", this.clone(y), intConst(1))), intConst(10))), intConst(10)))
3055
+ : (() => {
3056
+ const n = unit === "century" ? 100 : 1000;
3057
+ return caseWhen([[op(">", y, intConst(0)), op("-", op("*", op("//", op("+", this.clone(y), intConst(n - 1)), intConst(n)), intConst(n)), intConst(n - 1))]], op("+", op("*", op("-", intConst(0), op("//", op("-", intConst(n - 1), op("-", this.clone(y), intConst(1))), intConst(n))), intConst(n)), intConst(1)));
3058
+ })();
3059
+ const truncated = op("+", fn("date_trunc", [strConst("year"), this.clone(x)]), fn("to_years", [cast(op("-", first, this.clone(y)), ["integer"])]));
3060
+ return caseWhen([[fn("isfinite", [this.clone(x)]), truncated]], type === TIMESTAMPTZ ? cast(x, ["timestamptz"]) : cast(x, ["timestamp"]));
3061
+ }
2496
3062
  textMinMax(name, b, args) {
2497
3063
  const arg = args[0];
2498
3064
  // a window's too (`max(t) OVER (…)`): arg_min / arg_max are DuckDB window aggregates as well
@@ -2516,6 +3082,10 @@ class Rewriter {
2516
3082
  withinGroup(name, b) {
2517
3083
  const order = (b.agg_order ?? []);
2518
3084
  const key = order[0] !== undefined ? bodyOf(order[0]).node : undefined;
3085
+ // a jsonb's position in its order, or its most frequent value: DuckDB would pick by its text
3086
+ if (key !== undefined && (this.typeOf(key) === JSONB || this.typeOf(key) === JSONB_ARRAY)) {
3087
+ throw new Unrewritable(`${name}() WITHIN GROUP (ORDER BY a jsonb) is not supported by Mason (Postgres orders jsonb by its own rules)`, "ORDER BY the value … LIMIT 1 OFFSET n in a subquery", "jsonb-compare");
3088
+ }
2519
3089
  const c = key === undefined ? null : this.textCollation(key);
2520
3090
  if (c === null || c.kind === "bytes")
2521
3091
  return null;
@@ -2921,6 +3491,395 @@ class Rewriter {
2921
3491
  return { base: deeper === null ? b.lexpr : deeper.base, steps: [...(deeper === null ? [] : deeper.steps), ...JSON.parse(own)] };
2922
3492
  }
2923
3493
  // --------------------------------------------------------------------------------------------- FROM
3494
+ /**
3495
+ * WHERE THE ORDER OF A JOIN'S ROWS DECIDES THE ANSWER (leftLateral), computed for the whole statement before the walk
3496
+ * (`rewrite`): libpg-query writes a SELECT's keys in its struct's order (fromClause … limitCount, withClause, larg,
3497
+ * rarg) and the walk follows it, so a SELECT's CTEs and arms are met after its FROM, and a CTE another CTE reads is
3498
+ * met before the one that reads it. So every SELECT of the statement is collected with the CTEs it sees (its own
3499
+ * WITH's and those around it, the nearest first), and `limitBounds` runs over them all until no more is bounded.
3500
+ *
3501
+ * `wire`: the statement's own rows are bounded by a row count the Execute sets on the wire (JDBC's setMaxRows or
3502
+ * fetch size: the client reads the first rows and may read no more), which the rewrite never sees in the SQL — its
3503
+ * outermost SELECT is bounded as a SELECT a LIMIT reads is (limitBounds: no aggregate, GROUP BY, DISTINCT, window or
3504
+ * set operation but UNION ALL between, and under an ORDER BY only a LIMIT of its own, which a row count on the wire
3505
+ * is not: DuckDB sorts every row first whichever plan it is given). Unless `wire`, the bounds a wire's row count
3506
+ * would add are computed too (`boundedOnWire`), so the plan says whether such an Execute plans it again.
3507
+ */
3508
+ markBounds(root, wire = false) {
3509
+ this.bounded = this.boundsOf(root, wire);
3510
+ this.boundedOnWire = wire ? null : this.boundsOf(root, true);
3511
+ }
3512
+ boundsOf(root, wire) {
3513
+ const selects = [];
3514
+ const select = (b, outer) => {
3515
+ const scope = new Map(outer);
3516
+ for (const c of (b.withClause?.ctes ?? [])) {
3517
+ const cb = isNode(c) ? bodyOf(c) : null;
3518
+ if (cb !== null && typeof cb.ctename === "string" && isNode(cb.ctequery) && kindOf(cb.ctequery) === "SelectStmt")
3519
+ scope.set(cb.ctename, bodyOf(cb.ctequery));
3520
+ }
3521
+ selects.push({ b, scope });
3522
+ for (const [k, x] of Object.entries(b)) {
3523
+ const arm = armOf(k, x);
3524
+ if (arm !== null)
3525
+ select(bodyOf(arm), scope);
3526
+ else
3527
+ visit(x, scope);
3528
+ }
3529
+ };
3530
+ const visit = (v, scope) => {
3531
+ if (Array.isArray(v)) {
3532
+ for (const x of v)
3533
+ visit(x, scope);
3534
+ return;
3535
+ }
3536
+ if (v === null || typeof v !== "object")
3537
+ return;
3538
+ if (isNode(v) && kindOf(v) === "SelectStmt") {
3539
+ select(bodyOf(v), scope);
3540
+ return;
3541
+ }
3542
+ for (const x of Object.values(isNode(v) ? bodyOf(v) : v))
3543
+ visit(x, scope);
3544
+ };
3545
+ visit(root, new Map());
3546
+ const set = new WeakSet();
3547
+ let count = 0;
3548
+ const bound = (b) => { if (!set.has(b)) {
3549
+ set.add(b);
3550
+ count++;
3551
+ } };
3552
+ if (wire && isNode(root) && kindOf(root) === "SelectStmt")
3553
+ bound(bodyOf(root));
3554
+ let before;
3555
+ do {
3556
+ before = count;
3557
+ for (const s of selects)
3558
+ this.limitBounds(s.b, s.scope, set, bound);
3559
+ } while (count !== before);
3560
+ return set;
3561
+ }
3562
+ /**
3563
+ * One SELECT's part of markBounds. Its FROM's order decides the answer where:
3564
+ * - a LIMIT or an OFFSET keeps the first of its rows — its own, or one around it whose rows are its rows in their
3565
+ * order (`bounded`: the SELECT reads it as a derived table or a CTE, or as an arm of its UNION ALL) — with no
3566
+ * aggregate, GROUP BY, DISTINCT, window or other set operation between, each of which reads every row first and
3567
+ * hands back an order of its own. Under an ORDER BY only a LIMIT of its own bounds (never WITH TIES: Mason ranks
3568
+ * every row for it, src/duck/rewrite.ts withTies), and only when its first key, once the nulls idiom is folded
3569
+ * (nullsKeyFolded), is a column of a table sorted bare (sortsByTableColumn): DuckDB's top N pushes its bound into
3570
+ * that table's scan as a filter, so the rows reaching the lateral are few (measured, docs/DESIGN.md "A LEFT lateral
3571
+ * of a list": 0.1–6 s streamed against 0.4–106 s for the delimiter join over 20 M rows); any other key — the
3572
+ * lateral's own column, an expression, a text under a collation, a citext, a char(n), a jsonb, each sorted by an
3573
+ * expression — is a top N over every row, each row's function calls paid in full;
3574
+ * - an aggregate that reads its rows in order (string_agg, array_agg, json_agg …) has no ORDER BY of its own, or a
3575
+ * window function that reads its frame in order (row_number, lag, first_value …, or such an aggregate) has a
3576
+ * window with no ORDER BY: Postgres hands each base row's members to it in their order (`readsInOrder`).
3577
+ * Its joins, and the derived tables and CTEs its FROM reads, are bounded. An OFFSET alone is bounded for the
3578
+ * answer's sake (Postgres's rows after it) and streams every row it does not skip; a CTE read twice or MATERIALIZED
3579
+ * is computed whole, a call a row. Bounding too much costs time, never an answer (the streamed lateral is Postgres's
3580
+ * rows in Postgres's order); bounding too little keeps DuckDB's delimiter join, whose rows come in an order of its own.
3581
+ */
3582
+ limitBounds(b, scope, bounded, bound) {
3583
+ const keeps = (n) => isNode(n) && !(kindOf(n) === "A_Const" && bodyOf(n).isnull === true);
3584
+ const sort = (b.sortClause ?? []);
3585
+ const inOrder = this.readsInOrder(b);
3586
+ if (!inOrder) {
3587
+ if (!keeps(b.limitCount) && !keeps(b.limitOffset) && !bounded.has(b))
3588
+ return;
3589
+ if (b.limitOption === "LIMIT_OPTION_WITH_TIES")
3590
+ return;
3591
+ // a UNION ALL's arms, each read in turn (a set operation's arm is a SELECT body, unwrapped: walkBody)
3592
+ if (b.op !== undefined && b.op !== "SETOP_NONE") {
3593
+ if (b.op === "SETOP_UNION" && b.all === true && sort.length === 0)
3594
+ for (const arm of [b.larg, b.rarg])
3595
+ if (arm !== null && typeof arm === "object" && !isNode(arm))
3596
+ bound(arm);
3597
+ return;
3598
+ }
3599
+ if (sort.length > 0) {
3600
+ const first = nullsKeyFolded(sort, b)[0];
3601
+ if (!keeps(b.limitCount) || first === undefined || kindOf(first) !== "SortBy" || !this.sortsByTableColumn(bodyOf(first).node, b, scope))
3602
+ return;
3603
+ }
3604
+ if ((Array.isArray(b.groupClause) && b.groupClause.length > 0) || b.havingClause !== undefined || b.distinctClause !== undefined)
3605
+ return;
3606
+ if ((Array.isArray(b.windowClause) && b.windowClause.length > 0) || (b.targetList ?? []).some((t) => this.hasAggregate(t) || hasWindow(t)))
3607
+ return;
3608
+ }
3609
+ else if (b.op !== undefined && b.op !== "SETOP_NONE")
3610
+ return;
3611
+ const visit = (x) => {
3612
+ if (!isNode(x))
3613
+ return;
3614
+ const xb = bodyOf(x);
3615
+ if (kindOf(x) === "JoinExpr") {
3616
+ bound(xb);
3617
+ visit(xb.larg);
3618
+ visit(xb.rarg);
3619
+ return;
3620
+ }
3621
+ if (kindOf(x) === "RangeSubselect" && isNode(xb.subquery)) {
3622
+ bound(bodyOf(xb.subquery));
3623
+ return;
3624
+ }
3625
+ if (kindOf(x) === "RangeVar" && xb.schemaname === undefined) {
3626
+ const cte = scope.get(String(xb.relname));
3627
+ if (cte !== undefined)
3628
+ bound(cte);
3629
+ }
3630
+ };
3631
+ for (const x of (b.fromClause ?? []))
3632
+ visit(x);
3633
+ }
3634
+ /** whether a SELECT's aggregate or window function reads its rows in the order they come (limitBounds): an
3635
+ * order-sensitive aggregate with no ORDER BY of its own (nor DISTINCT, whose values Postgres sorts), or a window
3636
+ * function that reads its frame in order over a window with no ORDER BY; not inside a subquery */
3637
+ readsInOrder(b) {
3638
+ const windows = new Map((b.windowClause ?? []).filter(isNode).map((w) => [String(bodyOf(w).name ?? ""), bodyOf(w)]));
3639
+ const ordered = (w) => {
3640
+ if (w === undefined)
3641
+ return false;
3642
+ if (Array.isArray(w.orderClause) && w.orderClause.length > 0)
3643
+ return true;
3644
+ return typeof w.refname === "string" && ordered(windows.get(w.refname));
3645
+ };
3646
+ const reads = (v) => {
3647
+ if (Array.isArray(v))
3648
+ return v.some(reads);
3649
+ if (v === null || typeof v !== "object")
3650
+ return false;
3651
+ if (!isNode(v))
3652
+ return Object.values(v).some(reads);
3653
+ if (kindOf(v) === "SubLink")
3654
+ return false;
3655
+ if (kindOf(v) === "FuncCall") {
3656
+ const c = bodyOf(v), name = lastName(c.funcname);
3657
+ const aggregate = IN_ORDER_AGGREGATES.has(name) && c.agg_order === undefined && c.agg_distinct !== true;
3658
+ if (c.over === undefined ? aggregate : (aggregate || IN_ORDER_WINDOWS.has(name)) && !ordered(c.over))
3659
+ return true;
3660
+ }
3661
+ return Object.values(bodyOf(v)).some(reads);
3662
+ };
3663
+ return reads(b.targetList) || reads(b.havingClause);
3664
+ }
3665
+ /** whether an ORDER BY key is a column of a table sorted as it is stored (limitBounds): an ordinal or an output
3666
+ * name is its target's expression, and a derived table's or a CTE's column is followed to what it selects */
3667
+ sortsByTableColumn(key, b, scope) {
3668
+ const targets = (b.targetList ?? []);
3669
+ const n = constInt(key);
3670
+ if (n !== null)
3671
+ return n >= 1 && n <= targets.length && this.tableColumn(bodyOf(targets[n - 1]).val, b, scope, 0);
3672
+ const f = kindOf(key) === "ColumnRef" ? names(bodyOf(key).fields) : [];
3673
+ const named = f.length === 1 ? targets.filter((t) => bodyOf(t).name === f[0]) : [];
3674
+ return this.tableColumn(named.length === 1 ? bodyOf(named[0]).val : key, b, scope, 0);
3675
+ }
3676
+ /** a column of a table, or of a derived table or CTE that selects one, sorted bare: no collation's keys, citext's
3677
+ * lower(), char(n)'s rtrim() or jsonb's order key (sortBy), each an expression no top N filter reaches the scan by.
3678
+ * (A CTE is looked up among those the SELECT sees; a derived table's own WITH is not followed through its `*`.) */
3679
+ tableColumn(e, select, scope, depth) {
3680
+ if (!isNode(e) || kindOf(e) !== "ColumnRef" || depth > 8)
3681
+ return false;
3682
+ const t = this.typed(e);
3683
+ if (t !== null) {
3684
+ if (t.node !== "VAR" || t.functionColumn !== undefined || (t.levelsUp ?? 0) !== 0)
3685
+ return false;
3686
+ if (t.type === JSONB || t.type === BPCHAR || t.type === INET || this.isCitext(t.type))
3687
+ return false;
3688
+ const c = this.textCollation(e);
3689
+ if (c !== null && c.kind !== "bytes")
3690
+ return false;
3691
+ if (t.relation !== undefined)
3692
+ return true;
3693
+ }
3694
+ // a derived table's or a CTE's column: what it selects under that name, or a column of the one table its * reads
3695
+ const f = names(bodyOf(e).fields);
3696
+ const col = f.at(-1), alias = f.length === 2 ? f[0] : undefined;
3697
+ const sources = [];
3698
+ const from = (x) => {
3699
+ if (!isNode(x))
3700
+ return;
3701
+ const xb = bodyOf(x);
3702
+ if (kindOf(x) === "JoinExpr") {
3703
+ from(xb.larg);
3704
+ from(xb.rarg);
3705
+ return;
3706
+ }
3707
+ if (kindOf(x) === "RangeSubselect" && typeof xb.alias?.aliasname === "string" && isNode(xb.subquery) && xb.alias?.colnames === undefined)
3708
+ sources.push({ alias: xb.alias.aliasname, query: bodyOf(xb.subquery) });
3709
+ if (kindOf(x) === "RangeVar" && xb.schemaname === undefined && xb.alias?.colnames === undefined) {
3710
+ const cte = scope.get(String(xb.relname));
3711
+ if (cte !== undefined)
3712
+ sources.push({ alias: xb.alias?.aliasname ?? String(xb.relname), query: cte });
3713
+ }
3714
+ };
3715
+ for (const x of (select.fromClause ?? []))
3716
+ from(x);
3717
+ const matching = sources.filter((s) => alias === undefined || s.alias === alias);
3718
+ if (col === undefined || matching.length !== 1)
3719
+ return false;
3720
+ const q = matching[0].query;
3721
+ if (q.op !== undefined && q.op !== "SETOP_NONE")
3722
+ return false;
3723
+ const qt = (q.targetList ?? []);
3724
+ const hits = qt.filter((x) => isNode(bodyOf(x).val) && !hasStar(bodyOf(x).val) && (bodyOf(x).name ?? figureColname(bodyOf(x).val)) === col);
3725
+ if (hits.length === 1)
3726
+ return this.tableColumn(bodyOf(hits[0]).val, q, scope, depth + 1);
3727
+ const qf = (q.fromClause ?? []);
3728
+ return t !== null && hits.length === 0 && qt.some((x) => hasStar(bodyOf(x).val)) && qf.length === 1 && kindOf(qf[0]) === "RangeVar" && !scope.has(String(bodyOf(qf[0]).relname));
3729
+ }
3730
+ /**
3731
+ * A LEFT JOIN LATERAL OF A LIST'S ROWS ON TRUE (docs/DESIGN.md "A LEFT lateral of a list"): `FROM base LEFT JOIN
3732
+ * json_each(base.doc) d ON TRUE` is every base row with each of its members, or once with NULLs where it has none.
3733
+ * DuckDB plans a LEFT lateral as a delimiter join — the whole base grouped, the function called for every distinct
3734
+ * argument, a hash join back — so a LIMIT over it reads every row (a BI tool's "expand the JSON" preview: 37.9 s over
3735
+ * 22 M rows), and the join hands its rows back in an order of its own, so the LIMIT kept OTHER rows than Postgres's
3736
+ * (Postgres walks the base and each row's members in order). Written as an INNER lateral over a list that is never
3737
+ * empty — the members, or one NULL (`orNull`) — it is the same rows, and DuckDB runs it as a streaming UNNEST: in the
3738
+ * base's order, the members in theirs, stopping at the LIMIT. But it calls the function once per ROW where the
3739
+ * delimiter join calls it once per DISTINCT argument, so where every row is read anyway — a count, a GROUP BY, a
3740
+ * DISTINCT, a window, a whole result, a sort by the lateral's own column — it is the slower plan (a count over 22 M
3741
+ * rows of few distinct documents: 79 s joined, over 200 s streamed; 2.2 s against 62.5 s over 20 M rows of 2,000).
3742
+ * So it is written only where the order of the join's rows decides the answer — a LIMIT keeps the first of them, or
3743
+ * an aggregate or a window reads them in order (`bounded`: markBounds, limitBounds) — and only for a
3744
+ * join that is LEFT, ON a literal TRUE, its right side one function of a list Mason writes (the json rows, an array's
3745
+ * unnest: a column's, or an expression's, computed once), without ORDINALITY. A RIGHT or FULL join, another
3746
+ * condition, or another function keeps DuckDB's own plan. Such a join a row count on the wire alone would bound
3747
+ * (`boundedOnWire`) is left DuckDB's and said (`unboundedLateral`): the listener plans the statement again, bounded,
3748
+ * for an Execute that sets one.
3749
+ */
3750
+ leftLateral(b) {
3751
+ const bounded = this.bounded.has(b);
3752
+ if (!bounded && this.boundedOnWire?.has(b) !== true)
3753
+ return null;
3754
+ const quals = b.quals;
3755
+ if (b.jointype !== "JOIN_LEFT" || b.isNatural === true || b.usingClause !== undefined || quals === undefined || kindOf(quals) !== "A_Const" || bodyOf(quals).boolval?.boolval !== true)
3756
+ return null;
3757
+ const rarg = b.rarg;
3758
+ if (rarg === undefined || kindOf(rarg) !== "RangeFunction")
3759
+ return null;
3760
+ const r = bodyOf(rarg);
3761
+ const items = (r.functions ?? []);
3762
+ const calls = items.length === 1 ? (bodyOf(items[0]).items ?? []) : [];
3763
+ const call = calls[0];
3764
+ if (r.ordinality === true || r.is_rowsfrom === true || r.coldeflist !== undefined || call === undefined || kindOf(call) !== "FuncCall")
3765
+ return null;
3766
+ const c = bodyOf(call);
3767
+ const args = (c.args ?? []);
3768
+ const name = lastName(c.funcname);
3769
+ if (args.length !== 1 || c.agg_star === true || c.func_variadic === true || kindOf(args[0]) === "NamedArgExpr")
3770
+ return null;
3771
+ if (!LIST_ROWS.has(name) && name !== "unnest")
3772
+ return null;
3773
+ if (!bounded) {
3774
+ this.unboundedLateral = true;
3775
+ return null;
3776
+ }
3777
+ this.nonEmpty.add(r);
3778
+ const { quals: _true, ...rest } = b;
3779
+ // DuckDB 2.0 streams the INNER lateral only where its list reads ONE column of the row, or an array column
3780
+ // (measured, docs/DESIGN.md "A LEFT lateral of a list"): one that reads two or more is its delimiter join again,
3781
+ // the rows in an order of its own. Such an argument is computed by a step of its own before the lateral, `CROSS
3782
+ // JOIN LATERAL (SELECT <arg> AS __mason_arg<n>) AS __mason_arg<n>` (a name no column of the query has) — a
3783
+ // projection under the same UNNEST — and the lateral reads the step's one column; so is an unnest's expression of
3784
+ // any columns, whose never-empty CASE then names the step's column twice, at no cost but the expression's. Not where the argument reads an array (DuckDB plans that
3785
+ // step as a delimiter join, and the lateral streams without it: an unnest's expression is computed once inside,
3786
+ // rangeFunction's list_transform), nor where a `*` or a join's name reads every column of the join (the step's
3787
+ // would be one of them), nor for a subquery (a delimiter join in every form): `limit-lateral-argument`, decided
3788
+ const arg = args[0];
3789
+ if (kindOf(arg) === "ColumnRef" || this.readWhole.has(b))
3790
+ return { JoinExpr: { ...rest, jointype: "JOIN_INNER" } };
3791
+ const reads = this.rowReads(arg);
3792
+ if (reads === null || (reads < 2 && name !== "unnest"))
3793
+ return { JoinExpr: { ...rest, jointype: "JOIN_INNER" } };
3794
+ const alias = `__mason_arg${++this.steps}`;
3795
+ const sub = { targetList: [{ ResTarget: { name: alias, val: arg } }], op: "SETOP_NONE" };
3796
+ this.done.add(sub);
3797
+ this.stepArg.set(r, { ColumnRef: { fields: [S(alias), S(alias)] } });
3798
+ const step = { RangeSubselect: { lateral: true, subquery: { SelectStmt: sub }, alias: { aliasname: alias } } };
3799
+ return { JoinExpr: { ...rest, jointype: "JOIN_INNER", larg: { JoinExpr: { jointype: "JOIN_INNER", larg: b.larg, rarg: step } } } };
3800
+ }
3801
+ /** how many columns of the row an argument reads (each named once), or null where it reads an array, a whole row,
3802
+ * a column whose type Postgres did not say, or holds a subquery: a step before the lateral is no help there
3803
+ * (leftLateral) */
3804
+ rowReads(arg) {
3805
+ const seen = new Set();
3806
+ let plain = true;
3807
+ const visit = (v) => {
3808
+ if (!plain)
3809
+ return;
3810
+ if (Array.isArray(v)) {
3811
+ for (const x of v)
3812
+ visit(x);
3813
+ return;
3814
+ }
3815
+ if (v === null || typeof v !== "object")
3816
+ return;
3817
+ if (isNode(v)) {
3818
+ if (kindOf(v) === "SubLink") {
3819
+ plain = false;
3820
+ return;
3821
+ }
3822
+ if (kindOf(v) === "ColumnRef") {
3823
+ const t = this.typed(v)?.type;
3824
+ // an array (a list to DuckDB, whose step it plans as its join) or a row (a struct) reads as no column a
3825
+ // step could hold; a user type's array is named `_<type>` in pg_type. A scalar user type (an enum, a
3826
+ // domain, citext) is a column like any other
3827
+ if (t === undefined || ARRAYS.has(t) || this.enums.arrays.has(t) || this.isRowType(t) || this.userTypes.get(t)?.startsWith("_") === true || t === INET_ARRAY) {
3828
+ plain = false;
3829
+ return;
3830
+ }
3831
+ seen.add(JSON.stringify(names(bodyOf(v).fields)));
3832
+ return;
3833
+ }
3834
+ }
3835
+ for (const x of Object.values(isNode(v) ? bodyOf(v) : v))
3836
+ visit(x);
3837
+ };
3838
+ visit(arg);
3839
+ return plain ? seen.size : null;
3840
+ }
3841
+ /**
3842
+ * The joins whose every column the answer reads (leftLateral): those in the FROM of a SELECT whose list holds a bare
3843
+ * `*`, and those inside a join that has a name of its own (`(a JOIN b) AS j`, read whole by `j.*` or `j`). A column a
3844
+ * step added to one would be a column of the answer that Postgres's has not.
3845
+ */
3846
+ markWholeReads(root) {
3847
+ const joins = (x) => {
3848
+ if (!isNode(x) || kindOf(x) !== "JoinExpr")
3849
+ return;
3850
+ const j = bodyOf(x);
3851
+ this.readWhole.add(j);
3852
+ joins(j.larg);
3853
+ joins(j.rarg);
3854
+ };
3855
+ const bareStar = (t) => {
3856
+ const v = isNode(t) ? bodyOf(t).val : undefined;
3857
+ if (!isNode(v) || kindOf(v) !== "ColumnRef")
3858
+ return false;
3859
+ const fields = (bodyOf(v).fields ?? []);
3860
+ return fields.length === 1 && kindOf(fields[0]) === "A_Star";
3861
+ };
3862
+ const visit = (v) => {
3863
+ if (Array.isArray(v)) {
3864
+ for (const x of v)
3865
+ visit(x);
3866
+ return;
3867
+ }
3868
+ if (v === null || typeof v !== "object")
3869
+ return;
3870
+ if (isNode(v)) {
3871
+ const b = bodyOf(v);
3872
+ if (kindOf(v) === "SelectStmt" && (b.targetList ?? []).some(bareStar))
3873
+ for (const f of (b.fromClause ?? []))
3874
+ joins(f);
3875
+ if (kindOf(v) === "JoinExpr" && b.alias !== undefined)
3876
+ joins(v);
3877
+ }
3878
+ for (const [k, x] of Object.entries(isNode(v) ? bodyOf(v) : v))
3879
+ visit(armOf(k, x) ?? x);
3880
+ };
3881
+ visit(root);
3882
+ }
2924
3883
  rangeFunction(b) {
2925
3884
  const items = (b.functions ?? []);
2926
3885
  const first = items[0] !== undefined ? bodyOf(items[0]).items?.[0] : undefined;
@@ -2928,7 +3887,9 @@ class Rewriter {
2928
3887
  return null;
2929
3888
  const call = bodyOf(first);
2930
3889
  const name = lastName(call.funcname);
2931
- const args = (call.args ?? []);
3890
+ // a LEFT lateral's argument computed by a step of its own before it (leftLateral): its column, in its place
3891
+ const stepped = this.stepArg.get(b);
3892
+ const args = stepped !== undefined ? [stepped] : (call.args ?? []);
2932
3893
  const alias = b.alias;
2933
3894
  const aliasName = alias?.aliasname;
2934
3895
  const colnames = names(alias?.colnames);
@@ -2937,6 +3898,8 @@ class Rewriter {
2937
3898
  if (isNode(f) && kindOf(f) === "FuncCall")
2938
3899
  this.inFrom.add(bodyOf(f));
2939
3900
  const canonical = name.startsWith("jsonb");
3901
+ // the right side of a LEFT JOIN … ON TRUE whose rows are a list: written never empty (leftLateral)
3902
+ const orNull = this.nonEmpty.has(b);
2940
3903
  switch (name) {
2941
3904
  // jsonpath's rows, as Postgres finds them (src/duck/pg-eval.ts), unnested
2942
3905
  case "jsonb_path_query": {
@@ -2956,11 +3919,14 @@ class Rewriter {
2956
3919
  if (b.ordinality === true)
2957
3920
  throw new Unrewritable(`${name}() WITH ORDINALITY is not supported by Mason`, "number the elements with row_number() OVER () instead", "with-ordinality");
2958
3921
  const text = name.endsWith("_text");
2959
- const elements = fn("__mason_json_elements", [jsonArg(args[0]), boolConst(canonical), boolConst(text)]);
3922
+ const elements = fn("__mason_json_elements", [jsonArg(args[0]), boolConst(canonical), boolConst(text), boolConst(orNull)]);
2960
3923
  const list = text ? elements : arrayCast(elements, "json");
2961
- // a scalar function's column is named by its alias (or `value`), and so is DuckDB's unnest column here
2962
- const column = colnames[0] ?? aliasName ?? "value";
3924
+ // its column is its OUT parameter's, `value`, under a FROM name too (`e.value`), unless the alias names it;
3925
+ // the FROM name alone is the element (the ColumnRef rule: Typed.functionColumn)
3926
+ const column = colnames[0] ?? "value";
2963
3927
  const unnest = fn("unnest", [list]);
3928
+ if (orNull)
3929
+ return listLateral([{ name: column, list }], aliasName ?? name, this.done);
2964
3930
  const item = bodyOf(items[0]);
2965
3931
  return { RangeFunction: { ...b, functions: [{ List: { ...item, items: [unnest, ...item.items.slice(1)] } }], alias: { aliasname: aliasName ?? name, colnames: [S(column)] } } };
2966
3932
  }
@@ -3007,32 +3973,141 @@ class Rewriter {
3007
3973
  throw new Unrewritable(`${name}() WITH ORDINALITY is not supported by Mason`, "number the members with row_number() OVER () instead", "with-ordinality");
3008
3974
  const text = name.endsWith("_text");
3009
3975
  // each member's key and value, two lists of one length that unnest side by side
3010
- const sub = {
3011
- targetList: [
3012
- { ResTarget: { name: colnames[0] ?? "key", val: fn("unnest", [fn("__mason_json_keys", [jsonArg(args[0]), boolConst(canonical)])]) } },
3013
- { ResTarget: { name: colnames[1] ?? "value", val: fn("unnest", [fn("__mason_json_values", [this.clone(args[0]), boolConst(canonical), boolConst(text)])]) } },
3014
- ],
3015
- op: "SETOP_NONE",
3016
- };
3017
- this.done.add(sub);
3018
- return { RangeSubselect: { lateral: true, subquery: { SelectStmt: sub }, alias: { aliasname: aliasName ?? name } } };
3976
+ return listLateral([
3977
+ { name: colnames[0] ?? "key", list: fn("__mason_json_keys", [jsonArg(args[0]), boolConst(canonical), boolConst(orNull)]) },
3978
+ { name: colnames[1] ?? "value", list: fn("__mason_json_values", [this.clone(args[0]), boolConst(canonical), boolConst(text), boolConst(orNull)]) },
3979
+ ], aliasName ?? name, this.done);
3019
3980
  }
3020
3981
  case "jsonb_object_keys":
3021
3982
  case "json_object_keys": {
3022
3983
  const column = colnames[0] ?? aliasName ?? name;
3023
- const unnest = fn("unnest", [fn("__mason_json_keys", [args[0], boolConst(canonical)])]);
3984
+ const keys = fn("__mason_json_keys", [args[0], boolConst(canonical), boolConst(orNull)]);
3985
+ if (orNull)
3986
+ return listLateral([{ name: column, list: keys }], aliasName ?? name, this.done);
3024
3987
  const item = bodyOf(items[0]);
3025
- return { RangeFunction: { ...b, functions: [{ List: { ...item, items: [unnest, ...item.items.slice(1)] } }], alias: { aliasname: aliasName ?? name, colnames: [S(column)] } } };
3988
+ return { RangeFunction: { ...b, functions: [{ List: { ...item, items: [fn("unnest", [keys]), ...item.items.slice(1)] } }], alias: { aliasname: aliasName ?? name, colnames: [S(column)] } } };
3989
+ }
3990
+ // an array's elements, never empty: its list where it has one, else one NULL (leftLateral). The CASE names the
3991
+ // list twice: a column's (or the column of the step leftLateral computed the expression in) costs nothing
3992
+ // twice; an expression left here (it reads an array, holds a subquery, or a `*` reads the join) would be
3993
+ // computed twice (a call of Mason's string_to_array a row more), so it is computed ONCE, the one element of a
3994
+ // list whose lambda names it twice: `list_transform(list_value(<expr>), __mason_l -> CASE … __mason_l …
3995
+ // END)[1]`, a copy of its list a row more, the same streaming UNNEST (measured: docs/DESIGN.md "A LEFT lateral
3996
+ // of a list")
3997
+ case "unnest": {
3998
+ if (!orNull)
3999
+ return null;
4000
+ const list = args[0];
4001
+ const column = colnames[0] ?? aliasName ?? name;
4002
+ const never = (l) => caseWhen([[op(">", fn("len", [l()]), intConst(0)), l()]], arrayOf([nullConst()]));
4003
+ const each = () => ({ ColumnRef: { fields: [S("__mason_l")] } });
4004
+ const once = kindOf(list) === "ColumnRef" ? never(() => this.clone(list))
4005
+ : { A_Indirection: { arg: fn("list_transform", [fn("list_value", [list]), op("->", each(), never(each))]), indirection: [{ A_Indices: { uidx: intConst(1) } }] } };
4006
+ return listLateral([{ name: column, list: once }], aliasName ?? name, this.done);
3026
4007
  }
3027
4008
  default: return null;
3028
4009
  }
3029
4010
  }
3030
4011
  }
4012
+ /**
4013
+ * THE NULLS-PLACEMENT IDIOM AS ONE KEY (docs/DESIGN.md "Nulls placed by a key of their own"): `ORDER BY x IS NULL,
4014
+ * x DESC` — nulls last, written without NULLS LAST, as SQL that must also run where NULLS LAST is not grammar — orders
4015
+ * exactly as `ORDER BY x DESC NULLS LAST`. DuckDB's top N cannot push its threshold into the scan of an expression key,
4016
+ * so it read every row of a 197 M-row copy for a 100-row page (6.4 s by a timestamp; 91 s by a text id, cut off by
4017
+ * the tool); one plain key with its NULLS placement is pruned by the zone maps. So a pair of CONSECUTIVE keys, the
4018
+ * first `<e> IS [NOT] NULL` (either direction, no USING) and the second the same `<e>` (the same tree, parentheses
4019
+ * being no node of it), is one key on `<e>` with the second's direction and the nulls where the first put them: `IS
4020
+ * NULL ASC` and `IS NOT NULL DESC` last, `IS NULL DESC` and `IS NOT NULL ASC` first. Every other key stays in place.
4021
+ * Not under a DISTINCT ON, whose expressions must lead the ORDER BY as written, and never a window's ORDER BY.
4022
+ */
4023
+ function nullsKeyFolded(keys, select) {
4024
+ const distinctOn = Array.isArray(select.distinctClause) && select.distinctClause.some((d) => isNode(d));
4025
+ if (distinctOn || keys.length < 2)
4026
+ return keys;
4027
+ const out = [];
4028
+ for (let i = 0; i < keys.length; i++) {
4029
+ const first = keys[i], second = keys[i + 1];
4030
+ const a = kindOf(first) === "SortBy" ? bodyOf(first) : null, b = second !== undefined && kindOf(second) === "SortBy" ? bodyOf(second) : null;
4031
+ const test = a !== null && isNode(a.node) && kindOf(a.node) === "NullTest" ? bodyOf(a.node) : null;
4032
+ if (a === null || b === null || test === null || a.useOp !== undefined || b.useOp !== undefined || !isNode(test.arg) || !alikeTree(test.arg, b.node)
4033
+ || !bareKeyIsInput(test.arg, b.node, select)) {
4034
+ out.push(first);
4035
+ continue;
4036
+ }
4037
+ const last = (test.nulltesttype === "IS_NULL") === (a.sortby_dir !== "SORTBY_DESC");
4038
+ out.push({ SortBy: { ...b, sortby_nulls: last ? "SORTBY_NULLS_LAST" : "SORTBY_NULLS_FIRST" } });
4039
+ i++;
4040
+ }
4041
+ return out;
4042
+ }
4043
+ /**
4044
+ * Whether an ORDER BY key read BARE is the same value as the same tree read inside an expression (the `x` of `x IS
4045
+ * NULL`), where Postgres reads them by two rules (findTargetlistEntrySQL92, parse_clause.c): inside an expression a name
4046
+ * is always an input column, while a bare key that is an integer constant is an output column by its position (`ORDER
4047
+ * BY 2`), and a bare one-part name that is an output column's name is that output column (`SELECT coalesce(x, 0) AS x
4048
+ * … ORDER BY x`). Every other node is read alike either way. So a constant is never, and a one-part name is only
4049
+ * where no output column may bear its name or every one that may is the same tree as the input's (`SELECT x … ORDER BY
4050
+ * x`). An output column's name is its alias, else Postgres's FigureColname (a CASE's may be its ELSE's).
4051
+ */
4052
+ function bareKeyIsInput(inner, bare, select) {
4053
+ if (kindOf(bare) === "A_Const")
4054
+ return false;
4055
+ if (kindOf(bare) !== "ColumnRef")
4056
+ return true;
4057
+ const fields = (bodyOf(bare).fields ?? []);
4058
+ if (fields.length !== 1 || kindOf(fields[0]) !== "String")
4059
+ return true;
4060
+ const name = String(bodyOf(fields[0]).sval);
4061
+ return (select.targetList ?? []).every((t) => {
4062
+ const r = bodyOf(t), val = r.val;
4063
+ if (val === undefined)
4064
+ return true;
4065
+ // `*` and `t.*` are the input columns themselves
4066
+ const last = kindOf(val) === "ColumnRef" ? (bodyOf(val).fields ?? []).at(-1) : undefined;
4067
+ if (last !== undefined && kindOf(last) === "A_Star")
4068
+ return true;
4069
+ // every target is named by now: its alias, else Postgres's name for it (nameTargets, figureColname)
4070
+ return (typeof r.name === "string" && r.name !== name) || alikeTree(val, inner);
4071
+ });
4072
+ }
4073
+ /** two trees alike but for where their text stood (`location`): `(x)` and `x` are one tree, parentheses no node */
4074
+ function alikeTree(a, b) {
4075
+ if (Array.isArray(a))
4076
+ return Array.isArray(b) && a.length === b.length && a.every((x, i) => alikeTree(x, b[i]));
4077
+ if (a === null || typeof a !== "object")
4078
+ return a === b;
4079
+ if (b === null || typeof b !== "object" || Array.isArray(b))
4080
+ return false;
4081
+ const ka = Object.keys(a).filter((k) => k !== "location"), kb = Object.keys(b).filter((k) => k !== "location");
4082
+ return ka.length === kb.length && ka.every((k) => alikeTree(a[k], b[k]));
4083
+ }
4084
+ /** the json functions whose rows Mason writes as lists in FROM (src/duck/rewrite.ts rangeFunction), each with an
4085
+ * `orNull` form (src/duck/functions.ts) */
4086
+ const LIST_ROWS = new Set(["jsonb_each", "json_each", "jsonb_each_text", "json_each_text", "jsonb_array_elements", "json_array_elements",
4087
+ "jsonb_array_elements_text", "json_array_elements_text", "jsonb_object_keys", "json_object_keys"]);
4088
+ /** `LATERAL (SELECT unnest(<list>) AS <name>, …) AS <alias>`: lists of one length unnested side by side, a subquery
4089
+ * DuckDB runs as its UNNEST operator when the join is INNER (leftLateral) */
4090
+ function listLateral(columns, alias, done) {
4091
+ const sub = { targetList: columns.map((c) => ({ ResTarget: { name: c.name, val: fn("unnest", [c.list]) } })), op: "SETOP_NONE" };
4092
+ done.add(sub);
4093
+ return { RangeSubselect: { lateral: true, subquery: { SelectStmt: sub }, alias: { aliasname: alias } } };
4094
+ }
3031
4095
  /** a cell an exact program cannot take (in a grouped SELECT, a column not grouped): the column stays DuckDB's */
3032
4096
  const NOT_EXACT = new Error("not exact");
3033
4097
  /** a SELECT list item that is `*` or `t.*` */
3034
4098
  /** a set operation's arm (libpg-query's `larg` / `rarg`: a SelectStmt's body, unwrapped), as the node it is */
3035
4099
  const armOf = (key, v) => (key === "larg" || key === "rarg") && v !== null && typeof v === "object" && !Array.isArray(v) && !isNode(v) ? { SelectStmt: v } : null;
4100
+ /** the aggregates whose answer follows the order their rows come in, without an ORDER BY of their own (readsInOrder) */
4101
+ const IN_ORDER_AGGREGATES = new Set(["string_agg", "array_agg", "json_agg", "jsonb_agg", "json_agg_strict", "jsonb_agg_strict", "json_object_agg",
4102
+ "jsonb_object_agg", "json_object_agg_strict", "jsonb_object_agg_strict", "json_object_agg_unique", "jsonb_object_agg_unique",
4103
+ "json_object_agg_unique_strict", "jsonb_object_agg_unique_strict", "xmlagg"]);
4104
+ /** the window functions that read their frame in order: over a window with no ORDER BY, the order the rows come in */
4105
+ const IN_ORDER_WINDOWS = new Set(["row_number", "ntile", "lag", "lead", "first_value", "last_value", "nth_value"]);
4106
+ /** whether an expression holds a window function (outside a subquery) */
4107
+ const hasWindow = (v) => Array.isArray(v) ? v.some(hasWindow)
4108
+ : v === null || typeof v !== "object" ? false
4109
+ : isNode(v) ? kindOf(v) !== "SubLink" && ((kindOf(v) === "FuncCall" && bodyOf(v).over !== undefined) || Object.values(bodyOf(v)).some(hasWindow))
4110
+ : Object.values(v).some(hasWindow);
3036
4111
  const hasStar = (v) => isNode(v) && kindOf(v) === "ColumnRef" && bodyOf(v).fields.some((f) => kindOf(f) === "A_Star");
3037
4112
  /** whether a program does what DuckDB would compute otherwise: a division, a rounding, a scale chosen per value; a
3038
4113
  * sum, a difference or a product of DuckDB's decimals is DuckDB's own right answer */
@@ -3640,6 +4715,24 @@ function replacement(r) {
3640
4715
  return strConst(s.replace(/\\(.)/g, (_m, c) => c === "&" ? "\\0" : `\\${c}`));
3641
4716
  }
3642
4717
  /** substring(text FROM pattern): the first parenthesised part when there is one, else the whole match; NULL without one */
4718
+ /**
4719
+ * `current_setting(name[, missing_ok])` BESIDE A COPIED TABLE IS THE SESSION'S (GitHub issue 40): the name read, in any
4720
+ * case, from the settings the session's statement runs under (`SETTINGS_VARIABLE`: src/api/postgres/server.ts
4721
+ * `duckSession`, the listener's `sessionSetting` laid over the catalog Postgres's own, set by src/duck/store.ts
4722
+ * `settle`) — never DuckDB's own `current_setting`, which answered DuckDB's settings (`search_path` its schemas,
4723
+ * `work_mem` an error). A name neither holds is Postgres's 42704, or NULL under missing_ok; strict, as Postgres's.
4724
+ */
4725
+ function settingRead(copy, args) {
4726
+ const [name, missingOk] = args;
4727
+ const settings = () => fn("getvariable", [strConst(SETTINGS_VARIABLE)]);
4728
+ const key = () => fn("lower", [copy(name)]);
4729
+ const unknown = fn("error", [concat(strConst('mason[42704]: unrecognized configuration parameter "'), copy(name), strConst('"'))]);
4730
+ return caseWhen([
4731
+ [missingOk === undefined ? isNull(name) : or([isNull(name), isNull(missingOk)]), nullConst()],
4732
+ [fn("map_contains", [settings(), key()]), fn("map_extract_value", [settings(), key()])],
4733
+ ...(missingOk === undefined ? [] : [[copy(missingOk), nullConst()]]),
4734
+ ], unknown);
4735
+ }
3643
4736
  function regexSubstring(copy, s, p) {
3644
4737
  const pattern = regexPattern(p, true);
3645
4738
  const text = constString(p);
@@ -3749,29 +4842,51 @@ function strptimeFormat(fmt) {
3749
4842
  * …) s`). Every unnamed target is given this name (`nameTargets`).
3750
4843
  */
3751
4844
  export function figureColname(n) {
4845
+ const [name, strength] = colnameOf(n);
4846
+ return strength === 0 ? "?column?" : name;
4847
+ }
4848
+ /** FigureColnameInternal: the name and its strength — 2 a name of the expression's own (a column's, a function's), 1 a
4849
+ * kind's (`case`, a cast's type), 0 none. A CASE is named by its ELSE where that name is strong (`CASE … ELSE n END`
4850
+ * is `n`), and a cast by its argument where that one's is, else by its type */
4851
+ function colnameOf(n) {
3752
4852
  const k = kindOf(n), b = bodyOf(n);
3753
4853
  switch (k) {
3754
- case "ColumnRef": {
3755
- const f = (b.fields ?? []);
3756
- const last = f[f.length - 1];
3757
- return last !== undefined && kindOf(last) === "String" ? String(bodyOf(last).sval) : "?column?";
4854
+ case "TypeCast": {
4855
+ const inner = isNode(b.arg) ? colnameOf(b.arg) : ["?column?", 0];
4856
+ return inner[1] > 1 ? inner : [lastName(b.typeName?.names), 1];
4857
+ }
4858
+ case "CollateClause": return isNode(b.arg) ? colnameOf(b.arg) : ["?column?", 0];
4859
+ case "CaseExpr": {
4860
+ const d = isNode(b.defresult) ? colnameOf(b.defresult) : ["?column?", 0];
4861
+ return d[1] > 1 ? d : ["case", 1];
3758
4862
  }
4863
+ case "A_Expr": return b.kind === "AEXPR_NULLIF" ? ["nullif", 2] : ["?column?", 0];
3759
4864
  case "A_Indirection": {
3760
4865
  const ind = (b.indirection ?? []);
3761
4866
  for (let i = ind.length - 1; i >= 0; i--) {
3762
4867
  if (kindOf(ind[i]) === "String")
3763
- return String(bodyOf(ind[i]).sval);
4868
+ return [String(bodyOf(ind[i]).sval), 2];
3764
4869
  if (kindOf(ind[i]) === "A_Star")
3765
4870
  break;
3766
4871
  }
3767
- return isNode(b.arg) ? figureColname(b.arg) : "?column?";
4872
+ return isNode(b.arg) ? colnameOf(b.arg) : ["?column?", 0];
3768
4873
  }
3769
- case "FuncCall": return lastName(b.funcname);
3770
- case "TypeCast": {
3771
- const inner = isNode(b.arg) ? figureColname(b.arg) : "?column?";
3772
- return inner !== "?column?" ? inner : lastName(b.typeName?.names);
4874
+ default: {
4875
+ const name = strongColname(n);
4876
+ return name === "?column?" ? [name, 0] : [name, 2];
4877
+ }
4878
+ }
4879
+ }
4880
+ /** the kinds whose name is their own (strength 2), or none */
4881
+ function strongColname(n) {
4882
+ const k = kindOf(n), b = bodyOf(n);
4883
+ switch (k) {
4884
+ case "ColumnRef": {
4885
+ const f = (b.fields ?? []);
4886
+ const last = f[f.length - 1];
4887
+ return last !== undefined && kindOf(last) === "String" ? String(bodyOf(last).sval) : "?column?";
3773
4888
  }
3774
- case "CollateClause": return isNode(b.arg) ? figureColname(b.arg) : "?column?";
4889
+ case "FuncCall": return lastName(b.funcname);
3775
4890
  case "GroupingFunc": return "grouping";
3776
4891
  case "SubLink": {
3777
4892
  if (b.subLinkType === "EXISTS_SUBLINK")
@@ -3783,7 +4898,6 @@ export function figureColname(n) {
3783
4898
  const first = (bodyOf(b.subselect).targetList ?? [])[0];
3784
4899
  return first !== undefined ? (bodyOf(first).name ?? (isNode(bodyOf(first).val) ? figureColname(bodyOf(first).val) : "?column?")) : "?column?";
3785
4900
  }
3786
- case "CaseExpr": return "case";
3787
4901
  case "A_ArrayExpr": return "array";
3788
4902
  case "RowExpr": return "row";
3789
4903
  case "CoalesceExpr": return "coalesce";
@@ -3823,16 +4937,22 @@ function nameTargets(v) {
3823
4937
  for (const [k, x] of Object.entries(b))
3824
4938
  nameTargets(armOf(k, x) ?? x);
3825
4939
  }
3826
- export function rewrite(stmt, base, analysis, collation, userTypes = new Map(), rowTypes = new Map(), timeZone = "UTC", enums = { labels: new Map(), arrays: new Map() }, kept = null) {
3827
- const r = new Rewriter(analysis, base, collation, userTypes, rowTypes, timeZone, enums, kept);
4940
+ export function rewrite(stmt, base, analysis, collation, userTypes = new Map(), rowTypes = new Map(), timeZone = "UTC", enums = { labels: new Map(), arrays: new Map() }, kept = null, wireBound = false, keyCounts = new Set()) {
4941
+ const r = new Rewriter(analysis, base, collation, userTypes, rowTypes, timeZone, enums, kept, keyCounts);
4942
+ if (joinsUsing(analysis.tree, new Set([JSONB, JSONB_ARRAY]))) {
4943
+ throw new Unrewritable("a JOIN … USING or a NATURAL JOIN on a jsonb column is not supported by Mason (Postgres equates jsonb by its own rules)", "JOIN … ON a.j = b.j", "jsonb-compare");
4944
+ }
3828
4945
  const own = JSON.parse(JSON.stringify(stmt));
3829
4946
  volatileWindows(own);
3830
4947
  r.adopt(own);
3831
4948
  nameTargets(own);
3832
4949
  r.indexShapes(own);
4950
+ r.repeatedKeyCounts(own);
3833
4951
  const exact = r.exactThrough(own, analysis.fields);
4952
+ r.markBounds(own, wireBound);
4953
+ r.markWholeReads(own);
3834
4954
  const tree = guardFilters(flattenBool(r.walk(own)));
3835
- return { tree, dropLast: r.excludeRank, exact, clock: r.clock, jsonReads: [...r.jsonReads.values()] };
4955
+ return { tree, dropLast: r.excludeRank, exact, clock: r.clock, jsonReads: [...r.jsonReads.values()], unboundedLateral: r.unboundedLateral };
3836
4956
  }
3837
4957
  /** the functions whose every call is a new value (Postgres's volatile ones Mason answers) */
3838
4958
  const VOLATILE = new Set(["random", "gen_random_uuid", "uuid_generate_v4", "clock_timestamp", "timeofday"]);