@volter/world-core 2.0.37 → 3.0.1

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 (205) hide show
  1. package/README.md +4 -5
  2. package/app-route.cjs +12 -6
  3. package/app-route.d.cts +1 -1
  4. package/dist/app-route.cjs +12 -6
  5. package/dist/app-route.d.cts +1 -1
  6. package/dist/generated/pack-facts.json +1410 -3069
  7. package/dist/inject.cjs +64 -9
  8. package/dist/pack-facts.cjs +44 -0
  9. package/dist/src/actions.d.ts +3 -3
  10. package/dist/src/actions.js +22 -16
  11. package/dist/src/ancestry.d.ts +14 -2
  12. package/dist/src/ancestry.js +92 -2
  13. package/dist/src/anthropic-wire.d.ts +39 -0
  14. package/dist/src/anthropic-wire.js +136 -0
  15. package/dist/src/bytes.d.ts +7 -0
  16. package/dist/src/bytes.js +35 -0
  17. package/dist/src/changeset.d.ts +1 -1
  18. package/dist/src/changeset.js +0 -0
  19. package/dist/src/clickhouse/index.d.ts +3 -0
  20. package/dist/src/clickhouse/index.js +6 -0
  21. package/dist/src/clickhouse/sql.d.ts +233 -0
  22. package/dist/src/clickhouse/sql.js +4329 -0
  23. package/dist/src/clickhouse/types.d.ts +18 -0
  24. package/dist/src/clickhouse/types.js +47 -0
  25. package/dist/src/clickhouse/values.d.ts +146 -0
  26. package/dist/src/clickhouse/values.js +858 -0
  27. package/dist/src/client-bundle.js +2 -3
  28. package/dist/src/cors.d.ts +15 -0
  29. package/dist/src/cors.js +31 -0
  30. package/dist/src/derived-core.d.ts +487 -24
  31. package/dist/src/derived-core.js +788 -144
  32. package/dist/src/derived-real.d.ts +13 -0
  33. package/dist/src/derived-real.js +518 -0
  34. package/dist/src/derived.d.ts +35 -1
  35. package/dist/src/derived.js +61 -9
  36. package/dist/src/emit.js +1 -2
  37. package/dist/src/events.d.ts +206 -0
  38. package/dist/src/events.js +341 -0
  39. package/dist/src/executor.d.ts +3 -0
  40. package/dist/src/executor.js +19 -2
  41. package/dist/src/file-response.d.ts +6 -0
  42. package/dist/src/file-response.js +30 -0
  43. package/dist/src/fork.js +3 -2
  44. package/dist/src/git/history.d.ts +7 -0
  45. package/dist/src/git/history.js +24 -0
  46. package/dist/src/git/index.d.ts +1 -0
  47. package/dist/src/git/index.js +1 -0
  48. package/dist/src/git/lfs.d.ts +28 -0
  49. package/dist/src/git/lfs.js +66 -0
  50. package/dist/src/git/objects.js +3 -8
  51. package/dist/src/git/smart-http.d.ts +3 -1
  52. package/dist/src/git/smart-http.js +67 -6
  53. package/dist/src/graphql-wire.d.ts +29 -0
  54. package/dist/src/graphql-wire.js +101 -0
  55. package/dist/src/grpc-wire.d.ts +67 -0
  56. package/dist/src/grpc-wire.js +170 -0
  57. package/dist/src/h2.d.ts +40 -0
  58. package/dist/src/h2.js +656 -0
  59. package/dist/src/head.d.ts +32 -3
  60. package/dist/src/head.js +161 -40
  61. package/dist/src/history.d.ts +1 -1
  62. package/dist/src/history.js +6 -6
  63. package/dist/src/hpack.json +1 -0
  64. package/dist/src/index.d.ts +64 -75
  65. package/dist/src/index.js +58 -101
  66. package/dist/src/log.js +28 -19
  67. package/dist/src/machines.d.ts +50 -0
  68. package/dist/src/machines.js +151 -0
  69. package/dist/src/managed-database.d.ts +86 -0
  70. package/dist/src/managed-database.js +283 -0
  71. package/dist/src/multipart.d.ts +11 -0
  72. package/dist/src/multipart.js +51 -0
  73. package/dist/src/observe.d.ts +15 -5
  74. package/dist/src/observe.js +23 -9
  75. package/dist/src/openai-wire.d.ts +108 -0
  76. package/dist/src/openai-wire.js +337 -0
  77. package/dist/src/pack-assets.d.ts +3 -4
  78. package/dist/src/pack-assets.js +15 -10
  79. package/dist/src/pack-fetch.d.ts +77 -0
  80. package/dist/src/pack-fetch.js +449 -0
  81. package/dist/src/pack-paths.d.ts +12 -0
  82. package/dist/src/pack-paths.js +86 -0
  83. package/dist/src/packRegistry.d.ts +69 -162
  84. package/dist/src/packRegistry.js +55 -20
  85. package/dist/src/people.d.ts +13 -0
  86. package/dist/src/people.js +18 -0
  87. package/dist/src/placeholder-image.d.ts +5 -0
  88. package/dist/src/placeholder-image.js +114 -0
  89. package/dist/src/protobuf.d.ts +28 -0
  90. package/dist/src/protobuf.js +332 -0
  91. package/dist/src/redis/engine.js +1 -1
  92. package/dist/src/request-scope.d.ts +1 -1
  93. package/dist/src/request-scope.js +6 -4
  94. package/dist/src/resource-blob.d.ts +5 -0
  95. package/dist/src/resource-blob.js +11 -0
  96. package/dist/src/runtime.d.ts +85 -0
  97. package/dist/src/runtime.js +104 -0
  98. package/dist/src/s3/wire.d.ts +60 -0
  99. package/dist/src/s3/wire.js +157 -0
  100. package/dist/src/scenario.d.ts +3 -0
  101. package/dist/src/scenario.js +2 -0
  102. package/dist/src/schema-sample.d.ts +1 -0
  103. package/dist/src/schema-sample.js +21 -0
  104. package/dist/src/sealed-box.d.ts +14 -0
  105. package/dist/src/sealed-box.js +225 -0
  106. package/dist/src/serve-http.d.ts +14 -0
  107. package/dist/src/serve-http.js +27 -3
  108. package/dist/src/serve.d.ts +6 -0
  109. package/dist/src/serve.js +69 -14
  110. package/dist/src/signing.d.ts +135 -0
  111. package/dist/src/signing.js +222 -0
  112. package/dist/src/sigv4.d.ts +48 -0
  113. package/dist/src/sigv4.js +167 -0
  114. package/dist/src/smtp.d.ts +16 -0
  115. package/dist/src/smtp.js +72 -0
  116. package/dist/src/sockets.d.ts +51 -0
  117. package/dist/src/sockets.js +90 -0
  118. package/dist/src/state-system.d.ts +1 -0
  119. package/dist/src/state-system.js +1 -1
  120. package/dist/src/storage.d.ts +1 -1
  121. package/dist/src/storage.js +3 -3
  122. package/dist/src/trace-context.js +1 -1
  123. package/dist/src/twin-fetch.d.ts +0 -7
  124. package/dist/src/twin-fetch.js +0 -14
  125. package/dist/src/vendor-call.d.ts +6 -0
  126. package/dist/src/vendor-call.js +41 -0
  127. package/dist/src/world-store.js +1 -1
  128. package/dist/vendor-hosts.cjs +36 -125
  129. package/dist/vendor-hosts.d.cts +8 -0
  130. package/generated/pack-facts.json +1410 -3069
  131. package/inject.cjs +64 -9
  132. package/pack-facts.cjs +44 -0
  133. package/package.json +17 -3
  134. package/src/actions.ts +23 -16
  135. package/src/ancestry.ts +74 -2
  136. package/src/anthropic-wire.ts +137 -0
  137. package/src/bytes.ts +42 -0
  138. package/src/changeset.ts +5 -5
  139. package/src/clickhouse/index.ts +6 -0
  140. package/src/clickhouse/sql.ts +3059 -0
  141. package/src/clickhouse/types.ts +44 -0
  142. package/src/clickhouse/values.ts +697 -0
  143. package/src/client-bundle.ts +2 -3
  144. package/src/cors.ts +34 -0
  145. package/src/derived-core.ts +1013 -146
  146. package/src/derived-real.ts +434 -0
  147. package/src/derived.ts +73 -3
  148. package/src/emit.ts +1 -2
  149. package/src/events.ts +449 -0
  150. package/src/executor.ts +24 -2
  151. package/src/file-response.ts +27 -0
  152. package/src/fork.ts +3 -2
  153. package/src/git/history.ts +19 -0
  154. package/src/git/index.ts +1 -0
  155. package/src/git/lfs.ts +67 -0
  156. package/src/git/objects.ts +3 -5
  157. package/src/git/smart-http.ts +56 -6
  158. package/src/graphql-wire.ts +106 -0
  159. package/src/grpc-wire.ts +159 -0
  160. package/src/h2.ts +627 -0
  161. package/src/head.ts +132 -41
  162. package/src/history.ts +6 -6
  163. package/src/hpack.json +1 -0
  164. package/src/index.ts +82 -329
  165. package/src/log.ts +27 -18
  166. package/src/machines.ts +151 -0
  167. package/src/managed-database.ts +299 -0
  168. package/src/multipart.ts +51 -0
  169. package/src/observe.ts +31 -15
  170. package/src/openai-wire.ts +371 -0
  171. package/src/pack-assets.ts +15 -11
  172. package/src/pack-fetch.ts +458 -0
  173. package/src/pack-paths.ts +72 -0
  174. package/src/packRegistry.ts +79 -167
  175. package/src/people.ts +31 -0
  176. package/src/placeholder-image.ts +88 -0
  177. package/src/protobuf.ts +251 -0
  178. package/src/redis/engine.ts +1 -1
  179. package/src/request-scope.ts +8 -4
  180. package/src/resource-blob.ts +13 -0
  181. package/src/runtime.ts +344 -0
  182. package/src/s3/wire.ts +172 -0
  183. package/src/scenario.ts +4 -0
  184. package/src/schema-sample.ts +24 -0
  185. package/src/sealed-box.ts +182 -0
  186. package/src/serve-http.ts +31 -3
  187. package/src/serve.ts +58 -14
  188. package/src/signing.ts +231 -0
  189. package/src/sigv4.ts +158 -0
  190. package/src/smtp.ts +76 -0
  191. package/src/sockets.ts +140 -0
  192. package/src/state-system.ts +2 -2
  193. package/src/storage.ts +3 -3
  194. package/src/trace-context.ts +1 -1
  195. package/src/twin-fetch.ts +0 -20
  196. package/src/vendor-call.ts +41 -0
  197. package/src/world-store.ts +1 -1
  198. package/vendor-hosts.cjs +36 -125
  199. package/vendor-hosts.d.cts +8 -0
  200. package/dist/src/mirror-shell.d.ts +0 -2
  201. package/dist/src/mirror-shell.js +0 -13
  202. package/dist/src/v1-removed.d.ts +0 -159
  203. package/dist/src/v1-removed.js +0 -124
  204. package/src/mirror-shell.ts +0 -15
  205. package/src/v1-removed.ts +0 -172
@@ -0,0 +1,4329 @@
1
+ // The kernel's ClickHouse® SQL engine — deterministic, over rows a pack supplies (Tinybird's Data Sources and pipe nodes,
2
+ // PostHog's events and persons).
3
+ //
4
+ // ── WHY AN ENGINE AND NOT A LOOKUP TABLE ────────────────────────────────────────────────────
5
+ // A Tinybird Pipe IS its SQL, and a HogQL query is ClickHouse SQL: the query a consumer writes tomorrow must be answered
6
+ // by executing it, never by a per-query answer. So SQL is parsed into an AST and evaluated over the rows the pack's
7
+ // catalog holds; coverage is a claim about the GRAMMAR and the FUNCTION LIBRARY, not about a list of blessed queries.
8
+ // Anything outside them fails the way ClickHouse fails (an error naming the unknown function or the syntax), never with
9
+ // a guessed answer. A pack keeps its own front: Tinybird's templates, HogQL's tables and property access.
10
+ //
11
+ // ── THE MODELED GRAMMAR ─────────────────────────────────────────────────────────────────────
12
+ // query ::= select (UNION ALL select)* [FORMAT name]
13
+ // select ::= [WITH (expr AS name | name AS (query)) , …]
14
+ // SELECT [DISTINCT] item, … [,]
15
+ // [FROM table_expr (join | , table_expr)*]
16
+ // [PREWHERE expr] [WHERE expr] [GROUP BY expr, …] [HAVING expr]
17
+ // [ORDER BY expr [ASC|DESC] [NULLS FIRST|LAST], …] [LIMIT n [OFFSET m] | LIMIT m, n] [OFFSET m]
18
+ // table_expr ::= (name | (query)) [FINAL] [[AS] alias]
19
+ // join ::= [GLOBAL] [ANY|ALL|SEMI|ANTI] [INNER|LEFT|RIGHT|FULL|CROSS] [OUTER] [ANY|ALL] JOIN
20
+ // table_expr (ON expr | USING (col, …) | USING col)
21
+ // expr ::= ClickHouse operators (OR AND NOT, = == != <> < <= > >=, [NOT] LIKE/ILIKE, [NOT] IN,
22
+ // IS [NOT] NULL, [NOT] BETWEEN, ||, + - * / %, unary -, x[i]), literals, arrays
23
+ // `[…]`, tuples, `CASE`, `INTERVAL n unit`, `CAST(x AS T)`, lambdas `x -> …`,
24
+ // scalar subqueries, and an `AS alias` on any sub-expression.
25
+ // Names resolve as ClickHouse's analyzer does: a SELECT alias is visible in every clause and wins
26
+ // over a column of the same name, except inside its own definition (`toLowCardinality(country)
27
+ // AS country`); `WITH expr AS name` defines a query-wide constant.
28
+ //
29
+ // ── MODELED SEMANTICS ───────────────────────────────────────────────────────────────────────
30
+ // • JOIN defaults to ALL strictness; ANY keeps the first match. A non-matching side is filled
31
+ // with its columns' type defaults (ClickHouse's `join_use_nulls = 0`), which is why
32
+ // `COALESCE(x.col, 0)` and a default `0` both read as zero.
33
+ // • FINAL over a ReplacingMergeTree keeps, per sorting key, the row with the greatest version
34
+ // column (the last inserted on a tie) and drops rows whose is-deleted column is 1.
35
+ // • An un-grouped aggregate returns exactly one row even over zero rows; `count()` is UInt64.
36
+ // • `arrayJoin(arr)` in the SELECT list multiplies each row by the array's elements.
37
+ // • Comparisons with NULL are NULL, and WHERE keeps only true rows. A string literal meeting a
38
+ // DateTime is parsed in that DateTime's zone.
39
+ //
40
+ // `statistics.elapsed` is DERIVED from the scan (`rows_read * 1e-6 + 1.23e-4`), never timed: a
41
+ // wall-clock number would make every verify non-deterministic.
42
+ import { digest } from "../signing.js";
43
+ import { AggState, baseType, ChMap, castDateTime, fromStored, civilFromDays, compareValues, daysFromCivil, DDate, defaultOf, DT, formatDateTimePattern, instantOf, Interval, isFloatType, isIntegerType, isNullableType, isNumberType, isUnsignedType, parseDateText, SERVER_TZ, toOutput, typeArgs, typeHead, typeScale, typeZone, valueKey, valueKeyAs, valuesEqual, valueToText, wallOf, aggKind, makeDT, normInt, stringifyJson, toBigInt, wrapInt, } from "./values.js";
44
+ import { isModeledSchemaType } from "./types.js";
45
+ import { ClickHouseError } from "./values.js";
46
+ export { ClickHouseError };
47
+ function fail(message) {
48
+ throw new ClickHouseError(message);
49
+ }
50
+ function unescapeClickHouseString(body) {
51
+ let out = '';
52
+ for (let i = 0; i < body.length; i++) {
53
+ const c = body[i];
54
+ if (c !== '\\') {
55
+ out += c;
56
+ continue;
57
+ }
58
+ const e = body[++i];
59
+ if (e === undefined)
60
+ break;
61
+ switch (e) {
62
+ case 'n':
63
+ out += '\n';
64
+ break;
65
+ case 't':
66
+ out += '\t';
67
+ break;
68
+ case 'r':
69
+ out += '\r';
70
+ break;
71
+ case '0':
72
+ out += '\0';
73
+ break;
74
+ case 'b':
75
+ out += '\b';
76
+ break;
77
+ case 'f':
78
+ out += '\f';
79
+ break;
80
+ case 'a':
81
+ out += '\x07';
82
+ break;
83
+ case 'v':
84
+ out += '\v';
85
+ break;
86
+ case 'e':
87
+ out += '\x1b';
88
+ break;
89
+ case 'x': {
90
+ out += String.fromCharCode(Number.parseInt(body.slice(i + 1, i + 3), 16));
91
+ i += 2;
92
+ break;
93
+ }
94
+ case '\\':
95
+ case "'":
96
+ case '"':
97
+ case '`':
98
+ case '/':
99
+ case '=':
100
+ out += e;
101
+ break;
102
+ // ClickHouse keeps the backslash of an unknown escape (`\%` and `\_` stay LIKE escapes).
103
+ default: out += `\\${e}`;
104
+ }
105
+ }
106
+ return out;
107
+ }
108
+ function lex(sql) {
109
+ const out = [];
110
+ let i = 0;
111
+ const n = sql.length;
112
+ while (i < n) {
113
+ const c = sql[i];
114
+ if (/\s/.test(c)) {
115
+ i++;
116
+ continue;
117
+ }
118
+ if (c === '-' && sql[i + 1] === '-') {
119
+ while (i < n && sql[i] !== '\n')
120
+ i++;
121
+ continue;
122
+ }
123
+ if (c === '#' && (sql[i + 1] === ' ' || sql[i + 1] === '!')) {
124
+ while (i < n && sql[i] !== '\n')
125
+ i++;
126
+ continue;
127
+ }
128
+ if (c === '/' && sql[i + 1] === '*') {
129
+ const end = sql.indexOf('*/', i + 2);
130
+ i = end < 0 ? n : end + 2;
131
+ continue;
132
+ }
133
+ if (c === "'") {
134
+ let j = i + 1;
135
+ let body = '';
136
+ for (;;) {
137
+ if (j >= n)
138
+ fail('Syntax error: unterminated string literal');
139
+ const d = sql[j];
140
+ if (d === '\\') {
141
+ body += d + (sql[j + 1] ?? '');
142
+ j += 2;
143
+ continue;
144
+ }
145
+ if (d === "'") {
146
+ if (sql[j + 1] === "'") {
147
+ body += "\\'";
148
+ j += 2;
149
+ continue;
150
+ }
151
+ break;
152
+ }
153
+ body += d;
154
+ j++;
155
+ }
156
+ out.push({ k: 'str', v: unescapeClickHouseString(body) });
157
+ i = j + 1;
158
+ continue;
159
+ }
160
+ if (c === '`' || c === '"') {
161
+ const end = sql.indexOf(c, i + 1);
162
+ if (end < 0)
163
+ fail('Syntax error: unterminated quoted identifier');
164
+ out.push({ k: 'qid', v: sql.slice(i + 1, end) });
165
+ i = end + 1;
166
+ continue;
167
+ }
168
+ if (/[0-9]/.test(c) || (c === '.' && /[0-9]/.test(sql[i + 1] ?? '') && !prevIsValue(out))) {
169
+ const m = /^(0x[0-9a-fA-F]+|\d+\.?\d*(?:[eE][+-]?\d+)?|\.\d+(?:[eE][+-]?\d+)?)/.exec(sql.slice(i));
170
+ const text = m[0];
171
+ // An integer literal is exact: beyond 2^53 it is a bigint (ClickHouse reads it as UInt64/Int64).
172
+ const isInt = text.startsWith('0x') || /^\d+$/.test(text);
173
+ out.push({ k: 'num', v: isInt ? normInt(BigInt(text)) : Number(text), text });
174
+ i += text.length;
175
+ continue;
176
+ }
177
+ if (/[A-Za-z_]/.test(c)) {
178
+ let j = i + 1;
179
+ while (j < n && /[A-Za-z0-9_$]/.test(sql[j]))
180
+ j++;
181
+ const v = sql.slice(i, j);
182
+ out.push({ k: 'word', v, up: v.toUpperCase() });
183
+ i = j;
184
+ continue;
185
+ }
186
+ const three = sql.slice(i, i + 3);
187
+ const two = sql.slice(i, i + 2);
188
+ if (three === '<=>') {
189
+ out.push({ k: 'op', v: three });
190
+ i += 3;
191
+ continue;
192
+ }
193
+ if (['==', '!=', '<>', '<=', '>=', '||', '->', '::'].includes(two)) {
194
+ out.push({ k: 'op', v: two });
195
+ i += 2;
196
+ continue;
197
+ }
198
+ if ('()[]{},.;=<>+-*/%?:'.includes(c)) {
199
+ out.push({ k: 'op', v: c });
200
+ i++;
201
+ continue;
202
+ }
203
+ fail(`Syntax error: unexpected character '${c}'`);
204
+ }
205
+ out.push({ k: 'end' });
206
+ return out;
207
+ }
208
+ /** ClickHouse's type for an integer literal: the smallest UInt that holds it, or Int when negative. */
209
+ function literalIntType(v) {
210
+ const n = BigInt(v);
211
+ if (n >= 0n)
212
+ return n < 256n ? 'UInt8' : n < 65536n ? 'UInt16' : n < 4294967296n ? 'UInt32' : n < 18446744073709551616n ? 'UInt64' : n < (1n << 128n) ? 'UInt128' : 'UInt256';
213
+ return n >= -128n ? 'Int8' : n >= -32768n ? 'Int16' : n >= -2147483648n ? 'Int32' : n >= -(1n << 63n) ? 'Int64' : n >= -(1n << 127n) ? 'Int128' : 'Int256';
214
+ }
215
+ function prevIsValue(out) {
216
+ const p = out[out.length - 1];
217
+ return p !== undefined && (p.k === 'word' || p.k === 'qid' || (p.k === 'op' && (p.v === ')' || p.v === ']')));
218
+ }
219
+ // ── parser ──────────────────────────────────────────────────────────────────────────────────
220
+ const CLAUSE_WORDS = new Set([
221
+ 'FROM', 'WHERE', 'PREWHERE', 'GROUP', 'ORDER', 'LIMIT', 'HAVING', 'UNION', 'FORMAT', 'SETTINGS',
222
+ 'OFFSET', 'EXCEPT', 'INTERSECT', 'INTO', 'WINDOW', 'QUALIFY', 'WITH', 'SELECT',
223
+ ]);
224
+ const TABLE_ALIAS_STOP = new Set([
225
+ ...CLAUSE_WORDS, 'LEFT', 'RIGHT', 'INNER', 'FULL', 'CROSS', 'JOIN', 'ANY', 'ALL', 'SEMI', 'ANTI',
226
+ 'ASOF', 'GLOBAL', 'ARRAY', 'ON', 'USING', 'FINAL', 'SAMPLE', 'AS', 'OUTER', 'PASTE', 'LOCAL',
227
+ ]);
228
+ const INTERVAL_UNITS = {
229
+ MILLISECOND: 'millisecond', MILLISECONDS: 'millisecond', SECOND: 'second', SECONDS: 'second',
230
+ MINUTE: 'minute', MINUTES: 'minute', HOUR: 'hour', HOURS: 'hour', DAY: 'day', DAYS: 'day',
231
+ WEEK: 'week', WEEKS: 'week', MONTH: 'month', MONTHS: 'month', QUARTER: 'quarter', QUARTERS: 'quarter',
232
+ YEAR: 'year', YEARS: 'year',
233
+ };
234
+ // The tokens after a FROM that make it a column named `from` (ClickHouse's ParserExpressionImpl::operators_table, plus the
235
+ // comma, a second FROM and an explicit alias its trailing-comma check tests first).
236
+ const FROM_COLUMN_OPS = new Set([',', '->', '?', ':', '==', '!=', '<=>', '<>', '<=', '>=', '<', '>', '=', '~', '~*', '!~', '!~*', '||', '+', '-', '−', '*', '/', '%', '.', '[', '::']);
237
+ const FROM_COLUMN_WORDS = new Set(['FROM', 'AS', 'OR', 'AND', 'IS', 'BETWEEN', 'LIKE', 'ILIKE', 'REGEXP', 'IN', 'MOD', 'DIV']);
238
+ class Parser {
239
+ t;
240
+ i = 0;
241
+ constructor(t) {
242
+ this.t = t;
243
+ }
244
+ peek(o = 0) { return this.t[this.i + o] ?? { k: 'end' }; }
245
+ next() { return this.t[this.i++] ?? { k: 'end' }; }
246
+ isWord(w, o = 0) { const t = this.peek(o); return t.k === 'word' && t.up === w; }
247
+ isOp(v, o = 0) { const t = this.peek(o); return t.k === 'op' && t.v === v; }
248
+ acceptWord(w) { if (this.isWord(w)) {
249
+ this.i++;
250
+ return true;
251
+ } return false; }
252
+ acceptOp(v) { if (this.isOp(v)) {
253
+ this.i++;
254
+ return true;
255
+ } return false; }
256
+ /** Whether the FROM at the cursor is a column named `from` rather than the FROM clause: ClickHouse's trailing-comma
257
+ * test reads it so when the next token is a comma, a second FROM, `AS` or one of its operators_table entries. */
258
+ fromIsColumn() {
259
+ const t = this.peek(1);
260
+ if (t.k === 'op')
261
+ return FROM_COLUMN_OPS.has(t.v);
262
+ if (t.k !== 'word')
263
+ return false;
264
+ if (t.up === 'NOT')
265
+ return this.isWord('BETWEEN', 2) || this.isWord('LIKE', 2) || this.isWord('ILIKE', 2) || this.isWord('IN', 2);
266
+ if (t.up === 'GLOBAL')
267
+ return this.isWord('IN', 2) || this.isWord('NOT', 2);
268
+ if (t.up === 'AT')
269
+ return this.isWord('TIME', 2);
270
+ return FROM_COLUMN_WORDS.has(t.up);
271
+ }
272
+ expectWord(w) { if (!this.acceptWord(w))
273
+ this.syntax(`expected ${w}`); }
274
+ expectOp(v) { if (!this.acceptOp(v))
275
+ this.syntax(`expected '${v}'`); }
276
+ syntax(what) {
277
+ const t = this.peek();
278
+ const near = t.k === 'end' ? 'end of query' : t.k === 'word' || t.k === 'qid' || t.k === 'op' ? `'${t.v}'` : t.k === 'num' ? t.text : `'${t.v}'`;
279
+ return fail(`Syntax error: ${what}, near ${near}`);
280
+ }
281
+ name() {
282
+ const t = this.next();
283
+ if (t.k === 'word' || t.k === 'qid')
284
+ return t.v;
285
+ this.i--;
286
+ return this.syntax('expected identifier');
287
+ }
288
+ parseQuery() {
289
+ const q = this.query();
290
+ this.acceptOp(';');
291
+ if (this.peek().k !== 'end')
292
+ this.syntax('unexpected trailing input');
293
+ return q;
294
+ }
295
+ query() {
296
+ const selects = [this.select()];
297
+ while (this.isWord('UNION')) {
298
+ this.i++;
299
+ if (!this.acceptWord('ALL')) {
300
+ if (this.acceptWord('DISTINCT'))
301
+ fail('UNION DISTINCT is not supported by this twin');
302
+ fail('UNION without ALL is not supported by this twin (ClickHouse requires a union mode)');
303
+ }
304
+ selects.push(this.select());
305
+ }
306
+ let format;
307
+ if (this.acceptWord('FORMAT'))
308
+ format = this.name();
309
+ if (this.isWord('SETTINGS'))
310
+ fail('SETTINGS clauses are not supported by this twin');
311
+ return { selects, ...(format !== undefined ? { format } : {}) };
312
+ }
313
+ select() {
314
+ if (this.acceptOp('(')) {
315
+ const inner = this.query();
316
+ this.expectOp(')');
317
+ if (inner.selects.length !== 1)
318
+ fail('parenthesised UNION is not supported by this twin');
319
+ return inner.selects[0];
320
+ }
321
+ const withItems = [];
322
+ if (this.acceptWord('WITH')) {
323
+ do {
324
+ // `name AS (SELECT …)` is a common table expression; anything else is `expr AS name`.
325
+ if ((this.peek().k === 'word' || this.peek().k === 'qid') && this.isWord('AS', 1) && this.isOp('(', 2) && (this.isWord('SELECT', 3) || this.isWord('WITH', 3))) {
326
+ const name = this.name();
327
+ this.i++;
328
+ this.expectOp('(');
329
+ const query = this.query();
330
+ this.expectOp(')');
331
+ withItems.push({ name, query });
332
+ }
333
+ else {
334
+ const e = this.expr();
335
+ if (e.k !== 'alias') {
336
+ if (e.k === 'id' && e.parts.length === 1)
337
+ fail('WITH <name> without AS is not supported by this twin');
338
+ fail('WITH expressions need an alias');
339
+ }
340
+ withItems.push({ name: e.name, expr: e.e });
341
+ }
342
+ } while (this.acceptOp(','));
343
+ }
344
+ this.expectWord('SELECT');
345
+ const distinct = this.acceptWord('DISTINCT');
346
+ if (this.isWord('TOP'))
347
+ fail('SELECT TOP is not supported by this twin');
348
+ const items = [];
349
+ // A trailing comma may end the list: ClickHouse allows `SELECT a, b, FROM t` (ClickHouse/ClickHouse#48438, "Allow
350
+ // trailing commas in expression list of SELECT query"), and Dub's v3_usage pipe renders one when grouped. Its parser
351
+ // (ExpressionListParsers.cpp) takes the comma as trailing only at the "End of query" (the end, or its `;`) or before
352
+ // a FROM, and then eliminates "WITH 1 AS from SELECT 2, from", "SELECT to, from FROM table", "SELECT to, from AS
353
+ // alias FROM table" and "SELECT to, from + to, from IN [1,2,3], FROM table": a FROM followed by a comma, a second
354
+ // FROM, an explicit alias or an operator is a column named `from`. Its test 02676_trailing_commas runs `SELECT 1,;`
355
+ // and `SELECT n, FROM (SELECT 1 AS n)`; a comma before `)` is none of these and stays a syntax error.
356
+ const listEnds = () => this.peek().k === 'end' || this.isOp(';') || (this.isWord('FROM') && !this.fromIsColumn());
357
+ do {
358
+ items.push(this.selectItem());
359
+ } while (this.acceptOp(',') && !listEnds());
360
+ const sel = { with: withItems, distinct, items, from: [], groupBy: [], orderBy: [] };
361
+ if (this.acceptWord('FROM'))
362
+ sel.from = this.fromClause();
363
+ if (this.isWord('ARRAY') && this.isWord('JOIN', 1))
364
+ fail('ARRAY JOIN is not supported by this twin');
365
+ if (this.acceptWord('PREWHERE'))
366
+ sel.prewhere = this.expr();
367
+ if (this.acceptWord('WHERE'))
368
+ sel.where = this.expr();
369
+ if (this.acceptWord('GROUP')) {
370
+ this.expectWord('BY');
371
+ if ((this.isWord('ROLLUP') || this.isWord('CUBE')) && this.isOp('(', 1))
372
+ fail(`GROUP BY ${this.peek().v}(…) is not supported by this twin`);
373
+ if (this.isWord('GROUPING') && this.isWord('SETS', 1))
374
+ fail('GROUP BY GROUPING SETS is not supported by this twin');
375
+ do
376
+ sel.groupBy.push(this.expr());
377
+ while (this.acceptOp(','));
378
+ if (this.isWord('WITH'))
379
+ fail('GROUP BY … WITH ROLLUP/CUBE/TOTALS is not supported by this twin');
380
+ }
381
+ if (this.acceptWord('HAVING'))
382
+ sel.having = this.expr();
383
+ if (this.isWord('WINDOW') || this.isWord('QUALIFY'))
384
+ fail(`${this.peek().v} is not supported by this twin`);
385
+ if (this.acceptWord('ORDER')) {
386
+ this.expectWord('BY');
387
+ do {
388
+ const e = this.expr();
389
+ let desc = false;
390
+ if (this.acceptWord('DESC') || this.acceptWord('DESCENDING'))
391
+ desc = true;
392
+ else if (this.acceptWord('ASC') || this.acceptWord('ASCENDING'))
393
+ desc = false;
394
+ let nullsFirst = false;
395
+ if (this.acceptWord('NULLS')) {
396
+ if (this.acceptWord('FIRST'))
397
+ nullsFirst = true;
398
+ else {
399
+ this.expectWord('LAST');
400
+ nullsFirst = false;
401
+ }
402
+ }
403
+ if (this.isWord('COLLATE') || this.isWord('WITH'))
404
+ fail('ORDER BY … COLLATE / WITH FILL is not supported by this twin');
405
+ sel.orderBy.push({ e, desc, nullsFirst });
406
+ } while (this.acceptOp(','));
407
+ }
408
+ if (this.acceptWord('LIMIT')) {
409
+ const first = this.expr();
410
+ if (this.acceptOp(',')) {
411
+ sel.offset = first;
412
+ sel.limit = this.expr();
413
+ }
414
+ else {
415
+ sel.limit = first;
416
+ if (this.acceptWord('OFFSET'))
417
+ sel.offset = this.expr();
418
+ }
419
+ if (this.isWord('BY'))
420
+ fail('LIMIT … BY is not supported by this twin');
421
+ if (this.isWord('WITH'))
422
+ fail('LIMIT … WITH TIES is not supported by this twin');
423
+ }
424
+ if (this.acceptWord('OFFSET')) {
425
+ sel.offset = this.expr();
426
+ this.acceptWord('ROW') || this.acceptWord('ROWS');
427
+ if (this.acceptWord('FETCH'))
428
+ fail('OFFSET … FETCH is not supported by this twin');
429
+ }
430
+ return sel;
431
+ }
432
+ selectItem() {
433
+ const e = this.expr();
434
+ if (e.k === 'alias')
435
+ return e;
436
+ // An implicit alias: `toLowCardinality(country) country`.
437
+ const t = this.peek();
438
+ if ((t.k === 'word' && !CLAUSE_WORDS.has(t.up) && t.up !== 'AS') || t.k === 'qid') {
439
+ this.i++;
440
+ return { k: 'alias', name: t.v, e };
441
+ }
442
+ return e;
443
+ }
444
+ fromClause() {
445
+ const items = [this.tableExpr()];
446
+ for (;;) {
447
+ if (this.acceptOp(',')) {
448
+ items.push({ ...this.tableExpr(), join: { kind: 'cross', strictness: 'all' } });
449
+ continue;
450
+ }
451
+ const start = this.i;
452
+ let strictness;
453
+ let kind;
454
+ let sawJoinWord = false;
455
+ for (let guard = 0; guard < 6; guard++) {
456
+ if (this.acceptWord('GLOBAL') || this.acceptWord('LOCAL'))
457
+ continue;
458
+ if (this.isWord('ASOF'))
459
+ fail('ASOF JOIN is not supported by this twin');
460
+ if (this.isWord('PASTE'))
461
+ fail('PASTE JOIN is not supported by this twin');
462
+ if (this.acceptWord('ANY')) {
463
+ strictness = 'any';
464
+ continue;
465
+ }
466
+ if (this.acceptWord('ALL')) {
467
+ strictness = 'all';
468
+ continue;
469
+ }
470
+ if (this.acceptWord('SEMI')) {
471
+ strictness = 'semi';
472
+ continue;
473
+ }
474
+ if (this.acceptWord('ANTI')) {
475
+ strictness = 'anti';
476
+ continue;
477
+ }
478
+ if (this.acceptWord('INNER')) {
479
+ kind = 'inner';
480
+ continue;
481
+ }
482
+ if (this.acceptWord('LEFT')) {
483
+ kind = 'left';
484
+ continue;
485
+ }
486
+ if (this.acceptWord('RIGHT')) {
487
+ kind = 'right';
488
+ continue;
489
+ }
490
+ if (this.acceptWord('FULL')) {
491
+ kind = 'full';
492
+ continue;
493
+ }
494
+ if (this.acceptWord('CROSS')) {
495
+ kind = 'cross';
496
+ continue;
497
+ }
498
+ if (this.acceptWord('OUTER'))
499
+ continue;
500
+ if (this.acceptWord('JOIN')) {
501
+ sawJoinWord = true;
502
+ break;
503
+ }
504
+ break;
505
+ }
506
+ if (!sawJoinWord) {
507
+ this.i = start;
508
+ break;
509
+ }
510
+ const item = this.tableExpr();
511
+ const join = { kind: kind ?? 'inner', strictness: strictness ?? 'all' };
512
+ if (join.kind !== 'cross') {
513
+ if (this.acceptWord('ON'))
514
+ join.on = this.expr();
515
+ else if (this.acceptWord('USING')) {
516
+ const using = [];
517
+ if (this.acceptOp('(')) {
518
+ do
519
+ using.push(this.name());
520
+ while (this.acceptOp(','));
521
+ this.expectOp(')');
522
+ }
523
+ else {
524
+ do
525
+ using.push(this.name());
526
+ while (this.acceptOp(','));
527
+ }
528
+ join.using = using;
529
+ }
530
+ else
531
+ this.syntax('expected ON or USING after JOIN');
532
+ }
533
+ items.push({ ...item, join });
534
+ }
535
+ return items;
536
+ }
537
+ tableExpr() {
538
+ let source;
539
+ if (this.acceptOp('(')) {
540
+ const query = this.query();
541
+ this.expectOp(')');
542
+ source = { kind: 'sub', query };
543
+ }
544
+ else {
545
+ let name = this.name();
546
+ if (this.acceptOp('.'))
547
+ name = `${name}.${this.name()}`;
548
+ if (this.isOp('('))
549
+ fail(`table functions (${name}(…)) are not supported by this twin`);
550
+ source = { kind: 'table', name };
551
+ }
552
+ let final = this.acceptWord('FINAL');
553
+ let alias;
554
+ if (this.acceptWord('AS'))
555
+ alias = this.name();
556
+ else {
557
+ const t = this.peek();
558
+ if ((t.k === 'word' && !TABLE_ALIAS_STOP.has(t.up)) || t.k === 'qid') {
559
+ alias = t.v;
560
+ this.i++;
561
+ }
562
+ }
563
+ if (this.acceptWord('FINAL'))
564
+ final = true;
565
+ if (this.isWord('SAMPLE'))
566
+ fail('SAMPLE is not supported by this twin');
567
+ return { source, final, ...(alias !== undefined ? { alias } : {}) };
568
+ }
569
+ // ── expressions ──
570
+ expr(allowAlias = true) {
571
+ // A lambda: `x -> body` or `(x, y) -> body`.
572
+ if ((this.peek().k === 'word' || this.peek().k === 'qid') && this.isOp('->', 1)) {
573
+ const p = this.name();
574
+ this.i++;
575
+ return { k: 'lambda', params: [p], body: this.expr() };
576
+ }
577
+ if (this.isOp('(')) {
578
+ let j = 1;
579
+ const params = [];
580
+ let ok = true;
581
+ for (;;) {
582
+ const t = this.peek(j);
583
+ if (t.k !== 'word' && t.k !== 'qid') {
584
+ ok = false;
585
+ break;
586
+ }
587
+ params.push(t.v);
588
+ j++;
589
+ if (this.isOp(',', j)) {
590
+ j++;
591
+ continue;
592
+ }
593
+ if (this.isOp(')', j)) {
594
+ j++;
595
+ break;
596
+ }
597
+ ok = false;
598
+ break;
599
+ }
600
+ if (ok && this.isOp('->', j)) {
601
+ this.i += j + 1;
602
+ return { k: 'lambda', params, body: this.expr() };
603
+ }
604
+ }
605
+ let e = this.or();
606
+ if (this.isOp('?'))
607
+ fail('the ternary operator ?: is not supported by this twin');
608
+ // Inside CAST(… AS T) the AS introduces the type, never an alias.
609
+ while (allowAlias && this.acceptWord('AS'))
610
+ e = { k: 'alias', name: this.name(), e };
611
+ return e;
612
+ }
613
+ or() {
614
+ let a = this.and();
615
+ while (this.acceptWord('OR'))
616
+ a = { k: 'bin', op: 'or', a, b: this.and() };
617
+ return a;
618
+ }
619
+ and() {
620
+ let a = this.not();
621
+ while (this.acceptWord('AND'))
622
+ a = { k: 'bin', op: 'and', a, b: this.not() };
623
+ return a;
624
+ }
625
+ not() {
626
+ if (this.isWord('NOT') && !this.isOp('(', 1)) {
627
+ this.i++;
628
+ return { k: 'not', a: this.not() };
629
+ }
630
+ if (this.isWord('NOT') && this.isOp('(', 1)) {
631
+ this.i++;
632
+ const inner = this.comparison();
633
+ return { k: 'not', a: inner };
634
+ }
635
+ return this.comparison();
636
+ }
637
+ comparison() {
638
+ let a = this.concat();
639
+ for (;;) {
640
+ const t = this.peek();
641
+ if (t.k === 'op' && ['=', '==', '!=', '<>', '<', '<=', '>', '>=', '<=>'].includes(t.v)) {
642
+ this.i++;
643
+ const op = t.v === '==' ? '=' : t.v === '<>' ? '!=' : t.v;
644
+ a = { k: 'bin', op, a, b: this.concat() };
645
+ continue;
646
+ }
647
+ let neg = false;
648
+ if (this.isWord('NOT') && (this.isWord('LIKE', 1) || this.isWord('ILIKE', 1) || this.isWord('IN', 1) || this.isWord('BETWEEN', 1))) {
649
+ this.i++;
650
+ neg = true;
651
+ }
652
+ if (this.acceptWord('LIKE')) {
653
+ a = { k: 'bin', op: neg ? 'notlike' : 'like', a, b: this.concat() };
654
+ continue;
655
+ }
656
+ if (this.acceptWord('ILIKE')) {
657
+ a = { k: 'bin', op: neg ? 'notilike' : 'ilike', a, b: this.concat() };
658
+ continue;
659
+ }
660
+ if (this.isWord('GLOBAL') && (this.isWord('IN', 1) || (this.isWord('NOT', 1) && this.isWord('IN', 2)))) {
661
+ this.i++;
662
+ continue;
663
+ }
664
+ if (this.acceptWord('IN')) {
665
+ a = { k: 'bin', op: neg ? 'notin' : 'in', a, b: this.concat() };
666
+ continue;
667
+ }
668
+ if (this.acceptWord('BETWEEN')) {
669
+ const lo = this.concat();
670
+ this.expectWord('AND');
671
+ const hi = this.concat();
672
+ a = { k: 'between', a, lo, hi, not: neg };
673
+ continue;
674
+ }
675
+ if (this.isWord('IS')) {
676
+ this.i++;
677
+ const isNot = this.acceptWord('NOT');
678
+ if (this.acceptWord('NULL')) {
679
+ a = { k: 'isnull', a, not: isNot };
680
+ continue;
681
+ }
682
+ if (this.isWord('DISTINCT'))
683
+ fail('IS [NOT] DISTINCT FROM is not supported by this twin');
684
+ this.syntax('expected NULL after IS');
685
+ }
686
+ return a;
687
+ }
688
+ }
689
+ concat() {
690
+ let a = this.additive();
691
+ while (this.acceptOp('||'))
692
+ a = { k: 'fn', name: 'concat', args: [a, this.additive()] };
693
+ return a;
694
+ }
695
+ additive() {
696
+ let a = this.term();
697
+ for (;;) {
698
+ if (this.acceptOp('+')) {
699
+ a = { k: 'bin', op: '+', a, b: this.term() };
700
+ continue;
701
+ }
702
+ if (this.acceptOp('-')) {
703
+ a = { k: 'bin', op: '-', a, b: this.term() };
704
+ continue;
705
+ }
706
+ return a;
707
+ }
708
+ }
709
+ term() {
710
+ let a = this.unary();
711
+ for (;;) {
712
+ if (this.acceptOp('*')) {
713
+ a = { k: 'bin', op: '*', a, b: this.unary() };
714
+ continue;
715
+ }
716
+ if (this.acceptOp('/')) {
717
+ a = { k: 'bin', op: '/', a, b: this.unary() };
718
+ continue;
719
+ }
720
+ if (this.acceptOp('%')) {
721
+ a = { k: 'bin', op: '%', a, b: this.unary() };
722
+ continue;
723
+ }
724
+ if (this.isWord('DIV')) {
725
+ this.i++;
726
+ a = { k: 'fn', name: 'intDiv', args: [a, this.unary()] };
727
+ continue;
728
+ }
729
+ if (this.isWord('MOD')) {
730
+ this.i++;
731
+ a = { k: 'bin', op: '%', a, b: this.unary() };
732
+ continue;
733
+ }
734
+ return a;
735
+ }
736
+ }
737
+ unary() {
738
+ if (this.acceptOp('-')) {
739
+ const inner = this.unary();
740
+ if (inner.k === 'lit' && (typeof inner.v === 'number' || typeof inner.v === 'bigint')) {
741
+ const v = typeof inner.v === 'bigint' ? normInt(-inner.v) : -inner.v;
742
+ return { k: 'lit', v, type: inner.type === 'Float64' ? 'Float64' : literalIntType(v), text: `-${inner.text}` };
743
+ }
744
+ return { k: 'neg', a: inner };
745
+ }
746
+ if (this.acceptOp('+'))
747
+ return this.unary();
748
+ return this.postfix();
749
+ }
750
+ postfix() {
751
+ let e = this.primary();
752
+ for (;;) {
753
+ if (this.isOp('[')) {
754
+ this.i++;
755
+ const idx = this.expr();
756
+ this.expectOp(']');
757
+ e = { k: 'index', a: e, i: idx };
758
+ continue;
759
+ }
760
+ if (this.isOp('::')) {
761
+ this.i++;
762
+ e = { k: 'cast', e, type: this.typeName() };
763
+ continue;
764
+ }
765
+ if (this.isOp('.') && this.peek(1).k === 'num') {
766
+ fail('tuple element access (t.1) is not supported by this twin');
767
+ }
768
+ return e;
769
+ }
770
+ }
771
+ typeName() {
772
+ let s = this.name();
773
+ if (this.acceptOp('(')) {
774
+ let depth = 1;
775
+ s += '(';
776
+ while (depth > 0) {
777
+ const t = this.next();
778
+ if (t.k === 'end')
779
+ return this.syntax('unterminated type');
780
+ if (t.k === 'op' && t.v === '(')
781
+ depth++;
782
+ if (t.k === 'op' && t.v === ')') {
783
+ depth--;
784
+ if (depth === 0)
785
+ break;
786
+ }
787
+ s += t.k === 'str' ? `'${t.v}'` : t.k === 'num' ? t.text : t.v;
788
+ if (t.k === 'op' && t.v === ',')
789
+ s += ' ';
790
+ }
791
+ s += ')';
792
+ }
793
+ return s;
794
+ }
795
+ primary() {
796
+ const t = this.next();
797
+ if (t.k === 'num') {
798
+ const isFloat = /[.eE]/.test(t.text) && !t.text.startsWith('0x');
799
+ return { k: 'lit', v: t.v, type: isFloat ? 'Float64' : literalIntType(t.v), text: t.text };
800
+ }
801
+ if (t.k === 'str')
802
+ return { k: 'lit', v: t.v, type: 'String', text: `'${t.v}'` };
803
+ if (t.k === 'op') {
804
+ if (t.v === '*')
805
+ return { k: 'star' };
806
+ if (t.v === '(') {
807
+ if (this.isWord('SELECT') || this.isWord('WITH')) {
808
+ const q = this.query();
809
+ this.expectOp(')');
810
+ return { k: 'subquery', q };
811
+ }
812
+ if (this.acceptOp(')'))
813
+ return { k: 'tuple', items: [] };
814
+ const first = this.expr();
815
+ if (this.acceptOp(',')) {
816
+ const items = [first];
817
+ do
818
+ items.push(this.expr());
819
+ while (this.acceptOp(','));
820
+ this.expectOp(')');
821
+ return { k: 'tuple', items };
822
+ }
823
+ this.expectOp(')');
824
+ return first;
825
+ }
826
+ if (t.v === '[') {
827
+ const items = [];
828
+ if (!this.acceptOp(']')) {
829
+ do
830
+ items.push(this.expr());
831
+ while (this.acceptOp(','));
832
+ this.expectOp(']');
833
+ }
834
+ return { k: 'array', items };
835
+ }
836
+ if (t.v === '{') {
837
+ const name = this.name();
838
+ this.expectOp(':');
839
+ const type = this.typeName();
840
+ this.expectOp('}');
841
+ return { k: 'param', name, type };
842
+ }
843
+ this.i--;
844
+ return this.syntax('unexpected token');
845
+ }
846
+ if (t.k === 'end') {
847
+ this.i--;
848
+ return this.syntax('unexpected end of query');
849
+ }
850
+ if (t.k === 'qid')
851
+ return this.identifierTail(t.v);
852
+ // A word.
853
+ switch (t.up) {
854
+ case 'NULL': return { k: 'lit', v: null, type: 'Nullable(Nothing)', text: 'NULL' };
855
+ case 'TRUE': return { k: 'lit', v: true, type: 'Bool', text: 'true' };
856
+ case 'FALSE': return { k: 'lit', v: false, type: 'Bool', text: 'false' };
857
+ case 'CASE': return this.caseExpr();
858
+ case 'INTERVAL': {
859
+ // `interval` is also an ordinary column name (`GROUP BY interval`); only a following
860
+ // value makes it an interval literal.
861
+ const nx = this.peek();
862
+ if (!(nx.k === 'num' || nx.k === 'str' || (nx.k === 'op' && (nx.v === '(' || nx.v === '-'))))
863
+ break;
864
+ const n = this.additive();
865
+ const u = this.next();
866
+ if (u.k !== 'word' || !INTERVAL_UNITS[u.up]) {
867
+ this.i--;
868
+ this.syntax('expected an interval unit');
869
+ }
870
+ return { k: 'interval', n, unit: INTERVAL_UNITS[u.up] };
871
+ }
872
+ case 'CAST': {
873
+ if (!this.isOp('('))
874
+ break;
875
+ this.i++;
876
+ const e = this.expr(false);
877
+ let type;
878
+ if (this.acceptWord('AS'))
879
+ type = this.typeName();
880
+ else {
881
+ this.expectOp(',');
882
+ const s = this.next();
883
+ if (s.k !== 'str')
884
+ this.syntax('expected a type string');
885
+ type = s.v;
886
+ }
887
+ this.expectOp(')');
888
+ return { k: 'cast', e, type };
889
+ }
890
+ case 'EXISTS': return fail('EXISTS is not supported by this twin');
891
+ case 'DATE':
892
+ case 'TIMESTAMP':
893
+ if (this.peek().k === 'str') {
894
+ const s = this.next();
895
+ return { k: 'fn', name: t.up === 'DATE' ? 'toDate' : 'toDateTime', args: [{ k: 'lit', v: s.v, type: 'String', text: `'${s.v}'` }] };
896
+ }
897
+ break;
898
+ default: break;
899
+ }
900
+ return this.identifierTail(t.v);
901
+ }
902
+ identifierTail(first) {
903
+ if (this.isOp('('))
904
+ return this.call(first);
905
+ const parts = [first];
906
+ while (this.isOp('.') && (this.peek(1).k === 'word' || this.peek(1).k === 'qid' || (this.peek(1).k === 'op' && this.peek(1).v === '*'))) {
907
+ this.i++;
908
+ const t = this.next();
909
+ if (t.k === 'op')
910
+ return { k: 'star', qualifier: parts.join('.') };
911
+ parts.push(t.v);
912
+ }
913
+ return { k: 'id', parts };
914
+ }
915
+ call(name) {
916
+ this.expectOp('(');
917
+ const args = this.argList(')');
918
+ // Parametric aggregate: quantile(0.5)(x).
919
+ if (this.isOp('(')) {
920
+ this.i++;
921
+ const real = this.argList(')');
922
+ return { k: 'fn', name, params: args.list, args: real.list, ...(real.distinct ? { distinct: true } : {}) };
923
+ }
924
+ if (this.isWord('OVER'))
925
+ fail('window functions (OVER) are not supported by this twin');
926
+ return { k: 'fn', name, args: args.list, ...(args.distinct ? { distinct: true } : {}) };
927
+ }
928
+ argList(close) {
929
+ const list = [];
930
+ let distinct = false;
931
+ if (this.acceptOp(close))
932
+ return { list, distinct };
933
+ if (this.acceptWord('DISTINCT'))
934
+ distinct = true;
935
+ do
936
+ list.push(this.expr());
937
+ while (this.acceptOp(','));
938
+ this.expectOp(close);
939
+ return { list, distinct };
940
+ }
941
+ caseExpr() {
942
+ let operand;
943
+ if (!this.isWord('WHEN'))
944
+ operand = this.expr();
945
+ const whens = [];
946
+ while (this.acceptWord('WHEN')) {
947
+ const c = this.expr();
948
+ this.expectWord('THEN');
949
+ whens.push([c, this.expr()]);
950
+ }
951
+ let otherwise;
952
+ if (this.acceptWord('ELSE'))
953
+ otherwise = this.expr();
954
+ this.expectWord('END');
955
+ if (whens.length === 0)
956
+ this.syntax('CASE needs at least one WHEN');
957
+ return { k: 'case', ...(operand ? { operand } : {}), whens, ...(otherwise ? { else: otherwise } : {}) };
958
+ }
959
+ }
960
+ /** Parse one SQL statement (a SELECT, possibly with UNION ALL and a trailing FORMAT). */
961
+ export function parseSql(sql) {
962
+ const trimmed = sql.trim();
963
+ // Leading comments (the `-- max_threads 2` a template hint renders) are not the statement.
964
+ const statement = trimmed.replace(/^(?:\s+|--[^\n]*(?:\n|$)|#[ !][^\n]*(?:\n|$)|\/\*[\s\S]*?\*\/)*/, '');
965
+ if (!/^(\(?\s*)*(SELECT|WITH)\b/i.test(statement)) {
966
+ const word = /^\w+/.exec(statement)?.[0] ?? statement.slice(0, 12);
967
+ fail(`Only SELECT queries are supported (got '${word}')`);
968
+ }
969
+ const q = new Parser(lex(trimmed)).parseQuery();
970
+ resolvePositional(q);
971
+ return q;
972
+ }
973
+ /**
974
+ * `GROUP BY 1` / `ORDER BY 2 DESC`: ClickHouse's `enable_positional_arguments` (on by default) makes
975
+ * an integer literal there name the SELECT item at that 1-based position.
976
+ */
977
+ function resolvePositional(q) {
978
+ const visitQuery = (query) => query.selects.forEach(visitSelect);
979
+ const visitSelect = (sel) => {
980
+ const at = (e) => {
981
+ if (e.k !== 'lit' || !(typeof e.v === 'bigint' || (typeof e.v === 'number' && Number.isInteger(e.v))) || e.type === 'Float64')
982
+ return e;
983
+ const n = Number(e.v);
984
+ if (n < 1 || n > sel.items.length)
985
+ fail(`Positional argument number ${n} is out of bounds. Expected in range [1, ${sel.items.length}]`);
986
+ const item = sel.items[n - 1];
987
+ if (item.k === 'star')
988
+ fail('Positional argument refers to an asterisk, which is not supported by this twin');
989
+ return item;
990
+ };
991
+ sel.groupBy = sel.groupBy.map(at);
992
+ sel.orderBy = sel.orderBy.map((o) => ({ ...o, e: at(o.e) }));
993
+ for (const w of sel.with)
994
+ if (w.query)
995
+ visitQuery(w.query);
996
+ for (const f of sel.from)
997
+ if (f.source.kind === 'sub')
998
+ visitQuery(f.source.query);
999
+ };
1000
+ visitQuery(q);
1001
+ }
1002
+ function makeSource(rel, alias, name) {
1003
+ const index = new Map();
1004
+ rel.columns.forEach((c, i) => { if (!index.has(c.name))
1005
+ index.set(c.name, i); });
1006
+ return { ...(alias !== undefined ? { alias } : {}), ...(name !== undefined ? { name } : {}), columns: rel.columns, index, ...(rel.declared ? { declared: true } : {}) };
1007
+ }
1008
+ const AGG_BASES = new Set([
1009
+ 'count', 'sum', 'avg', 'min', 'max', 'any', 'anylast', 'uniq', 'uniqexact', 'uniqcombined', 'uniqcombined64', 'uniqhll12',
1010
+ 'argmin', 'argmax', 'grouparray', 'grouparraysample', 'groupuniqarray', 'quantile', 'quantileexact', 'quantiles', 'quantilesexact', 'median', 'medianexact',
1011
+ 'countdistinct',
1012
+ ]);
1013
+ /** An aggregate function name with its combinators (`-If`, `-State`, `-Merge`), or undefined. */
1014
+ function parseAggregate(name) {
1015
+ let n = name.toLowerCase();
1016
+ // "first_value … is an alias for any" (https://clickhouse.com/docs/sql-reference/aggregate-functions/reference/first_value):
1017
+ // the first value met, NULLs ignored as every aggregate ignores them
1018
+ if (n === 'first_value')
1019
+ n = 'any';
1020
+ let mode = 'plain';
1021
+ if (n.endsWith('merge') && n.length > 5) {
1022
+ n = n.slice(0, -5);
1023
+ mode = 'merge';
1024
+ }
1025
+ else if (n.endsWith('state') && n.length > 5) {
1026
+ n = n.slice(0, -5);
1027
+ mode = 'state';
1028
+ }
1029
+ let cond = false;
1030
+ if (!AGG_BASES.has(n) && n.endsWith('if') && AGG_BASES.has(n.slice(0, -2))) {
1031
+ n = n.slice(0, -2);
1032
+ cond = true;
1033
+ }
1034
+ if (!AGG_BASES.has(n) || (cond && mode === 'merge'))
1035
+ return undefined;
1036
+ return { base: n, mode, cond };
1037
+ }
1038
+ const isAggregateName = (name) => parseAggregate(name) !== undefined;
1039
+ /** The name an `AggregateFunction(…)` state carries: the function plus its parameters. */
1040
+ function stateName(base, params) {
1041
+ return params.length ? `${base}(${params.map(valueToText).join(',')})` : base;
1042
+ }
1043
+ /** The result type of an aggregate call (ClickHouse's, including the -Null / -State / -Merge rules). */
1044
+ function aggregateType(e, T) {
1045
+ const agg = parseAggregate(e.name);
1046
+ const { base } = agg;
1047
+ const a0 = e.args[0];
1048
+ const params = (e.params ?? []).map((p) => (p.k === 'lit' ? p.v : exprText(p)));
1049
+ let argT = a0 && a0.k !== 'star' ? T(a0) : 'UInt64';
1050
+ if (agg.mode === 'state') {
1051
+ const argTypes = e.args.slice(0, agg.cond ? -1 : undefined).map((x) => (x.k === 'star' ? 'UInt64' : T(x)));
1052
+ return `AggregateFunction(${stateName(base, params)}${argTypes.length ? `, ${argTypes.join(', ')}` : ''})`;
1053
+ }
1054
+ if (agg.mode === 'merge')
1055
+ argT = typeArgs(argT)[1] ?? 'String';
1056
+ const inner = stripNullable(argT);
1057
+ let out;
1058
+ switch (base) {
1059
+ case 'count':
1060
+ case 'uniq':
1061
+ case 'uniqexact':
1062
+ case 'uniqcombined':
1063
+ case 'uniqcombined64':
1064
+ case 'uniqhll12':
1065
+ case 'countdistinct': return 'UInt64';
1066
+ case 'grouparray':
1067
+ case 'grouparraysample':
1068
+ case 'groupuniqarray': return `Array(${inner})`;
1069
+ case 'sum':
1070
+ out = isFloatType(inner) ? 'Float64' : isUnsignedType(inner) ? 'UInt64' : 'Int64';
1071
+ break;
1072
+ case 'avg':
1073
+ case 'quantile':
1074
+ case 'median':
1075
+ out = 'Float64';
1076
+ break;
1077
+ case 'quantiles': return 'Array(Float64)';
1078
+ case 'quantilesexact': return `Array(${inner})`;
1079
+ default: out = inner;
1080
+ }
1081
+ return isNullableType(argT) && agg.mode !== 'merge' ? `Nullable(${out})` : out;
1082
+ }
1083
+ function containsAggregate(e, aliases, seen = new Set()) {
1084
+ switch (e.k) {
1085
+ case 'fn':
1086
+ if (isAggregateName(e.name))
1087
+ return true;
1088
+ return e.args.some((a) => containsAggregate(a, aliases, seen));
1089
+ case 'alias': return containsAggregate(e.e, aliases, seen);
1090
+ case 'bin': return containsAggregate(e.a, aliases, seen) || containsAggregate(e.b, aliases, seen);
1091
+ case 'not':
1092
+ case 'neg': return containsAggregate(e.a, aliases, seen);
1093
+ case 'isnull': return containsAggregate(e.a, aliases, seen);
1094
+ case 'between': return [e.a, e.lo, e.hi].some((x) => containsAggregate(x, aliases, seen));
1095
+ case 'case': return (e.operand ? containsAggregate(e.operand, aliases, seen) : false) || e.whens.some(([a, b]) => containsAggregate(a, aliases, seen) || containsAggregate(b, aliases, seen)) || (e.else ? containsAggregate(e.else, aliases, seen) : false);
1096
+ case 'array':
1097
+ case 'tuple': return e.items.some((x) => containsAggregate(x, aliases, seen));
1098
+ case 'index': return containsAggregate(e.a, aliases, seen) || containsAggregate(e.i, aliases, seen);
1099
+ case 'interval': return containsAggregate(e.n, aliases, seen);
1100
+ case 'cast': return containsAggregate(e.e, aliases, seen);
1101
+ case 'lambda': return containsAggregate(e.body, aliases, seen);
1102
+ case 'id': {
1103
+ if (!aliases || e.parts.length !== 1)
1104
+ return false;
1105
+ const name = e.parts[0];
1106
+ const target = aliases.get(name);
1107
+ if (!target || seen.has(name))
1108
+ return false;
1109
+ seen.add(name);
1110
+ const r = containsAggregate(target, aliases, seen);
1111
+ seen.delete(name);
1112
+ return r;
1113
+ }
1114
+ default: return false;
1115
+ }
1116
+ }
1117
+ function collectAliases(e, out) {
1118
+ switch (e.k) {
1119
+ case 'alias': {
1120
+ const prev = out.get(e.name);
1121
+ // ClickHouse allows a name to alias the SAME expression twice, never two different ones.
1122
+ if (prev && !sameExpr(prev, e.e))
1123
+ fail(`Different expressions with the same alias ${e.name}`);
1124
+ if (!prev)
1125
+ out.set(e.name, e.e);
1126
+ collectAliases(e.e, out);
1127
+ return;
1128
+ }
1129
+ case 'fn':
1130
+ e.args.forEach((a) => collectAliases(a, out));
1131
+ return;
1132
+ case 'bin':
1133
+ collectAliases(e.a, out);
1134
+ collectAliases(e.b, out);
1135
+ return;
1136
+ case 'not':
1137
+ case 'neg':
1138
+ case 'isnull':
1139
+ collectAliases(e.a, out);
1140
+ return;
1141
+ case 'between':
1142
+ collectAliases(e.a, out);
1143
+ collectAliases(e.lo, out);
1144
+ collectAliases(e.hi, out);
1145
+ return;
1146
+ case 'case':
1147
+ if (e.operand)
1148
+ collectAliases(e.operand, out);
1149
+ e.whens.forEach(([a, b]) => { collectAliases(a, out); collectAliases(b, out); });
1150
+ if (e.else)
1151
+ collectAliases(e.else, out);
1152
+ return;
1153
+ case 'array':
1154
+ case 'tuple':
1155
+ e.items.forEach((x) => collectAliases(x, out));
1156
+ return;
1157
+ case 'index':
1158
+ collectAliases(e.a, out);
1159
+ collectAliases(e.i, out);
1160
+ return;
1161
+ case 'interval':
1162
+ collectAliases(e.n, out);
1163
+ return;
1164
+ case 'cast':
1165
+ collectAliases(e.e, out);
1166
+ return;
1167
+ default: return;
1168
+ }
1169
+ }
1170
+ function unalias(e) {
1171
+ return e.k === 'alias' ? unalias(e.e) : e;
1172
+ }
1173
+ /** The output column name ClickHouse gives an expression. */
1174
+ function exprName(e) {
1175
+ switch (e.k) {
1176
+ case 'alias': return e.name;
1177
+ case 'id': return e.parts[e.parts.length - 1];
1178
+ case 'lit': return e.text;
1179
+ case 'star': return '*';
1180
+ case 'fn': return `${e.name}${e.params ? `(${e.params.map(exprText).join(', ')})` : ''}(${e.distinct ? 'DISTINCT ' : ''}${e.args.map(exprText).join(', ')})`;
1181
+ default: return exprText(e);
1182
+ }
1183
+ }
1184
+ function exprText(e) {
1185
+ switch (e.k) {
1186
+ case 'alias': return e.name;
1187
+ case 'id': return e.parts.join('.');
1188
+ case 'lit': return e.text;
1189
+ case 'star': return e.qualifier ? `${e.qualifier}.*` : '*';
1190
+ case 'fn': return `${e.name}${e.params ? `(${e.params.map(exprText).join(', ')})` : ''}(${e.distinct ? 'DISTINCT ' : ''}${e.args.map(exprText).join(', ')})`;
1191
+ case 'lambda': return `lambda(tuple(${e.params.join(', ')}), ${exprText(e.body)})`;
1192
+ case 'bin': {
1193
+ const names = {
1194
+ '+': 'plus', '-': 'minus', '*': 'multiply', '/': 'divide', '%': 'modulo', '=': 'equals', '!=': 'notEquals',
1195
+ '<': 'less', '<=': 'lessOrEquals', '>': 'greater', '>=': 'greaterOrEquals', and: 'and', or: 'or', like: 'like',
1196
+ notlike: 'notLike', ilike: 'ilike', notilike: 'notILike', in: 'in', notin: 'notIn', '<=>': 'isNotDistinctFrom',
1197
+ };
1198
+ return `${names[e.op] ?? e.op}(${exprText(e.a)}, ${exprText(e.b)})`;
1199
+ }
1200
+ case 'not': return `not(${exprText(e.a)})`;
1201
+ case 'neg': return `negate(${exprText(e.a)})`;
1202
+ case 'isnull': return `${e.not ? 'isNotNull' : 'isNull'}(${exprText(e.a)})`;
1203
+ case 'between': return `${e.not ? 'not' : ''}between(${exprText(e.a)}, ${exprText(e.lo)}, ${exprText(e.hi)})`;
1204
+ case 'case': return 'multiIf(…)';
1205
+ case 'subquery': return '_subquery';
1206
+ case 'array': return `[${e.items.map(exprText).join(', ')}]`;
1207
+ case 'tuple': return `(${e.items.map(exprText).join(', ')})`;
1208
+ case 'index': return `arrayElement(${exprText(e.a)}, ${exprText(e.i)})`;
1209
+ case 'interval': return `toInterval${e.unit[0].toUpperCase()}${e.unit.slice(1)}(${exprText(e.n)})`;
1210
+ case 'cast': return `CAST(${exprText(e.e)}, '${e.type}')`;
1211
+ case 'param': return `{${e.name}:${e.type}}`;
1212
+ }
1213
+ }
1214
+ function sameExpr(a, b) {
1215
+ return JSON.stringify(unalias(a)) === JSON.stringify(unalias(b));
1216
+ }
1217
+ // ── identifier resolution ───────────────────────────────────────────────────────────────────
1218
+ function findColumn(scope, name) {
1219
+ for (let s = 0; s < scope.sources.length; s++) {
1220
+ const c = scope.sources[s].index.get(name);
1221
+ if (c !== undefined)
1222
+ return { s, c };
1223
+ }
1224
+ return undefined;
1225
+ }
1226
+ function findSource(scope, qualifier) {
1227
+ const byAlias = scope.sources.findIndex((s) => s.alias === qualifier);
1228
+ if (byAlias >= 0)
1229
+ return byAlias;
1230
+ const byName = scope.sources.findIndex((s) => s.alias === undefined && s.name === qualifier);
1231
+ if (byName >= 0)
1232
+ return byName;
1233
+ const byTableName = scope.sources.findIndex((s) => s.name === qualifier);
1234
+ return byTableName >= 0 ? byTableName : undefined;
1235
+ }
1236
+ function resolveId(parts, env) {
1237
+ const scope = env.scope;
1238
+ if (parts.length === 1) {
1239
+ const name = parts[0];
1240
+ if (env.lambda?.has(name))
1241
+ return env.lambda.get(name);
1242
+ const alias = scope.aliases.get(name);
1243
+ if (alias && !env.evaluating.has(name)) {
1244
+ if (env.aliasCache.has(name))
1245
+ return env.aliasCache.get(name);
1246
+ env.evaluating.add(name);
1247
+ try {
1248
+ const v = evalExpr(alias, env);
1249
+ env.aliasCache.set(name, v);
1250
+ return v;
1251
+ }
1252
+ finally {
1253
+ env.evaluating.delete(name);
1254
+ }
1255
+ }
1256
+ const col = findColumn(scope, name);
1257
+ if (col)
1258
+ return env.row[col.s][col.c];
1259
+ if (scope.withExprs.has(name))
1260
+ return withValue(scope, name);
1261
+ if (scope.outer) {
1262
+ const outerEnv = { scope: scope.outer, row: [], aliasCache: new Map(), evaluating: new Set() };
1263
+ return resolveId(parts, outerEnv);
1264
+ }
1265
+ return fail(`Missing columns: '${name}' while processing query`);
1266
+ }
1267
+ const qualifier = parts.slice(0, -1).join('.');
1268
+ const column = parts[parts.length - 1];
1269
+ const s = findSource(scope, qualifier);
1270
+ if (s !== undefined) {
1271
+ const c = scope.sources[s].index.get(column);
1272
+ if (c === undefined) {
1273
+ const whole = scope.sources[s].index.get(parts.join('.'));
1274
+ if (whole !== undefined)
1275
+ return env.row[s][whole];
1276
+ return fail(`Missing columns: '${parts.join('.')}' while processing query`);
1277
+ }
1278
+ return env.row[s][c];
1279
+ }
1280
+ // A column literally named `q.c` (a subquery's output), then the bare column.
1281
+ const whole = findColumn(scope, parts.join('.'));
1282
+ if (whole)
1283
+ return env.row[whole.s][whole.c];
1284
+ return resolveId([column], env);
1285
+ }
1286
+ function withValue(scope, name) {
1287
+ if (scope.withCache.has(name))
1288
+ return scope.withCache.get(name);
1289
+ const e = scope.withExprs.get(name);
1290
+ // A WITH constant sees the other WITH names but no row.
1291
+ const env = { scope: { ...scope, sources: [], aliases: new Map() }, row: [], aliasCache: new Map(), evaluating: new Set([name]) };
1292
+ const v = evalExpr(e, env);
1293
+ scope.withCache.set(name, v);
1294
+ return v;
1295
+ }
1296
+ // ── type inference (static) ─────────────────────────────────────────────────────────────────
1297
+ function columnType(scope, parts, seen) {
1298
+ if (parts.length === 1) {
1299
+ const name = parts[0];
1300
+ const alias = scope.aliases.get(name);
1301
+ if (alias && !seen.has(name)) {
1302
+ seen.add(name);
1303
+ const t = typeOf(alias, scope, seen);
1304
+ seen.delete(name);
1305
+ return t;
1306
+ }
1307
+ const col = findColumn(scope, name);
1308
+ if (col)
1309
+ return scope.sources[col.s].columns[col.c].type;
1310
+ const w = scope.withExprs.get(name);
1311
+ if (w && !seen.has(name)) {
1312
+ seen.add(name);
1313
+ const t = typeOf(w, { ...scope, aliases: new Map() }, seen);
1314
+ seen.delete(name);
1315
+ return t;
1316
+ }
1317
+ return undefined;
1318
+ }
1319
+ const s = findSource(scope, parts.slice(0, -1).join('.'));
1320
+ if (s !== undefined) {
1321
+ const c = scope.sources[s].index.get(parts[parts.length - 1]);
1322
+ return c === undefined ? undefined : scope.sources[s].columns[c].type;
1323
+ }
1324
+ const whole = findColumn(scope, parts.join('.'));
1325
+ if (whole)
1326
+ return scope.sources[whole.s].columns[whole.c].type;
1327
+ return columnType(scope, [parts[parts.length - 1]], seen);
1328
+ }
1329
+ const stripNullable = (t) => { const b = baseType(t); return /^LowCardinality\(/.test(t) && !b.nullable ? t : b.base; };
1330
+ const nullableOf = (t) => (isNullableType(t) || /^(Array|AggregateFunction|Nullable)\(/.test(t) ? t : `Nullable(${t.replace(/^LowCardinality\((.*)\)$/s, '$1')})`);
1331
+ function arrayElementType(t) {
1332
+ if (!t)
1333
+ return 'String';
1334
+ const { base } = baseType(t);
1335
+ return /^Array\(/.test(base) ? typeArgs(base)[0] ?? 'String' : 'String';
1336
+ }
1337
+ function typeOf(e, scope, seen = new Set(), lambdaTypes) {
1338
+ const T = (x) => typeOf(x, scope, seen, lambdaTypes);
1339
+ switch (e.k) {
1340
+ case 'lit': return e.type;
1341
+ case 'alias': {
1342
+ if (seen.has(e.name))
1343
+ return T(e.e);
1344
+ seen.add(e.name);
1345
+ const t = T(e.e);
1346
+ seen.delete(e.name);
1347
+ return t;
1348
+ }
1349
+ case 'id': {
1350
+ if (e.parts.length === 1 && lambdaTypes?.has(e.parts[0]))
1351
+ return lambdaTypes.get(e.parts[0]);
1352
+ return columnType(scope, e.parts, seen) ?? 'String';
1353
+ }
1354
+ case 'star': return 'String';
1355
+ case 'not':
1356
+ case 'isnull':
1357
+ case 'between': return 'UInt8';
1358
+ case 'neg': {
1359
+ const t = T(e.a);
1360
+ return isUnsignedType(t) ? t.replace('UInt', 'Int') : t;
1361
+ }
1362
+ case 'bin': {
1363
+ if (['=', '!=', '<', '<=', '>', '>=', 'like', 'notlike', 'ilike', 'notilike', 'in', 'notin', 'and', 'or', '<=>'].includes(e.op))
1364
+ return 'UInt8';
1365
+ const a = T(e.a);
1366
+ const b = T(e.b);
1367
+ const ha = typeHead(a);
1368
+ if (ha === 'DateTime' || ha === 'DateTime64' || ha === 'Date' || ha === 'Date32')
1369
+ return stripNullable(a);
1370
+ if (e.op === '/')
1371
+ return 'Float64';
1372
+ if (isFloatType(a) || isFloatType(b))
1373
+ return 'Float64';
1374
+ return 'Int64';
1375
+ }
1376
+ case 'case': return e.whens.length ? T(e.whens[0][1]) : 'String';
1377
+ case 'subquery': return 'String';
1378
+ case 'array': return `Array(${e.items.length ? stripNullable(T(e.items[0])) : 'Nothing'})`;
1379
+ case 'tuple': return `Tuple(${e.items.map(T).join(', ')})`;
1380
+ case 'index': return arrayElementType(T(e.a));
1381
+ case 'interval': return `Interval${e.unit[0].toUpperCase()}${e.unit.slice(1)}`;
1382
+ case 'cast':
1383
+ case 'param': return e.type;
1384
+ case 'lambda': return 'Function';
1385
+ case 'fn': return fnType(e, scope, seen, lambdaTypes);
1386
+ }
1387
+ }
1388
+ function fnType(e, scope, seen, lambdaTypes) {
1389
+ const T = (x) => typeOf(x, scope, seen, lambdaTypes);
1390
+ const name = e.name.toLowerCase();
1391
+ const a0 = e.args[0];
1392
+ if (isAggregateName(name))
1393
+ return aggregateType(e, T);
1394
+ switch (name) {
1395
+ case 'length': return 'UInt64';
1396
+ case 'if': return e.args[1] ? T(e.args[1]) : 'String';
1397
+ case 'multiif': return e.args[1] ? T(e.args[1]) : 'String';
1398
+ case 'coalesce':
1399
+ case 'ifnull': {
1400
+ const t = e.args.map(T).find((x) => !isNullableType(x)) ?? (a0 ? T(a0) : 'String');
1401
+ return stripNullable(t);
1402
+ }
1403
+ case 'assumenotnull': return a0 ? stripNullable(T(a0)) : 'String';
1404
+ case 'nullif': return nullableOf(a0 ? T(a0) : 'String');
1405
+ case 'touuid': return 'UUID';
1406
+ case 'touuidornull': return 'Nullable(UUID)';
1407
+ case 'isnull':
1408
+ case 'isnotnull':
1409
+ case 'startswith':
1410
+ case 'endswith':
1411
+ case 'has':
1412
+ case 'hasany':
1413
+ case 'hasall':
1414
+ case 'empty':
1415
+ case 'notempty':
1416
+ case 'like':
1417
+ case 'match':
1418
+ case 'equals':
1419
+ case 'notequals': return 'UInt8';
1420
+ case 'tolowcardinality': {
1421
+ const t = a0 ? T(a0) : 'String';
1422
+ return /^LowCardinality/.test(t) ? t : `LowCardinality(${t})`;
1423
+ }
1424
+ case 'tostring':
1425
+ case 'concat':
1426
+ case 'replace':
1427
+ case 'replaceall':
1428
+ case 'replaceone':
1429
+ case 'replaceregexpall':
1430
+ case 'replaceregexpone':
1431
+ case 'lower':
1432
+ case 'upper':
1433
+ case 'lowerutf8':
1434
+ case 'upperutf8':
1435
+ case 'substring':
1436
+ case 'substr':
1437
+ case 'trim':
1438
+ case 'trimboth':
1439
+ case 'trimleft':
1440
+ case 'trimright':
1441
+ case 'encodeurlformcomponent':
1442
+ case 'decodeurlformcomponent':
1443
+ case 'encodeurlcomponent':
1444
+ case 'decodeurlcomponent':
1445
+ case 'extracturlparameter':
1446
+ case 'jsonextractstring':
1447
+ case 'jsonextractraw':
1448
+ case 'formatdatetime':
1449
+ case 'domain':
1450
+ case 'domainwithoutwww':
1451
+ case 'path':
1452
+ case 'cutquerystring':
1453
+ case 'leftpad':
1454
+ case 'rightpad':
1455
+ case 'reverse':
1456
+ case 'base64encode':
1457
+ case 'base64decode':
1458
+ case 'hex':
1459
+ return 'String';
1460
+ case 'splitbystring':
1461
+ case 'splitbychar':
1462
+ case 'extracturlparameters':
1463
+ case 'extracturlparameternames': return 'Array(String)';
1464
+ case 'mapfilter': return e.args[1] ? T(e.args[1]) : 'Map(String, String)';
1465
+ case 'mapkeys':
1466
+ case 'mapvalues': {
1467
+ const m = a0 ? stripNullable(T(a0)) : 'Map(String, String)';
1468
+ const [k, v] = typeArgs(m);
1469
+ return `Array(${(name === 'mapkeys' ? k : v) ?? 'String'})`;
1470
+ }
1471
+ case 'mapcontains':
1472
+ case 'mapcontainskey': return 'UInt8';
1473
+ case 'totypename': return 'String';
1474
+ case 'lengthutf8':
1475
+ case 'char_length':
1476
+ case 'position':
1477
+ case 'locate': return 'UInt64';
1478
+ case 'greatest':
1479
+ case 'least': return a0 ? T(a0) : 'String';
1480
+ case 'totimezone': {
1481
+ const tz = e.args[1]?.k === 'lit' ? String(e.args[1].v) : undefined;
1482
+ return tz ? `DateTime('${tz}')` : 'DateTime';
1483
+ }
1484
+ case 'toyyyymmdd': return 'UInt32';
1485
+ case 'toyyyymmddhhmmss': return 'UInt64';
1486
+ case 'toquarter': return 'UInt8';
1487
+ case 'todayofyear': return 'UInt16';
1488
+ case 'tounixtimestamp64nano': return 'Int64';
1489
+ case 'fromunixtimestamp64micro':
1490
+ case 'fromunixtimestamp64nano': {
1491
+ const tz = e.args[1]?.k === 'lit' ? String(e.args[1].v) : undefined;
1492
+ const scale = name.endsWith('micro') ? 6 : 9;
1493
+ return tz ? `DateTime64(${scale}, '${tz}')` : `DateTime64(${scale})`;
1494
+ }
1495
+ case 'tostartofinterval':
1496
+ case 'time_bucket':
1497
+ case 'date_bin': {
1498
+ const iv = e.args[1];
1499
+ const unit = iv?.k === 'interval' ? iv.unit : iv?.k === 'fn' && /^tointerval/i.test(iv.name) ? iv.name.slice(10).toLowerCase() : 'second';
1500
+ const argT = a0 ? stripNullable(T(a0)) : 'DateTime';
1501
+ const tzLit = e.args[2]?.k === 'lit' ? String(e.args[2].v) : typeZone(argT);
1502
+ const zoned = (t) => (tzLit ? `${t}('${tzLit}')` : t);
1503
+ if (unit === 'millisecond')
1504
+ return tzLit ? `DateTime64(3, '${tzLit}')` : 'DateTime64(3)';
1505
+ if (unit === 'week' || unit === 'month' || unit === 'quarter' || unit === 'year')
1506
+ return 'Date';
1507
+ if (unit === 'day' && /^Date/.test(argT) && !/^DateTime/.test(argT))
1508
+ return 'DateTime';
1509
+ return zoned('DateTime');
1510
+ }
1511
+ case 'tofloat64':
1512
+ case 'tofloat32':
1513
+ case 'tofloat64orzero':
1514
+ case 'round':
1515
+ case 'floor':
1516
+ case 'ceil':
1517
+ case 'jsonextractfloat':
1518
+ case 'sqrt':
1519
+ case 'exp':
1520
+ case 'log':
1521
+ case 'pow':
1522
+ case 'power':
1523
+ case 'divide':
1524
+ if (name === 'round' || name === 'floor' || name === 'ceil')
1525
+ return a0 ? stripNullable(T(a0)) : 'Float64';
1526
+ return name === 'tofloat32' ? 'Float32' : 'Float64';
1527
+ case 'abs': return a0 ? T(a0) : 'Float64';
1528
+ case 'jsonextractint': return 'Int64';
1529
+ case 'jsonextractbool':
1530
+ case 'jsonhas': return 'UInt8';
1531
+ case 'tounixtimestamp64milli':
1532
+ case 'tounixtimestamp64micro':
1533
+ case 'datediff':
1534
+ case 'date_diff':
1535
+ case 'intdiv': return 'Int64';
1536
+ case 'tounixtimestamp': return 'UInt32';
1537
+ case 'toyyyymm': return 'UInt32';
1538
+ case 'toyear': return 'UInt16';
1539
+ case 'tomonth':
1540
+ case 'todayofmonth':
1541
+ case 'tohour':
1542
+ case 'tominute':
1543
+ case 'tosecond':
1544
+ case 'todayofweek': return 'UInt8';
1545
+ case 'todatetime64':
1546
+ case 'todatetime64orzero': {
1547
+ const scale = e.args[1]?.k === 'lit' ? Number(e.args[1].v) : 3;
1548
+ const tz = e.args[2]?.k === 'lit' ? String(e.args[2].v) : undefined;
1549
+ return tz ? `DateTime64(${scale}, '${tz}')` : `DateTime64(${scale})`;
1550
+ }
1551
+ case 'todatetime':
1552
+ case 'now':
1553
+ case 'tostartofday':
1554
+ case 'tostartofhour':
1555
+ case 'tostartofminute':
1556
+ case 'tostartoffiveminutes':
1557
+ case 'tostartoffifteenminutes':
1558
+ case 'tostartofsecond':
1559
+ case 'parsedatetimebesteffort':
1560
+ case 'parsedatetimebesteffortornull': {
1561
+ const tzArg = name === 'now' ? a0 : e.args[1];
1562
+ const tz = tzArg?.k === 'lit' ? String(tzArg.v) : undefined;
1563
+ return tz ? `DateTime('${tz}')` : 'DateTime';
1564
+ }
1565
+ case 'now64': return 'DateTime64(3)';
1566
+ case 'todate':
1567
+ case 'today':
1568
+ case 'tostartofmonth':
1569
+ case 'tostartofweek':
1570
+ case 'tostartofyear':
1571
+ case 'tostartofquarter':
1572
+ case 'tomonday':
1573
+ case 'yesterday': return 'Date';
1574
+ case 'arrayjoin': return arrayElementType(a0 ? T(a0) : undefined);
1575
+ case 'arrayelement': return arrayElementType(a0 ? T(a0) : undefined);
1576
+ case 'array': return `Array(${a0 ? stripNullable(T(a0)) : 'Nothing'})`;
1577
+ case 'range': return 'Array(UInt64)';
1578
+ case 'arrayfilter':
1579
+ case 'arraydistinct':
1580
+ case 'arraysort':
1581
+ case 'arrayreversesort':
1582
+ case 'arrayslice':
1583
+ case 'arrayreverse':
1584
+ case 'arrayconcat':
1585
+ case 'arraypushback':
1586
+ case 'arraypushfront':
1587
+ return name === 'arrayfilter' ? (e.args[1] ? T(e.args[1]) : 'Array(String)') : a0 ? T(a0) : 'Array(String)';
1588
+ case 'arrayintersect': return a0 ? T(a0) : 'Array(String)';
1589
+ case 'arraymap': {
1590
+ const lam = a0;
1591
+ const arrT = e.args[1] ? T(e.args[1]) : 'Array(String)';
1592
+ if (lam?.k === 'lambda') {
1593
+ const lt = new Map(lambdaTypes ?? []);
1594
+ lam.params.forEach((p, i) => lt.set(p, arrayElementType(e.args[i + 1] ? T(e.args[i + 1]) : arrT)));
1595
+ return `Array(${typeOf(lam.body, scope, seen, lt)})`;
1596
+ }
1597
+ return arrT;
1598
+ }
1599
+ case 'indexof':
1600
+ case 'arraycount': return 'UInt64';
1601
+ case 'arraysum': return 'Float64';
1602
+ default: {
1603
+ if (name.startsWith('tointerval'))
1604
+ return `Interval${e.name.slice(10)}`;
1605
+ const conv = /^to(u?int(?:8|16|32|64|128|256)|float(?:32|64))(orzero|ornull)?$/.exec(name);
1606
+ if (conv) {
1607
+ const t = conv[1].replace(/^u?int|^float/, (m) => (m === 'uint' ? 'UInt' : m === 'int' ? 'Int' : 'Float'));
1608
+ return conv[2] === 'ornull' ? `Nullable(${t})` : t;
1609
+ }
1610
+ return 'String';
1611
+ }
1612
+ }
1613
+ }
1614
+ // ── scalar evaluation ───────────────────────────────────────────────────────────────────────
1615
+ function truth(v) {
1616
+ if (v === null || v === undefined)
1617
+ return null;
1618
+ if (typeof v === 'boolean')
1619
+ return v;
1620
+ if (typeof v === 'number')
1621
+ return v !== 0;
1622
+ if (typeof v === 'bigint')
1623
+ return v !== 0n;
1624
+ if (typeof v === 'string') {
1625
+ if (v === '' || v === '0')
1626
+ return false;
1627
+ const n = Number(v);
1628
+ if (!Number.isNaN(n))
1629
+ return n !== 0;
1630
+ return fail(`Cannot convert string '${v}' to a boolean condition`);
1631
+ }
1632
+ return true;
1633
+ }
1634
+ const u8 = (b) => (b === null ? null : b ? 1 : 0);
1635
+ function asNumber(v) {
1636
+ if (v === null || v === undefined)
1637
+ return null;
1638
+ if (typeof v === 'number')
1639
+ return v;
1640
+ if (typeof v === 'bigint')
1641
+ return Number(v);
1642
+ if (typeof v === 'boolean')
1643
+ return v ? 1 : 0;
1644
+ if (v instanceof DT)
1645
+ return v.ms / 1000 + v.ns / 1e9;
1646
+ if (v instanceof DDate)
1647
+ return v.days;
1648
+ if (typeof v === 'string') {
1649
+ const n = Number(v);
1650
+ if (v.trim() === '' || Number.isNaN(n))
1651
+ return fail(`Cannot parse number from string '${v}'`);
1652
+ return n;
1653
+ }
1654
+ return fail(`Illegal type for arithmetic: ${valueToText(v)}`);
1655
+ }
1656
+ /** An arithmetic operand that is an exact integer (a safe-integer number or a bigint)? */
1657
+ const isExactInt = (v) => typeof v === 'bigint' || (typeof v === 'number' && Number.isInteger(v)) || typeof v === 'boolean';
1658
+ function likeToRegex(pattern, insensitive) {
1659
+ let re = '^';
1660
+ for (let i = 0; i < pattern.length; i++) {
1661
+ const c = pattern[i];
1662
+ if (c === '\\' && i + 1 < pattern.length) {
1663
+ re += pattern[++i].replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1664
+ continue;
1665
+ }
1666
+ if (c === '%')
1667
+ re += '[\\s\\S]*';
1668
+ else if (c === '_')
1669
+ re += '[\\s\\S]';
1670
+ else
1671
+ re += c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1672
+ }
1673
+ return new RegExp(`${re}$`, insensitive ? 'i' : '');
1674
+ }
1675
+ const LIKE_CACHE = new Map();
1676
+ function likeMatch(value, pattern, insensitive) {
1677
+ const key = `${insensitive ? 'i' : 's'}:${pattern}`;
1678
+ let re = LIKE_CACHE.get(key);
1679
+ if (!re) {
1680
+ re = likeToRegex(pattern, insensitive);
1681
+ LIKE_CACHE.set(key, re);
1682
+ }
1683
+ return re.test(value);
1684
+ }
1685
+ function addInterval(v, iv, sign) {
1686
+ const n = iv.n * sign;
1687
+ if (v instanceof DDate) {
1688
+ if (iv.unit === 'day')
1689
+ return new DDate(v.days + n);
1690
+ if (iv.unit === 'week')
1691
+ return new DDate(v.days + 7 * n);
1692
+ if (iv.unit === 'month' || iv.unit === 'quarter' || iv.unit === 'year') {
1693
+ const months = iv.unit === 'month' ? n : iv.unit === 'quarter' ? 3 * n : 12 * n;
1694
+ const c = civilFromDays(v.days);
1695
+ return new DDate(addMonthsCivil(c.y, c.mo, c.d, months));
1696
+ }
1697
+ return addInterval(castDateTime(v, 0, SERVER_TZ), iv, sign);
1698
+ }
1699
+ if (v instanceof DT) {
1700
+ const tz = v.tz ?? SERVER_TZ;
1701
+ switch (iv.unit) {
1702
+ case 'millisecond': return new DT(v.ms + n, v.scale, v.tz, v.ns);
1703
+ case 'second': return new DT(v.ms + n * 1000, v.scale, v.tz, v.ns);
1704
+ case 'minute': return new DT(v.ms + n * 60_000, v.scale, v.tz, v.ns);
1705
+ case 'hour': return new DT(v.ms + n * 3_600_000, v.scale, v.tz, v.ns);
1706
+ case 'day':
1707
+ case 'week': {
1708
+ const w = wallOf(v.ms, tz);
1709
+ const days = daysFromCivil(w.y, w.mo, w.d) + (iv.unit === 'day' ? n : 7 * n);
1710
+ const c = civilFromDays(days);
1711
+ return new DT(instantOf({ ...w, ...c }, tz), v.scale, v.tz, v.ns);
1712
+ }
1713
+ default: {
1714
+ const months = iv.unit === 'month' ? n : iv.unit === 'quarter' ? 3 * n : 12 * n;
1715
+ const w = wallOf(v.ms, tz);
1716
+ const c = civilFromDays(addMonthsCivil(w.y, w.mo, w.d, months));
1717
+ return new DT(instantOf({ ...w, ...c }, tz), v.scale, v.tz, v.ns);
1718
+ }
1719
+ }
1720
+ }
1721
+ if (typeof v === 'string')
1722
+ return addInterval(castDateTime(v, 0), iv, sign);
1723
+ return fail(`Illegal types for interval arithmetic: ${valueToText(v)}`);
1724
+ }
1725
+ function addMonthsCivil(y, mo, d, months) {
1726
+ const total = y * 12 + (mo - 1) + months;
1727
+ const ny = Math.floor(total / 12);
1728
+ const nm = total - ny * 12 + 1;
1729
+ const dim = new Date(Date.UTC(ny, nm, 0)).getUTCDate();
1730
+ return daysFromCivil(ny, nm, Math.min(d, dim));
1731
+ }
1732
+ function arithmetic(op, a, b) {
1733
+ if (a === null || b === null || a === undefined || b === undefined)
1734
+ return null;
1735
+ if (b instanceof Interval && (op === '+' || op === '-'))
1736
+ return addInterval(a, b, op === '+' ? 1 : -1);
1737
+ if (a instanceof Interval && op === '+')
1738
+ return addInterval(b, a, 1);
1739
+ if ((a instanceof DT || a instanceof DDate) && typeof b === 'number' && (op === '+' || op === '-')) {
1740
+ // DateTime ± n is seconds; Date ± n is days.
1741
+ return a instanceof DT ? new DT(a.ms + (op === '+' ? 1 : -1) * b * 1000, a.scale, a.tz, a.ns) : new DDate(a.days + (op === '+' ? 1 : -1) * b);
1742
+ }
1743
+ if (op === '-' && a instanceof DT && b instanceof DT)
1744
+ return (a.ms - b.ms) / 1000 + (a.ns - b.ns) / 1e9;
1745
+ if (op === '-' && a instanceof DDate && b instanceof DDate)
1746
+ return a.days - b.days;
1747
+ // Integer arithmetic is exact (ClickHouse's Int64/UInt64 never round through a double).
1748
+ if (isExactInt(a) && isExactInt(b) && op !== '/') {
1749
+ const x = toBigInt(a);
1750
+ const y = toBigInt(b);
1751
+ if (op === '+')
1752
+ return normInt(x + y);
1753
+ if (op === '-')
1754
+ return normInt(x - y);
1755
+ if (op === '*')
1756
+ return normInt(x * y);
1757
+ if (op === '%') {
1758
+ if (y === 0n)
1759
+ return fail('Division by zero');
1760
+ return normInt(x % y);
1761
+ }
1762
+ }
1763
+ const x = asNumber(a);
1764
+ const y = asNumber(b);
1765
+ switch (op) {
1766
+ case '+': return x + y;
1767
+ case '-': return x - y;
1768
+ case '*': return x * y;
1769
+ case '/': return x / y;
1770
+ case '%': {
1771
+ if (y === 0)
1772
+ return fail('Division by zero');
1773
+ return x % y;
1774
+ }
1775
+ }
1776
+ return fail(`Unknown operator ${op}`);
1777
+ }
1778
+ function toSetKeys(v) {
1779
+ if (Array.isArray(v))
1780
+ return v;
1781
+ return [v];
1782
+ }
1783
+ function evalIn(e, env) {
1784
+ const left = evalExpr(e.a, env);
1785
+ let members;
1786
+ const rhs = e.b;
1787
+ if (rhs.k === 'subquery') {
1788
+ let cached = env.scope.subCache.get(rhs);
1789
+ if (!cached) {
1790
+ const rel = env.scope.exec.run(rhs.q, env.scope);
1791
+ cached = rel.rows.map((r) => (r.length === 1 ? r[0] : r));
1792
+ env.scope.subCache.set(rhs, cached);
1793
+ }
1794
+ members = cached;
1795
+ }
1796
+ else if (rhs.k === 'tuple') {
1797
+ members = rhs.items.map((x) => evalExpr(x, env));
1798
+ }
1799
+ else if (rhs.k === 'id' && rhs.parts.length === 1 && !findColumn(env.scope, rhs.parts[0]) && !env.scope.aliases.has(rhs.parts[0]) && !env.scope.withExprs.has(rhs.parts[0])) {
1800
+ // `x IN table_or_node` — the relation's first column.
1801
+ let cached = env.scope.subCache.get(rhs);
1802
+ if (!cached) {
1803
+ const rel = env.scope.exec.table(rhs.parts[0], false, env.scope);
1804
+ cached = rel.rows.map((r) => r[0]);
1805
+ env.scope.subCache.set(rhs, cached);
1806
+ }
1807
+ members = cached;
1808
+ }
1809
+ else {
1810
+ members = toSetKeys(evalExpr(rhs, env));
1811
+ }
1812
+ if (left === null || left === undefined)
1813
+ return null;
1814
+ // ClickHouse's `transform_null_in = 0`: NULL members never match.
1815
+ const hit = members.some((m) => m !== null && m !== undefined && valueKeyAs(m, left) === valueKey(left));
1816
+ const negate = e.op === 'notin';
1817
+ return hit !== negate ? 1 : 0;
1818
+ }
1819
+ export function evalExpr(e, env) {
1820
+ switch (e.k) {
1821
+ case 'lit': return e.v;
1822
+ case 'alias': {
1823
+ // Inside its own definition a name means the column, never the alias (`f(x) AS x`).
1824
+ if (env.evaluating.has(e.name))
1825
+ return evalExpr(e.e, env);
1826
+ env.evaluating.add(e.name);
1827
+ try {
1828
+ return evalExpr(e.e, env);
1829
+ }
1830
+ finally {
1831
+ env.evaluating.delete(e.name);
1832
+ }
1833
+ }
1834
+ case 'id': return resolveId(e.parts, env);
1835
+ case 'star': return fail('* is only allowed in a SELECT list or count(*)');
1836
+ case 'not': {
1837
+ const t = truth(evalExpr(e.a, env));
1838
+ return t === null ? null : t ? 0 : 1;
1839
+ }
1840
+ case 'neg': {
1841
+ const v = evalExpr(e.a, env);
1842
+ if (v === null)
1843
+ return null;
1844
+ if (v instanceof Interval)
1845
+ return new Interval(v.unit, -v.n);
1846
+ if (typeof v === 'bigint')
1847
+ return normInt(-v);
1848
+ return -asNumber(v);
1849
+ }
1850
+ case 'isnull': {
1851
+ const v = evalExpr(e.a, env);
1852
+ return (v === null || v === undefined) !== e.not ? 1 : 0;
1853
+ }
1854
+ case 'between': {
1855
+ const v = evalExpr(e.a, env);
1856
+ const lo = evalExpr(e.lo, env);
1857
+ const hi = evalExpr(e.hi, env);
1858
+ if (v === null || lo === null || hi === null)
1859
+ return null;
1860
+ const r = compareValues(v, lo) >= 0 && compareValues(v, hi) <= 0;
1861
+ return r !== e.not ? 1 : 0;
1862
+ }
1863
+ case 'case': {
1864
+ if (e.operand) {
1865
+ const v = evalExpr(e.operand, env);
1866
+ for (const [w, t] of e.whens) {
1867
+ const c = evalExpr(w, env);
1868
+ if (v !== null && c !== null && valuesEqual(v, c))
1869
+ return evalExpr(t, env);
1870
+ }
1871
+ }
1872
+ else {
1873
+ for (const [w, t] of e.whens)
1874
+ if (truth(evalExpr(w, env)) === true)
1875
+ return evalExpr(t, env);
1876
+ }
1877
+ return e.else ? evalExpr(e.else, env) : null;
1878
+ }
1879
+ case 'subquery': {
1880
+ if (env.scope.subCache.has(e))
1881
+ return env.scope.subCache.get(e);
1882
+ const rel = env.scope.exec.run(e.q, env.scope);
1883
+ const v = rel.rows.length === 0 ? defaultOf(rel.columns[0]?.type ?? 'Nullable(String)') : rel.rows[0].length === 1 ? rel.rows[0][0] : rel.rows[0];
1884
+ env.scope.subCache.set(e, v);
1885
+ return v;
1886
+ }
1887
+ case 'array': return e.items.map((x) => evalExpr(x, env));
1888
+ case 'tuple': return e.items.map((x) => evalExpr(x, env));
1889
+ case 'index': {
1890
+ const arr = evalExpr(e.a, env);
1891
+ const idx = asNumber(evalExpr(e.i, env));
1892
+ if (arr === null || idx === null)
1893
+ return null;
1894
+ if (!Array.isArray(arr))
1895
+ return fail(`arrayElement: first argument must be an array, got ${valueToText(arr)}`);
1896
+ return arrayAt(arr, idx);
1897
+ }
1898
+ case 'interval': {
1899
+ const n = asNumber(evalExpr(e.n, env));
1900
+ return n === null ? null : new Interval(e.unit, n);
1901
+ }
1902
+ case 'cast': return castTo(evalExpr(e.e, env), e.type);
1903
+ // No request supplies ClickHouse query parameters (this twin stores no Workspace secrets);
1904
+ // publish-time analysis types the parameter without a value, as the vendor's check does.
1905
+ case 'param':
1906
+ if (env.scope.exec.validating)
1907
+ return defaultOf(e.type);
1908
+ return fail(`Substitution \`${e.name}\` is not set`);
1909
+ case 'lambda': return fail('a lambda is only allowed as a higher-order function argument');
1910
+ case 'bin': {
1911
+ if (e.op === 'and') {
1912
+ const a = truth(evalExpr(e.a, env));
1913
+ if (a === false)
1914
+ return 0;
1915
+ const b = truth(evalExpr(e.b, env));
1916
+ if (b === false)
1917
+ return 0;
1918
+ return a === null || b === null ? null : 1;
1919
+ }
1920
+ if (e.op === 'or') {
1921
+ const a = truth(evalExpr(e.a, env));
1922
+ if (a === true)
1923
+ return 1;
1924
+ const b = truth(evalExpr(e.b, env));
1925
+ if (b === true)
1926
+ return 1;
1927
+ return a === null || b === null ? null : 0;
1928
+ }
1929
+ if (e.op === 'in' || e.op === 'notin')
1930
+ return evalIn(e, env);
1931
+ const a = evalExpr(e.a, env);
1932
+ const b = evalExpr(e.b, env);
1933
+ if (['+', '-', '*', '/', '%'].includes(e.op))
1934
+ return arithmetic(e.op, a, b);
1935
+ if (e.op === '<=>')
1936
+ return (a === null && b === null) || (a !== null && b !== null && valuesEqual(a, b)) ? 1 : 0;
1937
+ if (a === null || b === null || a === undefined || b === undefined)
1938
+ return null;
1939
+ if (e.op === 'like' || e.op === 'notlike' || e.op === 'ilike' || e.op === 'notilike') {
1940
+ const r = likeMatch(valueToText(a), valueToText(b), e.op.includes('ilike'));
1941
+ return (e.op.startsWith('not') ? !r : r) ? 1 : 0;
1942
+ }
1943
+ const c = compareValues(a, b);
1944
+ switch (e.op) {
1945
+ case '=': return c === 0 ? 1 : 0;
1946
+ case '!=': return c !== 0 ? 1 : 0;
1947
+ case '<': return c < 0 ? 1 : 0;
1948
+ case '<=': return c <= 0 ? 1 : 0;
1949
+ case '>': return c > 0 ? 1 : 0;
1950
+ case '>=': return c >= 0 ? 1 : 0;
1951
+ }
1952
+ return fail(`Unknown operator ${e.op}`);
1953
+ }
1954
+ case 'fn': return evalFunction(e, env);
1955
+ }
1956
+ }
1957
+ function arrayAt(arr, idx) {
1958
+ const i = idx > 0 ? idx - 1 : arr.length + idx;
1959
+ if (idx === 0 || i < 0 || i >= arr.length) {
1960
+ const sample = arr.find((x) => x !== null && x !== undefined);
1961
+ if (typeof sample === 'number')
1962
+ return 0;
1963
+ if (sample instanceof DT)
1964
+ return new DT(0, sample.scale, sample.tz);
1965
+ if (Array.isArray(sample))
1966
+ return [];
1967
+ return '';
1968
+ }
1969
+ return arr[i];
1970
+ }
1971
+ function castTo(v, type) {
1972
+ if (v === null || v === undefined)
1973
+ return isNullableType(type) ? null : fail(`Cannot convert NULL value to non-Nullable type ${type}`);
1974
+ const h = typeHead(type);
1975
+ if (h === 'String' || h === 'FixedString')
1976
+ return valueToText(v);
1977
+ if (h === 'DateTime' || h === 'DateTime64')
1978
+ return castDateTime(v, typeScale(type), typeZone(type));
1979
+ if (h === 'Date' || h === 'Date32')
1980
+ return toDateValue(v);
1981
+ if (h === 'Bool' || h === 'Boolean')
1982
+ return truth(v) === true;
1983
+ if (isIntegerType(type))
1984
+ return toIntegerOf(v, h);
1985
+ if (isFloatType(type))
1986
+ return asNumber(v);
1987
+ if (h === 'Array') {
1988
+ if (!Array.isArray(v))
1989
+ return fail(`CAST: cannot convert ${valueToText(v)} to ${type}`);
1990
+ const inner = typeArgs(type)[0] ?? 'String';
1991
+ return v.map((x) => castTo(x, inner));
1992
+ }
1993
+ return v;
1994
+ }
1995
+ /**
1996
+ * `toIntN(x)` / `CAST(x AS IntN)`: an integer wrapped to N bits. A string must be integer text
1997
+ * (ClickHouse: "Cannot parse string '5.5' as Int32"); a float truncates toward zero.
1998
+ */
1999
+ function toIntegerOf(v, type) {
2000
+ if (typeof v === 'string' && !/^[+-]?\d+$/.test(v)) {
2001
+ fail(`Cannot parse string '${v}' as ${type}: syntax error at begin of string. Note: there are to${type}OrZero and to${type}OrNull functions, which returns zero/NULL instead of throwing exception`);
2002
+ }
2003
+ return wrapInt(toBigInt(v, type), type);
2004
+ }
2005
+ /** `toIntNOrZero` / `toIntNOrNull` / `toFloatNOrZero|OrNull`: the parse, or 0 / NULL when it fails. */
2006
+ function toNumberOrElse(v, type, orNull) {
2007
+ const miss = orNull ? null : 0;
2008
+ if (v === null)
2009
+ return null;
2010
+ const text = typeof v === 'string' ? v : valueToText(v);
2011
+ if (/^Float/.test(type)) {
2012
+ if (!/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$|^[+-]?(inf|nan)$/i.test(text))
2013
+ return miss;
2014
+ return Number(text.replace(/inf/i, 'Infinity'));
2015
+ }
2016
+ if (!/^[+-]?\d+$/.test(text))
2017
+ return miss;
2018
+ const n = BigInt(text);
2019
+ if (type.startsWith('U') && n < 0n)
2020
+ return miss;
2021
+ const w = wrapInt(n, type);
2022
+ return BigInt(w) === n ? w : miss;
2023
+ }
2024
+ function toDateValue(v, tz) {
2025
+ if (v instanceof DDate)
2026
+ return v;
2027
+ if (v instanceof DT) {
2028
+ const w = wallOf(v.ms, tz ?? v.tz);
2029
+ return new DDate(daysFromCivil(w.y, w.mo, w.d));
2030
+ }
2031
+ if (typeof v === 'number')
2032
+ return v > 65535 ? toDateValue(new DT(v * 1000, 0), tz) : new DDate(v);
2033
+ if (typeof v === 'string') {
2034
+ const d = parseDateText(v);
2035
+ if (d === undefined)
2036
+ return fail(`Cannot parse Date from String '${v}'`);
2037
+ return new DDate(d);
2038
+ }
2039
+ return fail(`Cannot convert ${valueToText(v)} to Date`);
2040
+ }
2041
+ function tzArg(e, i, env) {
2042
+ const a = e.args[i];
2043
+ if (!a)
2044
+ return undefined;
2045
+ const v = evalExpr(a, env);
2046
+ if (v === null)
2047
+ return undefined;
2048
+ const s = valueToText(v);
2049
+ wallOf(0, s); // validates the zone
2050
+ return s;
2051
+ }
2052
+ function toDateTimeArg(v) {
2053
+ if (v instanceof DT)
2054
+ return v;
2055
+ return castDateTime(v, 0);
2056
+ }
2057
+ function startOf(v, unit, tz) {
2058
+ if (v === null)
2059
+ return null;
2060
+ const dt = v instanceof DDate ? castDateTime(v, 0, tz) : toDateTimeArg(v);
2061
+ const zone = tz ?? dt.tz ?? SERVER_TZ;
2062
+ const w = wallOf(dt.ms, zone);
2063
+ let wall = { ...w, ms: 0 };
2064
+ if (unit === 'minute')
2065
+ wall = { ...wall, s: 0 };
2066
+ if (unit === 'fiveMinutes')
2067
+ wall = { ...wall, s: 0, mi: w.mi - (w.mi % 5) };
2068
+ if (unit === 'fifteenMinutes')
2069
+ wall = { ...wall, s: 0, mi: w.mi - (w.mi % 15) };
2070
+ if (unit === 'hour')
2071
+ wall = { ...wall, s: 0, mi: 0 };
2072
+ if (unit === 'day')
2073
+ wall = { ...wall, s: 0, mi: 0, h: 0 };
2074
+ return new DT(instantOf(wall, zone), 0, tz ?? dt.tz);
2075
+ }
2076
+ /**
2077
+ * `toStartOfInterval(t, INTERVAL n unit[, tz])`, per ClickHouse's DateLUT (checked against
2078
+ * clickhouse-local 26.10): sub-second and second/minute buckets count from the epoch in UTC ticks;
2079
+ * hour buckets floor the wall-clock hour of the local day; day buckets count local days from the
2080
+ * epoch and return a DateTime; weeks (Monday-aligned from 1970-01-05), months (from 1970-01),
2081
+ * quarters and years (from year 0) return a Date. A Date argument accepts day units and above.
2082
+ */
2083
+ function startOfInterval(v, iv, tz) {
2084
+ const n = Math.trunc(iv.n);
2085
+ if (n <= 0)
2086
+ return fail('Value for second argument of function toStartOfInterval must be positive');
2087
+ const isDate = v instanceof DDate;
2088
+ if (isDate && ['millisecond', 'second', 'minute', 'hour'].includes(iv.unit)) {
2089
+ return fail('Illegal interval kind for argument data type Date');
2090
+ }
2091
+ const dt = v instanceof DDate ? castDateTime(v, 0, tz) : v instanceof DT ? v : castDateTime(v, 0, tz);
2092
+ const zone = tz ?? dt.tz ?? SERVER_TZ;
2093
+ const outTz = tz ?? dt.tz;
2094
+ switch (iv.unit) {
2095
+ case 'millisecond': {
2096
+ const step = BigInt(n);
2097
+ const ms = BigInt(dt.ms);
2098
+ const floored = (ms >= 0n ? ms / step : -((-ms + step - 1n) / step)) * step;
2099
+ return new DT(Number(floored), 3, outTz);
2100
+ }
2101
+ case 'second':
2102
+ case 'minute': {
2103
+ const step = (iv.unit === 'second' ? 1000 : 60_000) * n;
2104
+ return new DT(Math.floor(dt.ms / step) * step, 0, outTz);
2105
+ }
2106
+ case 'hour': {
2107
+ const w = wallOf(dt.ms, zone);
2108
+ return new DT(instantOf({ ...w, h: w.h - (w.h % n), mi: 0, s: 0, ms: 0 }, zone), 0, outTz);
2109
+ }
2110
+ case 'day': {
2111
+ const w = wallOf(dt.ms, zone);
2112
+ const days = daysFromCivil(w.y, w.mo, w.d);
2113
+ const c = civilFromDays(Math.floor(days / n) * n);
2114
+ return new DT(instantOf({ ...c, h: 0, mi: 0, s: 0, ms: 0 }, zone), 0, isDate ? undefined : outTz);
2115
+ }
2116
+ case 'week': {
2117
+ const days = toDateValue(dt, zone).days;
2118
+ const week = Math.floor((days - 4) / 7);
2119
+ return new DDate(4 + Math.floor(week / n) * n * 7);
2120
+ }
2121
+ default: {
2122
+ const c = civilFromDays(toDateValue(dt, zone).days);
2123
+ if (iv.unit === 'year')
2124
+ return new DDate(daysFromCivil(Math.floor(c.y / n) * n, 1, 1));
2125
+ const months = iv.unit === 'quarter' ? 3 * n : n;
2126
+ const idx = (c.y - 1970) * 12 + (c.mo - 1);
2127
+ const start = Math.floor(idx / months) * months;
2128
+ return new DDate(daysFromCivil(1970 + Math.floor(start / 12), (start % 12 + 12) % 12 + 1, 1));
2129
+ }
2130
+ }
2131
+ }
2132
+ function relativeNumber(v, unit, tz) {
2133
+ const dt = v instanceof DDate ? castDateTime(v, 0, tz) : v;
2134
+ const zone = tz ?? dt.tz ?? SERVER_TZ;
2135
+ const w = wallOf(dt.ms, zone);
2136
+ const days = daysFromCivil(w.y, w.mo, w.d);
2137
+ switch (unit) {
2138
+ case 'year':
2139
+ case 'yy':
2140
+ case 'yyyy': return w.y;
2141
+ case 'quarter':
2142
+ case 'qq':
2143
+ case 'q': return w.y * 4 + Math.floor((w.mo - 1) / 3);
2144
+ case 'month':
2145
+ case 'mm':
2146
+ case 'm': return w.y * 12 + w.mo - 1;
2147
+ case 'week':
2148
+ case 'wk':
2149
+ case 'ww': return Math.floor((days + 3) / 7); // weeks start Monday
2150
+ case 'day':
2151
+ case 'dd':
2152
+ case 'd': return days;
2153
+ case 'hour':
2154
+ case 'hh':
2155
+ case 'h': return days * 24 + w.h;
2156
+ case 'minute':
2157
+ case 'mi':
2158
+ case 'n': return (days * 24 + w.h) * 60 + w.mi;
2159
+ case 'second':
2160
+ case 'ss':
2161
+ case 's': return Math.floor(dt.ms / 1000);
2162
+ case 'millisecond': return dt.ms;
2163
+ default: return fail(`dateDiff: unknown unit '${unit}'`);
2164
+ }
2165
+ }
2166
+ function jsonPathValue(json, keys) {
2167
+ let cur;
2168
+ if (typeof json === 'string') {
2169
+ try {
2170
+ cur = JSON.parse(json);
2171
+ }
2172
+ catch {
2173
+ return undefined;
2174
+ }
2175
+ }
2176
+ else
2177
+ cur = json;
2178
+ for (const k of keys) {
2179
+ if (cur === null || typeof cur !== 'object')
2180
+ return undefined;
2181
+ if (Array.isArray(cur)) {
2182
+ const i = typeof k === 'number' ? (k > 0 ? k - 1 : cur.length + k) : Number.NaN;
2183
+ cur = cur[i];
2184
+ }
2185
+ else {
2186
+ cur = cur[valueToText(k)];
2187
+ }
2188
+ if (cur === undefined)
2189
+ return undefined;
2190
+ }
2191
+ return cur;
2192
+ }
2193
+ function encodeFormComponent(s) {
2194
+ return encodeURIComponent(s).replace(/%20/g, '+').replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
2195
+ }
2196
+ function decodeComponent(s, plusIsSpace) {
2197
+ const src = plusIsSpace ? s.replace(/\+/g, ' ') : s;
2198
+ // ClickHouse decodes byte-wise and leaves malformed escapes as-is.
2199
+ const bytes = [];
2200
+ for (let i = 0; i < src.length; i++) {
2201
+ const c = src[i];
2202
+ if (c === '%' && /^[0-9a-fA-F]{2}$/.test(src.slice(i + 1, i + 3))) {
2203
+ bytes.push(Number.parseInt(src.slice(i + 1, i + 3), 16));
2204
+ i += 2;
2205
+ continue;
2206
+ }
2207
+ for (const b of Buffer.from(c, 'utf8'))
2208
+ bytes.push(b);
2209
+ }
2210
+ return Buffer.from(bytes).toString('utf8');
2211
+ }
2212
+ function extractUrlParameter(url, name) {
2213
+ const q = url.indexOf('?');
2214
+ if (q < 0)
2215
+ return '';
2216
+ let query = url.slice(q + 1);
2217
+ const hash = query.indexOf('#');
2218
+ if (hash >= 0)
2219
+ query = query.slice(0, hash);
2220
+ for (const pair of query.split('&')) {
2221
+ const eq = pair.indexOf('=');
2222
+ const k = eq < 0 ? pair : pair.slice(0, eq);
2223
+ if (k === name)
2224
+ return eq < 0 ? '' : pair.slice(eq + 1);
2225
+ }
2226
+ return '';
2227
+ }
2228
+ function extractUrlParameterNames(url) {
2229
+ const q = url.indexOf('?');
2230
+ if (q < 0)
2231
+ return [];
2232
+ let query = url.slice(q + 1);
2233
+ const hash = query.indexOf('#');
2234
+ if (hash >= 0)
2235
+ query = query.slice(0, hash);
2236
+ return query.split('&').filter((pair) => pair !== '').map((pair) => { const eq = pair.indexOf('='); return eq < 0 ? pair : pair.slice(0, eq); });
2237
+ }
2238
+ function callLambda(fnArg, env, values) {
2239
+ if (!fnArg || fnArg.k !== 'lambda')
2240
+ return fail('higher-order functions need a lambda as their first argument');
2241
+ const lambda = new Map(env.lambda ?? []);
2242
+ fnArg.params.forEach((p, i) => lambda.set(p, values[i]));
2243
+ return evalExpr(fnArg.body, { ...env, lambda });
2244
+ }
2245
+ function argArrays(e, env, from) {
2246
+ const arrays = [];
2247
+ for (let i = from; i < e.args.length; i++) {
2248
+ const v = evalExpr(e.args[i], env);
2249
+ if (v === null)
2250
+ return null;
2251
+ if (!Array.isArray(v))
2252
+ return fail(`${e.name}: argument ${i + 1} must be an array`);
2253
+ arrays.push(v);
2254
+ }
2255
+ return arrays;
2256
+ }
2257
+ function evalFunction(e, env) {
2258
+ const name = e.name.toLowerCase();
2259
+ if (isAggregateName(name))
2260
+ return evalAggregate(e, env);
2261
+ const A = (i) => evalExpr(e.args[i] ?? fail(`${e.name}: missing argument ${i + 1}`), env);
2262
+ const argc = (n, m = n) => { if (e.args.length < n || e.args.length > m)
2263
+ fail(`Number of arguments for function ${e.name} doesn't match: passed ${e.args.length}`); };
2264
+ switch (name) {
2265
+ // conditionals
2266
+ case 'if': {
2267
+ argc(3);
2268
+ const c = truth(A(0));
2269
+ return c === true ? A(1) : A(2);
2270
+ }
2271
+ case 'multiif': {
2272
+ if (e.args.length < 3 || e.args.length % 2 === 0)
2273
+ fail(`Invalid number of arguments for function multiIf`);
2274
+ for (let i = 0; i + 1 < e.args.length; i += 2)
2275
+ if (truth(A(i)) === true)
2276
+ return A(i + 1);
2277
+ return A(e.args.length - 1);
2278
+ }
2279
+ case 'coalesce': {
2280
+ for (let i = 0; i < e.args.length; i++) {
2281
+ const v = A(i);
2282
+ if (v !== null && v !== undefined)
2283
+ return v;
2284
+ }
2285
+ return null;
2286
+ }
2287
+ case 'ifnull': {
2288
+ argc(2);
2289
+ const v = A(0);
2290
+ return v === null ? A(1) : v;
2291
+ }
2292
+ case 'nullif': {
2293
+ argc(2);
2294
+ const a = A(0);
2295
+ const b = A(1);
2296
+ return a !== null && b !== null && valuesEqual(a, b) ? null : a;
2297
+ }
2298
+ case 'isnull': {
2299
+ argc(1);
2300
+ return A(0) === null ? 1 : 0;
2301
+ }
2302
+ case 'isnotnull': {
2303
+ argc(1);
2304
+ return A(0) === null ? 0 : 1;
2305
+ }
2306
+ case 'assumenotnull': {
2307
+ argc(1);
2308
+ const v = A(0);
2309
+ return v === null ? defaultOf(typeOf(e.args[0], env.scope).replace(/^Nullable\((.*)\)$/, '$1')) : v;
2310
+ }
2311
+ case 'tolowcardinality':
2312
+ case 'identity':
2313
+ case 'materialize': {
2314
+ argc(1);
2315
+ return A(0);
2316
+ }
2317
+ case 'equals': {
2318
+ argc(2);
2319
+ const a = A(0);
2320
+ const b = A(1);
2321
+ return a === null || b === null ? null : valuesEqual(a, b) ? 1 : 0;
2322
+ }
2323
+ case 'notequals': {
2324
+ argc(2);
2325
+ const a = A(0);
2326
+ const b = A(1);
2327
+ return a === null || b === null ? null : valuesEqual(a, b) ? 0 : 1;
2328
+ }
2329
+ case 'greatest':
2330
+ case 'least': {
2331
+ let best = null;
2332
+ for (let i = 0; i < e.args.length; i++) {
2333
+ const v = A(i);
2334
+ if (v === null)
2335
+ return null;
2336
+ if (best === null || (name === 'greatest' ? compareValues(v, best) > 0 : compareValues(v, best) < 0))
2337
+ best = v;
2338
+ }
2339
+ return best;
2340
+ }
2341
+ // strings
2342
+ case 'concat': {
2343
+ const parts = e.args.map((_, i) => A(i));
2344
+ if (parts.some((p) => p === null))
2345
+ return null;
2346
+ return parts.map(valueToText).join('');
2347
+ }
2348
+ case 'tostring': {
2349
+ argc(1, 2);
2350
+ const v = A(0);
2351
+ if (v === null)
2352
+ return null;
2353
+ if (v instanceof DT && e.args[1])
2354
+ return valueToText(v.withTz(tzArg(e, 1, env)));
2355
+ return valueToText(v);
2356
+ }
2357
+ // a UUID is its canonical text (8-4-4-4-12 hexadecimal digits, lowercased); text that is not one fails, or is NULL for
2358
+ // the OrNull form (https://clickhouse.com/docs/sql-reference/functions/type-conversion-functions#touuid)
2359
+ case 'touuid':
2360
+ case 'touuidornull': {
2361
+ argc(1);
2362
+ const v = A(0);
2363
+ if (v === null)
2364
+ return null;
2365
+ const t = valueToText(v).trim();
2366
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(t))
2367
+ return t.toLowerCase();
2368
+ return e.name.toLowerCase() === 'touuidornull' ? null : fail(`Cannot parse uuid ${t}: Cannot parse UUID from String`);
2369
+ }
2370
+ case 'length':
2371
+ case 'char_length':
2372
+ case 'lengthutf8': {
2373
+ argc(1);
2374
+ const v = A(0);
2375
+ if (v === null)
2376
+ return null;
2377
+ if (Array.isArray(v))
2378
+ return v.length;
2379
+ const s = valueToText(v);
2380
+ return name === 'length' ? Buffer.byteLength(s, 'utf8') : [...s].length;
2381
+ }
2382
+ case 'empty':
2383
+ case 'notempty': {
2384
+ argc(1);
2385
+ const v = A(0);
2386
+ if (v === null)
2387
+ return null;
2388
+ const isEmpty = Array.isArray(v) ? v.length === 0 : valueToText(v) === '';
2389
+ return (name === 'empty' ? isEmpty : !isEmpty) ? 1 : 0;
2390
+ }
2391
+ // lower/upper change ASCII letters only (ClickHouse works on bytes); the UTF8 variants are Unicode.
2392
+ case 'lower':
2393
+ case 'lcase': {
2394
+ argc(1);
2395
+ const v = A(0);
2396
+ return v === null ? null : valueToText(v).replace(/[A-Z]/g, (c) => c.toLowerCase());
2397
+ }
2398
+ case 'upper':
2399
+ case 'ucase': {
2400
+ argc(1);
2401
+ const v = A(0);
2402
+ return v === null ? null : valueToText(v).replace(/[a-z]/g, (c) => c.toUpperCase());
2403
+ }
2404
+ case 'lowerutf8': {
2405
+ argc(1);
2406
+ const v = A(0);
2407
+ return v === null ? null : valueToText(v).toLowerCase();
2408
+ }
2409
+ case 'upperutf8': {
2410
+ argc(1);
2411
+ const v = A(0);
2412
+ return v === null ? null : valueToText(v).toUpperCase();
2413
+ }
2414
+ case 'trim':
2415
+ case 'trimboth': {
2416
+ argc(1);
2417
+ const v = A(0);
2418
+ return v === null ? null : valueToText(v).replace(/^ +| +$/g, '');
2419
+ }
2420
+ case 'trimleft':
2421
+ case 'ltrim': {
2422
+ argc(1);
2423
+ const v = A(0);
2424
+ return v === null ? null : valueToText(v).replace(/^ +/, '');
2425
+ }
2426
+ case 'trimright':
2427
+ case 'rtrim': {
2428
+ argc(1);
2429
+ const v = A(0);
2430
+ return v === null ? null : valueToText(v).replace(/ +$/, '');
2431
+ }
2432
+ case 'reverse': {
2433
+ argc(1);
2434
+ const v = A(0);
2435
+ if (v === null)
2436
+ return null;
2437
+ return Array.isArray(v) ? [...v].reverse() : [...valueToText(v)].reverse().join('');
2438
+ }
2439
+ case 'startswith':
2440
+ case 'endswith': {
2441
+ argc(2);
2442
+ const s = A(0);
2443
+ const p = A(1);
2444
+ if (s === null || p === null)
2445
+ return null;
2446
+ return (name === 'startswith' ? valueToText(s).startsWith(valueToText(p)) : valueToText(s).endsWith(valueToText(p))) ? 1 : 0;
2447
+ }
2448
+ case 'position':
2449
+ case 'locate': {
2450
+ argc(2);
2451
+ const s = A(0);
2452
+ const p = A(1);
2453
+ if (s === null || p === null)
2454
+ return null;
2455
+ const i = Buffer.from(valueToText(s)).indexOf(Buffer.from(valueToText(p)));
2456
+ return i + 1;
2457
+ }
2458
+ case 'replace':
2459
+ case 'replaceall': {
2460
+ argc(3);
2461
+ const s = A(0);
2462
+ const a = A(1);
2463
+ const b = A(2);
2464
+ if (s === null || a === null || b === null)
2465
+ return null;
2466
+ const from = valueToText(a);
2467
+ return from === '' ? valueToText(s) : valueToText(s).split(from).join(valueToText(b));
2468
+ }
2469
+ case 'replaceone': {
2470
+ argc(3);
2471
+ const s = A(0);
2472
+ const a = A(1);
2473
+ const b = A(2);
2474
+ if (s === null || a === null || b === null)
2475
+ return null;
2476
+ return valueToText(s).replace(valueToText(a), () => valueToText(b));
2477
+ }
2478
+ case 'substring':
2479
+ case 'substr':
2480
+ case 'mid': {
2481
+ argc(2, 3);
2482
+ const s = A(0);
2483
+ const off = asNumber(A(1));
2484
+ if (s === null || off === null)
2485
+ return null;
2486
+ const buf = Buffer.from(valueToText(s), 'utf8');
2487
+ const start = off > 0 ? off - 1 : Math.max(0, buf.length + off);
2488
+ const len = e.args.length === 3 ? asNumber(A(2)) : buf.length - start;
2489
+ return buf.subarray(start, Math.max(start, start + len)).toString('utf8');
2490
+ }
2491
+ case 'splitbystring':
2492
+ case 'splitbychar': {
2493
+ argc(2, 3);
2494
+ const sep = A(0);
2495
+ const s = A(1);
2496
+ if (sep === null || s === null)
2497
+ return null;
2498
+ const sepText = valueToText(sep);
2499
+ const text = valueToText(s);
2500
+ if (sepText === '')
2501
+ return [...text];
2502
+ return text.split(sepText);
2503
+ }
2504
+ case 'arraystringconcat': {
2505
+ argc(1, 2);
2506
+ const arr = A(0);
2507
+ const sep = e.args.length > 1 ? valueToText(A(1)) : '';
2508
+ return Array.isArray(arr) ? arr.map(valueToText).join(sep) : null;
2509
+ }
2510
+ case 'leftpad':
2511
+ case 'rightpad': {
2512
+ argc(2, 3);
2513
+ const s = valueToText(A(0));
2514
+ const n = asNumber(A(1));
2515
+ const fill = e.args.length > 2 ? valueToText(A(2)) : ' ';
2516
+ if (s.length >= n)
2517
+ return s.slice(0, n);
2518
+ const pad = fill.repeat(Math.ceil((n - s.length) / Math.max(1, fill.length))).slice(0, n - s.length);
2519
+ return name === 'leftpad' ? pad + s : s + pad;
2520
+ }
2521
+ case 'match': {
2522
+ argc(2);
2523
+ const s = A(0);
2524
+ const p = A(1);
2525
+ if (s === null || p === null)
2526
+ return null;
2527
+ return new RegExp(valueToText(p)).test(valueToText(s)) ? 1 : 0;
2528
+ }
2529
+ case 'replaceregexpall':
2530
+ case 'replaceregexpone': {
2531
+ argc(3);
2532
+ const s = A(0);
2533
+ const p = A(1);
2534
+ const r = A(2);
2535
+ if (s === null || p === null || r === null)
2536
+ return null;
2537
+ const re = new RegExp(valueToText(p), name === 'replaceregexpall' ? 'g' : '');
2538
+ return valueToText(s).replace(re, valueToText(r).replace(/\\(\d)/g, '$$$1'));
2539
+ }
2540
+ case 'base64encode': {
2541
+ argc(1);
2542
+ const v = A(0);
2543
+ return v === null ? null : Buffer.from(valueToText(v), 'utf8').toString('base64');
2544
+ }
2545
+ case 'base64decode': {
2546
+ argc(1);
2547
+ const v = A(0);
2548
+ return v === null ? null : Buffer.from(valueToText(v), 'base64').toString('utf8');
2549
+ }
2550
+ case 'hex': {
2551
+ argc(1);
2552
+ const v = A(0);
2553
+ if (v === null)
2554
+ return null;
2555
+ return typeof v === 'number' ? v.toString(16).toUpperCase() : Buffer.from(valueToText(v), 'utf8').toString('hex').toUpperCase();
2556
+ }
2557
+ // URLs
2558
+ case 'encodeurlformcomponent': {
2559
+ argc(1);
2560
+ const v = A(0);
2561
+ return v === null ? null : encodeFormComponent(valueToText(v));
2562
+ }
2563
+ case 'encodeurlcomponent': {
2564
+ argc(1);
2565
+ const v = A(0);
2566
+ return v === null ? null : encodeURIComponent(valueToText(v));
2567
+ }
2568
+ case 'decodeurlformcomponent': {
2569
+ argc(1);
2570
+ const v = A(0);
2571
+ return v === null ? null : decodeComponent(valueToText(v), true);
2572
+ }
2573
+ case 'decodeurlcomponent': {
2574
+ argc(1);
2575
+ const v = A(0);
2576
+ return v === null ? null : decodeComponent(valueToText(v), false);
2577
+ }
2578
+ case 'extracturlparameter': {
2579
+ argc(2);
2580
+ const u = A(0);
2581
+ const p = A(1);
2582
+ if (u === null || p === null)
2583
+ return null;
2584
+ return extractUrlParameter(valueToText(u), valueToText(p));
2585
+ }
2586
+ // "Returns an array of name strings corresponding to the names of URL parameters. The values are not decoded."
2587
+ // (https://clickhouse.com/docs/sql-reference/functions/url-functions#extractURLParameterNames)
2588
+ case 'extracturlparameternames': {
2589
+ argc(1);
2590
+ const u = A(0);
2591
+ if (u === null)
2592
+ return null;
2593
+ return extractUrlParameterNames(valueToText(u));
2594
+ }
2595
+ // Maps (https://clickhouse.com/docs/sql-reference/functions/tuple-map-functions): mapFilter "Returns a map containing
2596
+ // only the elements in the map for which func returns something other than 0"; mapKeys "Returns array containing
2597
+ // all keys from the map"; mapContainsKey, alias mapContains, "Returns 1 if map contains key, 0 if not".
2598
+ case 'mapfilter': {
2599
+ argc(2);
2600
+ const m = A(1);
2601
+ if (m === null)
2602
+ return null;
2603
+ if (!(m instanceof ChMap))
2604
+ return fail(`Illegal type of argument #2 of function ${e.name}, expected Map`);
2605
+ return new ChMap(m.entries.filter(([k, v]) => { const r = callLambda(e.args[0], env, [k, v]); return r !== null && r !== 0 && r !== false; }));
2606
+ }
2607
+ case 'mapkeys':
2608
+ case 'mapvalues': {
2609
+ argc(1);
2610
+ const m = A(0);
2611
+ if (m === null)
2612
+ return null;
2613
+ if (!(m instanceof ChMap))
2614
+ return fail(`Illegal type of argument #1 of function ${e.name}, expected Map`);
2615
+ return m.entries.map(([k, v]) => (name === 'mapkeys' ? k : v));
2616
+ }
2617
+ case 'mapcontains':
2618
+ case 'mapcontainskey': {
2619
+ argc(2);
2620
+ const m = A(0);
2621
+ const key = A(1);
2622
+ if (m === null)
2623
+ return null;
2624
+ if (!(m instanceof ChMap))
2625
+ return fail(`Illegal type of argument #1 of function ${e.name}, expected Map`);
2626
+ return m.entries.some(([k]) => (k === null ? key === null : key !== null && valuesEqual(k, key))) ? 1 : 0;
2627
+ }
2628
+ case 'cutquerystring': {
2629
+ argc(1);
2630
+ const u = A(0);
2631
+ if (u === null)
2632
+ return null;
2633
+ const s = valueToText(u);
2634
+ const i = s.search(/[?#]/);
2635
+ return i < 0 ? s : s.slice(0, i);
2636
+ }
2637
+ case 'domain':
2638
+ case 'domainwithoutwww': {
2639
+ argc(1);
2640
+ const u = A(0);
2641
+ if (u === null)
2642
+ return null;
2643
+ const m = /^(?:[a-zA-Z][a-zA-Z0-9+.-]*:)?\/\/([^/?#:@]+(?:@[^/?#:]+)?)/.exec(valueToText(u));
2644
+ let host = m ? m[1].replace(/^.*@/, '') : '';
2645
+ if (name === 'domainwithoutwww')
2646
+ host = host.replace(/^www\./, '');
2647
+ return host;
2648
+ }
2649
+ case 'path': {
2650
+ argc(1);
2651
+ const u = A(0);
2652
+ if (u === null)
2653
+ return null;
2654
+ const m = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^/?#]*([^?#]*)/.exec(valueToText(u));
2655
+ return m ? m[1] : '';
2656
+ }
2657
+ // JSON
2658
+ case 'jsonextractstring':
2659
+ case 'jsonextractraw':
2660
+ case 'jsonextractint':
2661
+ case 'jsonextractfloat':
2662
+ case 'jsonextractbool':
2663
+ case 'jsonhas': {
2664
+ if (e.args.length < 1)
2665
+ fail(`Number of arguments for function ${e.name} doesn't match`);
2666
+ const j = A(0);
2667
+ if (j === null)
2668
+ return null;
2669
+ const keys = e.args.slice(1).map((_, i) => A(i + 1));
2670
+ const v = jsonPathValue(typeof j === 'string' ? j : JSON.stringify(j), keys);
2671
+ if (name === 'jsonhas')
2672
+ return v === undefined ? 0 : 1;
2673
+ if (name === 'jsonextractstring')
2674
+ return typeof v === 'string' ? v : '';
2675
+ if (name === 'jsonextractraw')
2676
+ return v === undefined ? '' : JSON.stringify(v);
2677
+ if (name === 'jsonextractbool')
2678
+ return v === true ? 1 : 0;
2679
+ if (typeof v === 'number')
2680
+ return name === 'jsonextractint' ? Math.trunc(v) : v;
2681
+ if (typeof v === 'boolean')
2682
+ return v ? 1 : 0;
2683
+ return 0;
2684
+ }
2685
+ // numeric conversions
2686
+ case 'tofloat64':
2687
+ case 'tofloat32': {
2688
+ argc(1);
2689
+ const v = A(0);
2690
+ return v === null ? null : asNumber(typeof v === 'string' ? v.trim() : v);
2691
+ }
2692
+ case 'totypename': {
2693
+ argc(1);
2694
+ return typeOf(e.args[0], env.scope);
2695
+ }
2696
+ default: break;
2697
+ }
2698
+ // toIntN / toUIntN (8…256 bits) and their OrZero / OrNull forms, toFloatN OrZero / OrNull.
2699
+ const conv = /^to(u?int(?:8|16|32|64|128|256)|float(?:32|64))(orzero|ornull)?$/.exec(name);
2700
+ if (conv) {
2701
+ argc(1);
2702
+ const t = conv[1].replace(/^u?int|^float/, (m) => (m === 'uint' ? 'UInt' : m === 'int' ? 'Int' : 'Float'));
2703
+ const v = A(0);
2704
+ if (conv[2])
2705
+ return toNumberOrElse(v, t, conv[2] === 'ornull');
2706
+ if (v === null)
2707
+ return null;
2708
+ return toIntegerOf(typeof v === 'string' ? v : v, t);
2709
+ }
2710
+ switch (name) {
2711
+ case 'round': {
2712
+ argc(1, 2);
2713
+ const v = A(0);
2714
+ if (v === null)
2715
+ return null;
2716
+ const d = e.args.length > 1 ? asNumber(A(1)) : 0;
2717
+ const f = 10 ** d;
2718
+ return roundHalfEven(asNumber(v) * f) / f;
2719
+ }
2720
+ case 'floor': {
2721
+ argc(1, 2);
2722
+ const v = A(0);
2723
+ if (v === null)
2724
+ return null;
2725
+ const d = e.args.length > 1 ? asNumber(A(1)) : 0;
2726
+ const f = 10 ** d;
2727
+ return Math.floor(asNumber(v) * f) / f;
2728
+ }
2729
+ case 'ceil':
2730
+ case 'ceiling': {
2731
+ argc(1, 2);
2732
+ const v = A(0);
2733
+ if (v === null)
2734
+ return null;
2735
+ const d = e.args.length > 1 ? asNumber(A(1)) : 0;
2736
+ const f = 10 ** d;
2737
+ return Math.ceil(asNumber(v) * f) / f;
2738
+ }
2739
+ case 'abs': {
2740
+ argc(1);
2741
+ const v = A(0);
2742
+ return v === null ? null : Math.abs(asNumber(v));
2743
+ }
2744
+ case 'intdiv': {
2745
+ argc(2);
2746
+ const a = A(0);
2747
+ const b = A(1);
2748
+ if (a === null || b === null)
2749
+ return null;
2750
+ if (asNumber(b) === 0)
2751
+ fail('Division by zero');
2752
+ return Math.trunc(asNumber(a) / asNumber(b));
2753
+ }
2754
+ case 'divide':
2755
+ case 'plus':
2756
+ case 'minus':
2757
+ case 'multiply':
2758
+ case 'modulo': {
2759
+ argc(2);
2760
+ const ops = { divide: '/', plus: '+', minus: '-', multiply: '*', modulo: '%' };
2761
+ return arithmetic(ops[name], A(0), A(1));
2762
+ }
2763
+ case 'sqrt': {
2764
+ argc(1);
2765
+ const v = A(0);
2766
+ return v === null ? null : Math.sqrt(asNumber(v));
2767
+ }
2768
+ case 'pow':
2769
+ case 'power': {
2770
+ argc(2);
2771
+ const a = A(0);
2772
+ const b = A(1);
2773
+ return a === null || b === null ? null : asNumber(a) ** asNumber(b);
2774
+ }
2775
+ // dates and times
2776
+ case 'now': {
2777
+ argc(0, 1);
2778
+ return new DT(env.scope.exec.nowMs, 0, e.args.length ? tzArg(e, 0, env) : undefined);
2779
+ }
2780
+ case 'now64': {
2781
+ argc(0, 2);
2782
+ return new DT(env.scope.exec.nowMs, e.args.length ? asNumber(A(0)) : 3, e.args.length > 1 ? tzArg(e, 1, env) : undefined);
2783
+ }
2784
+ case 'today': {
2785
+ argc(0);
2786
+ return toDateValue(new DT(env.scope.exec.nowMs, 0));
2787
+ }
2788
+ case 'yesterday': {
2789
+ argc(0);
2790
+ return new DDate(toDateValue(new DT(env.scope.exec.nowMs, 0)).days - 1);
2791
+ }
2792
+ case 'todatetime':
2793
+ case 'parsedatetimebesteffort':
2794
+ case 'parsedatetime64besteffort':
2795
+ case 'parsedatetimebesteffortornull': {
2796
+ argc(1, 2);
2797
+ const v = A(0);
2798
+ if (v === null)
2799
+ return null;
2800
+ const tz = e.args.length > 1 ? tzArg(e, 1, env) : undefined;
2801
+ if (name === 'parsedatetimebesteffortornull') {
2802
+ try {
2803
+ return castDateTime(v, 0, tz);
2804
+ }
2805
+ catch {
2806
+ return null;
2807
+ }
2808
+ }
2809
+ return castDateTime(v, name === 'parsedatetime64besteffort' ? 3 : 0, tz);
2810
+ }
2811
+ case 'todatetime64':
2812
+ case 'todatetime64orzero': {
2813
+ argc(2, 3);
2814
+ const v = A(0);
2815
+ if (v === null)
2816
+ return null;
2817
+ const scale = asNumber(A(1));
2818
+ const tz = e.args.length > 2 ? tzArg(e, 2, env) : undefined;
2819
+ try {
2820
+ return castDateTime(v, scale, tz);
2821
+ }
2822
+ catch (err) {
2823
+ if (name === 'todatetime64orzero')
2824
+ return new DT(0, scale, tz);
2825
+ throw err;
2826
+ }
2827
+ }
2828
+ case 'todate': {
2829
+ argc(1, 2);
2830
+ const v = A(0);
2831
+ if (v === null)
2832
+ return null;
2833
+ return toDateValue(v, e.args.length > 1 ? tzArg(e, 1, env) : undefined);
2834
+ }
2835
+ case 'tostartofsecond': {
2836
+ argc(1, 2);
2837
+ return startOf(A(0), 'second', tzArg(e, 1, env));
2838
+ }
2839
+ case 'tostartofminute': {
2840
+ argc(1, 2);
2841
+ return startOf(A(0), 'minute', tzArg(e, 1, env));
2842
+ }
2843
+ case 'tostartoffiveminutes':
2844
+ case 'tostartoffiveminute': {
2845
+ argc(1, 2);
2846
+ return startOf(A(0), 'fiveMinutes', tzArg(e, 1, env));
2847
+ }
2848
+ case 'tostartoffifteenminutes': {
2849
+ argc(1, 2);
2850
+ return startOf(A(0), 'fifteenMinutes', tzArg(e, 1, env));
2851
+ }
2852
+ case 'tostartofhour': {
2853
+ argc(1, 2);
2854
+ return startOf(A(0), 'hour', tzArg(e, 1, env));
2855
+ }
2856
+ case 'tostartofday': {
2857
+ argc(1, 2);
2858
+ return startOf(A(0), 'day', tzArg(e, 1, env));
2859
+ }
2860
+ case 'tostartofweek':
2861
+ case 'tomonday':
2862
+ case 'tostartofmonth':
2863
+ case 'tostartofquarter':
2864
+ case 'tostartofyear': {
2865
+ argc(1, name === 'tostartofweek' ? 3 : 2);
2866
+ const v = A(0);
2867
+ if (v === null)
2868
+ return null;
2869
+ const tz = tzArg(e, name === 'tostartofweek' ? 2 : 1, env);
2870
+ const d = toDateValue(v, tz);
2871
+ const c = civilFromDays(d.days);
2872
+ if (name === 'tostartofmonth')
2873
+ return new DDate(daysFromCivil(c.y, c.mo, 1));
2874
+ if (name === 'tostartofquarter')
2875
+ return new DDate(daysFromCivil(c.y, c.mo - ((c.mo - 1) % 3), 1));
2876
+ if (name === 'tostartofyear')
2877
+ return new DDate(daysFromCivil(c.y, 1, 1));
2878
+ const mode = name === 'tostartofweek' && e.args.length > 1 ? asNumber(A(1)) : name === 'tomonday' ? 1 : 0;
2879
+ const dow = ((d.days % 7) + 7 + 4) % 7; // 0 = Sunday
2880
+ const back = mode % 2 === 1 ? (dow + 6) % 7 : dow;
2881
+ return new DDate(d.days - back);
2882
+ }
2883
+ case 'tostartofinterval':
2884
+ case 'time_bucket':
2885
+ case 'date_bin': {
2886
+ argc(2, 3);
2887
+ const v = A(0);
2888
+ const iv = A(1);
2889
+ if (v === null || iv === null)
2890
+ return null;
2891
+ if (!(iv instanceof Interval))
2892
+ return fail(`Illegal type of argument #2 of function ${e.name}: expected an INTERVAL`);
2893
+ return startOfInterval(v, iv, e.args.length > 2 ? tzArg(e, 2, env) : undefined);
2894
+ }
2895
+ case 'toyyyymmdd':
2896
+ case 'toyyyymmddhhmmss':
2897
+ case 'toquarter':
2898
+ case 'todayofyear': {
2899
+ argc(1, 2);
2900
+ const v = A(0);
2901
+ if (v === null)
2902
+ return null;
2903
+ const tz = tzArg(e, 1, env);
2904
+ const dt = v instanceof DDate ? castDateTime(v, 0, tz) : toDateTimeArg(v);
2905
+ const w = wallOf(dt.ms, tz ?? dt.tz);
2906
+ if (name === 'toyyyymmdd')
2907
+ return w.y * 10000 + w.mo * 100 + w.d;
2908
+ if (name === 'toquarter')
2909
+ return Math.floor((w.mo - 1) / 3) + 1;
2910
+ if (name === 'todayofyear')
2911
+ return daysFromCivil(w.y, w.mo, w.d) - daysFromCivil(w.y, 1, 1) + 1;
2912
+ return normInt(BigInt(w.y * 10000 + w.mo * 100 + w.d) * 1000000n + BigInt(w.h * 10000 + w.mi * 100 + w.s));
2913
+ }
2914
+ case 'toyyyymm':
2915
+ case 'toyear':
2916
+ case 'tomonth':
2917
+ case 'todayofmonth':
2918
+ case 'tohour':
2919
+ case 'tominute':
2920
+ case 'tosecond':
2921
+ case 'todayofweek': {
2922
+ argc(1, 2);
2923
+ const v = A(0);
2924
+ if (v === null)
2925
+ return null;
2926
+ const tz = tzArg(e, 1, env);
2927
+ const dt = v instanceof DDate ? castDateTime(v, 0, tz) : toDateTimeArg(v);
2928
+ const w = wallOf(dt.ms, tz ?? dt.tz);
2929
+ switch (name) {
2930
+ case 'toyyyymm': return w.y * 100 + w.mo;
2931
+ case 'toyear': return w.y;
2932
+ case 'tomonth': return w.mo;
2933
+ case 'todayofmonth': return w.d;
2934
+ case 'tohour': return w.h;
2935
+ case 'tominute': return w.mi;
2936
+ case 'tosecond': return w.s;
2937
+ default: {
2938
+ const dow = ((daysFromCivil(w.y, w.mo, w.d) % 7) + 7 + 4) % 7;
2939
+ return dow === 0 ? 7 : dow;
2940
+ }
2941
+ }
2942
+ }
2943
+ case 'tounixtimestamp': {
2944
+ argc(1, 2);
2945
+ const v = A(0);
2946
+ if (v === null)
2947
+ return null;
2948
+ if (typeof v === 'string')
2949
+ return Math.floor(castDateTime(v, 0, tzArg(e, 1, env)).ms / 1000);
2950
+ return Math.floor(toDateTimeArg(v).ms / 1000);
2951
+ }
2952
+ case 'tounixtimestamp64milli': {
2953
+ argc(1);
2954
+ const v = A(0);
2955
+ if (v === null)
2956
+ return null;
2957
+ return toDateTimeArg(v).ms;
2958
+ }
2959
+ case 'tounixtimestamp64micro': {
2960
+ argc(1);
2961
+ const v = A(0);
2962
+ if (v === null)
2963
+ return null;
2964
+ const d = toDateTimeArg(v);
2965
+ return normInt(BigInt(d.ms) * 1000n + BigInt(Math.floor(d.ns / 1000)));
2966
+ }
2967
+ case 'tounixtimestamp64nano': {
2968
+ argc(1);
2969
+ const v = A(0);
2970
+ if (v === null)
2971
+ return null;
2972
+ const d = toDateTimeArg(v);
2973
+ return normInt(BigInt(d.ms) * 1000000n + BigInt(d.ns));
2974
+ }
2975
+ case 'fromunixtimestamp64micro':
2976
+ case 'fromunixtimestamp64nano': {
2977
+ argc(1, 2);
2978
+ const v = A(0);
2979
+ if (v === null)
2980
+ return null;
2981
+ const n = toBigInt(v);
2982
+ const per = name.endsWith('micro') ? 1000n : 1000000n;
2983
+ const msPart = n >= 0n ? n / per : -((-n + per - 1n) / per);
2984
+ return makeDT(Number(msPart), Number((n - msPart * per) * (1000000n / per)), name.endsWith('micro') ? 6 : 9, tzArg(e, 1, env));
2985
+ }
2986
+ case 'fromunixtimestamp64milli': {
2987
+ argc(1, 2);
2988
+ const v = A(0);
2989
+ if (v === null)
2990
+ return null;
2991
+ return new DT(asNumber(v), 3, tzArg(e, 1, env));
2992
+ }
2993
+ case 'fromunixtimestamp': {
2994
+ argc(1, 2);
2995
+ const v = A(0);
2996
+ if (v === null)
2997
+ return null;
2998
+ return new DT(asNumber(v) * 1000, 0, tzArg(e, 1, env));
2999
+ }
3000
+ case 'datediff':
3001
+ case 'date_diff':
3002
+ case 'timestampdiff': {
3003
+ argc(3, 4);
3004
+ const u = A(0);
3005
+ const a = A(1);
3006
+ const b = A(2);
3007
+ if (u === null || a === null || b === null)
3008
+ return null;
3009
+ const tz = e.args.length > 3 ? tzArg(e, 3, env) : undefined;
3010
+ const unit = valueToText(u).toLowerCase();
3011
+ const da = a instanceof DDate || a instanceof DT ? a : castDateTime(a, 0, tz);
3012
+ const db = b instanceof DDate || b instanceof DT ? b : castDateTime(b, 0, tz);
3013
+ const zone = tz ?? (da instanceof DT ? da.tz : undefined) ?? (db instanceof DT ? db.tz : undefined);
3014
+ return relativeNumber(db, unit, zone) - relativeNumber(da, unit, zone);
3015
+ }
3016
+ case 'formatdatetime': {
3017
+ argc(2, 3);
3018
+ const v = A(0);
3019
+ const f = A(1);
3020
+ if (v === null || f === null)
3021
+ return null;
3022
+ const dt = v instanceof DDate || v instanceof DT ? v : castDateTime(v, 0);
3023
+ return formatDateTimePattern(dt, valueToText(f), e.args.length > 2 ? tzArg(e, 2, env) : undefined);
3024
+ }
3025
+ case 'totimezone': {
3026
+ argc(2);
3027
+ const v = A(0);
3028
+ if (v === null)
3029
+ return null;
3030
+ return toDateTimeArg(v).withTz(tzArg(e, 1, env));
3031
+ }
3032
+ case 'addseconds':
3033
+ case 'addminutes':
3034
+ case 'addhours':
3035
+ case 'adddays':
3036
+ case 'addweeks':
3037
+ case 'addmonths':
3038
+ case 'addyears':
3039
+ case 'subtractseconds':
3040
+ case 'subtractminutes':
3041
+ case 'subtracthours':
3042
+ case 'subtractdays':
3043
+ case 'subtractweeks':
3044
+ case 'subtractmonths':
3045
+ case 'subtractyears': {
3046
+ argc(2);
3047
+ const v = A(0);
3048
+ const n = asNumber(A(1));
3049
+ if (v === null || n === null)
3050
+ return null;
3051
+ const unit = name.replace(/^(add|subtract)/, '').replace(/s$/, '');
3052
+ return addInterval(v, new Interval(unit, n), name.startsWith('add') ? 1 : -1);
3053
+ }
3054
+ // arrays
3055
+ case 'array': return e.args.map((_, i) => A(i));
3056
+ case 'arrayjoin': {
3057
+ const bound = env.aj?.get(e);
3058
+ if (bound !== undefined || env.aj?.has(e))
3059
+ return bound;
3060
+ return fail('arrayJoin is only supported in the SELECT list of this twin');
3061
+ }
3062
+ case 'range': {
3063
+ argc(1, 3);
3064
+ const args = e.args.map((_, i) => asNumber(A(i)));
3065
+ if (args.some((x) => x === null))
3066
+ return null;
3067
+ const [start, end, step] = args.length === 1 ? [0, args[0], 1] : [args[0], args[1], args[2] ?? 1];
3068
+ if (step <= 0)
3069
+ fail('range: step must be positive');
3070
+ const out = [];
3071
+ for (let x = start; x < end; x += step) {
3072
+ out.push(x);
3073
+ if (out.length > 1_000_000)
3074
+ fail('range: too many elements');
3075
+ }
3076
+ return out;
3077
+ }
3078
+ case 'arrayelement': {
3079
+ argc(2);
3080
+ const arr = A(0);
3081
+ const i = asNumber(A(1));
3082
+ if (arr === null || i === null)
3083
+ return null;
3084
+ if (!Array.isArray(arr))
3085
+ fail('arrayElement: first argument must be an array');
3086
+ return arrayAt(arr, i);
3087
+ }
3088
+ case 'has': {
3089
+ argc(2);
3090
+ const arr = A(0);
3091
+ const x = A(1);
3092
+ if (arr === null)
3093
+ return null;
3094
+ if (!Array.isArray(arr))
3095
+ fail('has: first argument must be an array');
3096
+ return arr.some((y) => (y === null ? x === null : x !== null && valuesEqual(y, x))) ? 1 : 0;
3097
+ }
3098
+ case 'hasany':
3099
+ case 'hasall': {
3100
+ argc(2);
3101
+ const a = A(0);
3102
+ const b = A(1);
3103
+ if (a === null || b === null)
3104
+ return null;
3105
+ if (!Array.isArray(a) || !Array.isArray(b))
3106
+ fail(`${e.name}: arguments must be arrays`);
3107
+ const keys = new Set(a.map(valueKey));
3108
+ return (name === 'hasany' ? b.some((x) => keys.has(valueKey(x))) : b.every((x) => keys.has(valueKey(x)))) ? 1 : 0;
3109
+ }
3110
+ case 'indexof': {
3111
+ argc(2);
3112
+ const arr = A(0);
3113
+ const x = A(1);
3114
+ if (arr === null)
3115
+ return null;
3116
+ const i = arr.findIndex((y) => x !== null && y !== null && valuesEqual(y, x));
3117
+ return i + 1;
3118
+ }
3119
+ case 'arrayintersect': {
3120
+ const arrays = argArrays(e, env, 0);
3121
+ if (!arrays)
3122
+ return null;
3123
+ if (arrays.length === 0)
3124
+ return [];
3125
+ const rest = arrays.slice(1).map((a) => new Set(a.map(valueKey)));
3126
+ const seen = new Set();
3127
+ const out = [];
3128
+ for (const x of arrays[0]) {
3129
+ const k = valueKey(x);
3130
+ if (seen.has(k))
3131
+ continue;
3132
+ if (rest.every((s) => s.has(k))) {
3133
+ seen.add(k);
3134
+ out.push(x);
3135
+ }
3136
+ }
3137
+ return out;
3138
+ }
3139
+ case 'arraydistinct': {
3140
+ argc(1);
3141
+ const arr = A(0);
3142
+ if (arr === null)
3143
+ return null;
3144
+ const seen = new Set();
3145
+ return arr.filter((x) => { const k = valueKey(x); if (seen.has(k) || x === null)
3146
+ return false; seen.add(k); return true; });
3147
+ }
3148
+ case 'arrayconcat': {
3149
+ const arrays = argArrays(e, env, 0);
3150
+ return arrays ? arrays.flat() : null;
3151
+ }
3152
+ case 'arraysort':
3153
+ case 'arrayreversesort': {
3154
+ if (e.args[0]?.k === 'lambda') {
3155
+ const arrays = argArrays(e, env, 1);
3156
+ if (!arrays)
3157
+ return null;
3158
+ const keyed = arrays[0].map((x, i) => ({ x, k: callLambda(e.args[0], env, arrays.map((a) => a[i])) }));
3159
+ keyed.sort((p, q) => compareValues(p.k, q.k) * (name === 'arrayreversesort' ? -1 : 1));
3160
+ return keyed.map((p) => p.x);
3161
+ }
3162
+ argc(1);
3163
+ const arr = A(0);
3164
+ if (arr === null)
3165
+ return null;
3166
+ return [...arr].sort((p, q) => compareValues(p, q) * (name === 'arrayreversesort' ? -1 : 1));
3167
+ }
3168
+ case 'arrayreverse': {
3169
+ argc(1);
3170
+ const arr = A(0);
3171
+ return arr === null ? null : [...arr].reverse();
3172
+ }
3173
+ case 'arrayslice': {
3174
+ argc(2, 3);
3175
+ const arr = A(0);
3176
+ const off = asNumber(A(1));
3177
+ if (arr === null || off === null)
3178
+ return null;
3179
+ const start = off > 0 ? off - 1 : arr.length + off;
3180
+ const len = e.args.length > 2 ? asNumber(A(2)) : null;
3181
+ return arr.slice(start, len === null ? undefined : start + len);
3182
+ }
3183
+ case 'arraypushback': {
3184
+ argc(2);
3185
+ const arr = A(0);
3186
+ return arr === null ? null : [...arr, A(1)];
3187
+ }
3188
+ case 'arraypushfront': {
3189
+ argc(2);
3190
+ const arr = A(0);
3191
+ return arr === null ? null : [A(1), ...arr];
3192
+ }
3193
+ case 'arraymap': {
3194
+ const arrays = argArrays(e, env, 1);
3195
+ if (!arrays)
3196
+ return null;
3197
+ const n = arrays[0]?.length ?? 0;
3198
+ if (arrays.some((a) => a.length !== n))
3199
+ fail('arrayMap: arrays passed to the function must have equal size');
3200
+ const out = [];
3201
+ for (let i = 0; i < n; i++)
3202
+ out.push(callLambda(e.args[0], env, arrays.map((a) => a[i])));
3203
+ return out;
3204
+ }
3205
+ case 'arrayfilter':
3206
+ case 'arraycount':
3207
+ case 'arrayexists':
3208
+ case 'arrayall':
3209
+ case 'arrayfirst':
3210
+ case 'arraysum': {
3211
+ if (e.args[0]?.k !== 'lambda') {
3212
+ if (name === 'arraysum' || name === 'arraycount') {
3213
+ argc(1);
3214
+ const arr = A(0);
3215
+ if (arr === null)
3216
+ return null;
3217
+ return name === 'arraysum' ? arr.reduce((s, x) => s + (asNumber(x) ?? 0), 0) : arr.filter((x) => truth(x) === true).length;
3218
+ }
3219
+ return fail(`${e.name}: first argument must be a lambda`);
3220
+ }
3221
+ const arrays = argArrays(e, env, 1);
3222
+ if (!arrays)
3223
+ return null;
3224
+ const n = arrays[0]?.length ?? 0;
3225
+ const results = Array.from({ length: n }, (_, i) => callLambda(e.args[0], env, arrays.map((a) => a[i])));
3226
+ if (name === 'arrayfilter')
3227
+ return arrays[0].filter((_, i) => truth(results[i]) === true);
3228
+ if (name === 'arraycount')
3229
+ return results.filter((r) => truth(r) === true).length;
3230
+ if (name === 'arrayexists')
3231
+ return results.some((r) => truth(r) === true) ? 1 : 0;
3232
+ if (name === 'arrayall')
3233
+ return results.every((r) => truth(r) === true) ? 1 : 0;
3234
+ if (name === 'arraysum')
3235
+ return results.reduce((s, x) => s + (asNumber(x) ?? 0), 0);
3236
+ const i = results.findIndex((r) => truth(r) === true);
3237
+ return i < 0 ? arrayAt(arrays[0], 0) : arrays[0][i];
3238
+ }
3239
+ case 'tuple': return e.args.map((_, i) => A(i));
3240
+ default:
3241
+ if (name.startsWith('tointerval')) {
3242
+ argc(1);
3243
+ const unit = name.slice('tointerval'.length);
3244
+ if (!['millisecond', 'second', 'minute', 'hour', 'day', 'week', 'month', 'quarter', 'year'].includes(unit))
3245
+ fail(`Unknown function ${e.name}`);
3246
+ const n = asNumber(A(0));
3247
+ return n === null ? null : new Interval(unit, n);
3248
+ }
3249
+ return fail(`Unknown function ${e.name}`);
3250
+ }
3251
+ }
3252
+ function roundHalfEven(x) {
3253
+ const r = Math.round(x);
3254
+ return Math.abs(x % 1) === 0.5 && r % 2 !== 0 ? r - 1 : r;
3255
+ }
3256
+ // ── aggregates ──────────────────────────────────────────────────────────────────────────────
3257
+ /** sum()/avg() of a String is ClickHouse's ILLEGAL_TYPE_OF_ARGUMENT, never a coerced number. */
3258
+ function rejectStringArgument(e, env) {
3259
+ const arg = e.args[0];
3260
+ if (!arg || arg.k === 'star')
3261
+ return;
3262
+ const t = typeOf(arg, env.scope);
3263
+ const h = typeHead(t);
3264
+ if (h === 'String' || h === 'FixedString' || h === 'UUID') {
3265
+ fail(`Illegal type ${baseType(t).base} of argument for aggregate function ${e.name}`);
3266
+ }
3267
+ }
3268
+ /**
3269
+ * An aggregate's state: what finishing it (plain call or `-Merge`) needs. Its shape per kind is
3270
+ * documented beside `aggKind` in ./values.ts (scalar, sum, avg, set, list, arg).
3271
+ */
3272
+ function buildState(base, values, keys) {
3273
+ const kind = aggKind(base);
3274
+ switch (kind) {
3275
+ case 'sum': {
3276
+ if (base === 'count')
3277
+ return values.length;
3278
+ if (values.length === 0)
3279
+ return null;
3280
+ let exact = true;
3281
+ for (const v of values)
3282
+ if (!isExactInt(v)) {
3283
+ exact = false;
3284
+ break;
3285
+ }
3286
+ if (exact)
3287
+ return normInt(values.reduce((s, v) => s + toBigInt(v), 0n));
3288
+ return values.reduce((s, v) => s + asNumber(v), 0);
3289
+ }
3290
+ case 'avg': return values.length === 0 ? null : [values.reduce((s, v) => s + asNumber(v), 0), values.length];
3291
+ case 'scalar': {
3292
+ if (values.length === 0)
3293
+ return null;
3294
+ if (base === 'any')
3295
+ return values[0];
3296
+ if (base === 'anylast')
3297
+ return values[values.length - 1];
3298
+ return values.reduce((a, b) => ((base === 'min' ? compareValues(b, a) < 0 : compareValues(b, a) > 0) ? b : a));
3299
+ }
3300
+ case 'set': {
3301
+ const seen = new Set();
3302
+ return values.filter((v) => { const k = valueKey(v); if (seen.has(k))
3303
+ return false; seen.add(k); return true; });
3304
+ }
3305
+ case 'list': return values;
3306
+ case 'arg': {
3307
+ let best = -1;
3308
+ for (let i = 0; i < values.length; i++) {
3309
+ if (keys[i] === null || keys[i] === undefined)
3310
+ continue;
3311
+ if (best < 0 || (base === 'argmin' ? compareValues(keys[i], keys[best]) < 0 : compareValues(keys[i], keys[best]) > 0))
3312
+ best = i;
3313
+ }
3314
+ return best < 0 ? null : [values[best], keys[best]];
3315
+ }
3316
+ }
3317
+ }
3318
+ /** Union two states of the same aggregate (what `-Merge` does across parts). */
3319
+ function mergeStates(base, a, b) {
3320
+ if (a === null || a === undefined)
3321
+ return b;
3322
+ if (b === null || b === undefined)
3323
+ return a;
3324
+ switch (aggKind(base)) {
3325
+ case 'sum': return arithmetic('+', a, b);
3326
+ case 'avg': {
3327
+ const [s1, c1] = a;
3328
+ const [s2, c2] = b;
3329
+ return [s1 + s2, c1 + c2];
3330
+ }
3331
+ case 'scalar':
3332
+ if (base === 'any')
3333
+ return a;
3334
+ if (base === 'anylast')
3335
+ return b;
3336
+ return (base === 'min' ? compareValues(b, a) < 0 : compareValues(b, a) > 0) ? b : a;
3337
+ case 'set': return buildState(base, [...a, ...b], undefined);
3338
+ case 'list': return [...a, ...b];
3339
+ case 'arg': {
3340
+ const [, ka] = a;
3341
+ const [, kb] = b;
3342
+ return (base === 'argmin' ? compareValues(kb, ka) < 0 : compareValues(kb, ka) > 0) ? b : a;
3343
+ }
3344
+ }
3345
+ }
3346
+ /** ClickHouse's `quantile` family: linear interpolation (exact at this twin's sizes), or the
3347
+ * `quantileExact` element at ⌊n·p⌋ of the sorted values. */
3348
+ function quantileOf(values, p, exact) {
3349
+ if (values.length === 0)
3350
+ return exact ? null : Number.NaN;
3351
+ const sorted = [...values].sort(compareValues);
3352
+ if (exact)
3353
+ return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * p))];
3354
+ const pos = (sorted.length - 1) * p;
3355
+ const lo = Math.floor(pos);
3356
+ const hi = Math.min(sorted.length - 1, lo + 1);
3357
+ const a = asNumber(sorted[lo]);
3358
+ const b = asNumber(sorted[hi]);
3359
+ return a + (b - a) * (pos - lo);
3360
+ }
3361
+ /** Finish an aggregate from its state; `argType` is the argument's type (defaults for no rows). */
3362
+ function finishState(base, params, state, argType) {
3363
+ const nullable = isNullableType(argType) && !RETURNS_DEFAULT_ON_NULL.has(base);
3364
+ switch (aggKind(base)) {
3365
+ case 'sum': return state ?? (nullable ? null : 0);
3366
+ case 'avg': {
3367
+ if (state === null || state === undefined)
3368
+ return nullable ? null : Number.NaN;
3369
+ const [s, c] = state;
3370
+ return s / c;
3371
+ }
3372
+ case 'scalar': return state === null || state === undefined ? (nullable ? null : defaultOf(argType)) : state;
3373
+ case 'set': return base === 'groupuniqarray' ? (state ?? []) : (state ?? []).length;
3374
+ case 'arg': return state === null || state === undefined ? (nullable ? null : defaultOf(argType)) : state[0];
3375
+ case 'list': {
3376
+ const vs = state ?? [];
3377
+ if (base === 'grouparray')
3378
+ return vs;
3379
+ if (base === 'grouparraysample')
3380
+ return sampleOf(vs, params);
3381
+ const exact = base.includes('exact');
3382
+ const many = base.startsWith('quantiles');
3383
+ const ps = (params.length ? params : [0.5]).map((p) => asNumber(p));
3384
+ if (base.startsWith('median'))
3385
+ ps.splice(0, ps.length, 0.5);
3386
+ if (vs.length === 0 && nullable && !many)
3387
+ return null;
3388
+ const out = ps.map((p) => quantileOf(vs, p, exact));
3389
+ if (exact)
3390
+ for (let i = 0; i < out.length; i++)
3391
+ if (out[i] === null)
3392
+ out[i] = defaultOf(argType);
3393
+ return many ? out : out[0];
3394
+ }
3395
+ }
3396
+ }
3397
+ /** Aggregates that answer a default (not NULL) when every input is NULL (ClickHouse's -Null rules). */
3398
+ const RETURNS_DEFAULT_ON_NULL = new Set(['count', 'countdistinct', 'uniq', 'uniqexact', 'uniqcombined', 'uniqcombined64', 'uniqhll12', 'grouparray', 'grouparraysample', 'groupuniqarray']);
3399
+ /**
3400
+ * `groupArraySample(max_size[, seed])(x)`: "Creates an array of sample argument values. The size of the resulting array
3401
+ * is limited to max_size elements. Argument values are selected and added to the array randomly"
3402
+ * (https://clickhouse.com/docs/sql-reference/aggregate-functions/reference/grouparraysample): a reservoir sample. With
3403
+ * no seed, ClickHouse seeds from a thread-local generator; a World must replay byte for byte, so the twin seeds from what
3404
+ * the World holds (R9: a stable hash of the values sampled and the size asked), and a given `seed` seeds it instead.
3405
+ */
3406
+ function sampleOf(values, params) {
3407
+ const max = Math.max(0, Number(asNumber(params[0]) ?? 0));
3408
+ if (values.length <= max)
3409
+ return [...values];
3410
+ const seedText = params[1] !== undefined ? `seed:${valueToText(params[1])}` : `values:${max}:${values.map(valueKey).join('\u0001')}`;
3411
+ let state = parseInt(digest('sha256', seedText).slice(0, 8), 16) >>> 0;
3412
+ const nextUniform = () => {
3413
+ state = (state + 0x6d2b79f5) >>> 0;
3414
+ let t = state;
3415
+ t = Math.imul(t ^ (t >>> 15), t | 1);
3416
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
3417
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
3418
+ };
3419
+ const reservoir = values.slice(0, max);
3420
+ for (let i = max; i < values.length; i++) {
3421
+ const j = Math.floor(nextUniform() * (i + 1));
3422
+ if (j < max)
3423
+ reservoir[j] = values[i];
3424
+ }
3425
+ return reservoir;
3426
+ }
3427
+ function evalAggregate(e, env) {
3428
+ if (!env.group)
3429
+ return fail(`Aggregate function ${e.name}() is found in a clause where it is not allowed (WHERE, PREWHERE, ON or an array function)`);
3430
+ const agg = parseAggregate(e.name);
3431
+ const rows = env.group;
3432
+ const rowEnv = (row) => ({ scope: env.scope, row, aliasCache: new Map(), evaluating: new Set(env.evaluating), ...(env.lambda ? { lambda: env.lambda } : {}), ...(env.aj ? { aj: env.aj } : {}) });
3433
+ const star = e.args.length === 1 && e.args[0].k === 'star';
3434
+ const nArgs = agg.cond ? e.args.length - 1 : e.args.length;
3435
+ const mask = agg.cond ? rows.map((r) => truth(evalExpr(e.args[e.args.length - 1], rowEnv(r))) === true) : rows.map(() => true);
3436
+ const live = rows.filter((_, i) => mask[i]);
3437
+ const params = (e.params ?? []).map((p) => evalExpr(p, { ...env, group: undefined }));
3438
+ const { base } = agg;
3439
+ if ((base === 'sum' || base === 'avg') && agg.mode !== 'merge')
3440
+ rejectStringArgument(e, env);
3441
+ if (agg.mode === 'merge') {
3442
+ const states = live.map((r) => evalExpr(e.args[0], rowEnv(r))).filter((s) => s instanceof AggState);
3443
+ const want = stateName(base, params);
3444
+ if (states.some((s) => s.fn.toLowerCase().replace(/\s+/g, '') !== want))
3445
+ fail(`${e.name}: the argument holds a state of a different aggregate function`);
3446
+ let st = null;
3447
+ for (const s of states)
3448
+ st = mergeStates(base, st, s.value);
3449
+ const colType = e.args[0] ? typeOf(e.args[0], env.scope) : '';
3450
+ const inner = typeArgs(colType)[1] ?? 'String';
3451
+ return finishState(base, params, st, inner.replace(/^Nullable\((.*)\)$/s, '$1'));
3452
+ }
3453
+ // count(), count(*) and count(DISTINCT …).
3454
+ if (base === 'count' && (nArgs === 0 || star)) {
3455
+ const n = live.length;
3456
+ return agg.mode === 'state' ? new AggState('count', n) : n;
3457
+ }
3458
+ // uniq(a, b, …) hashes the tuple.
3459
+ const tupleArgs = (base.startsWith('uniq') || base === 'countdistinct') && nArgs > 1;
3460
+ let values;
3461
+ let keys;
3462
+ if (star) {
3463
+ // `uniq(*)` hashes the whole row (every column of every joined source).
3464
+ values = live.map((r) => r.map((s) => s.map(valueKey).join('|')).join('||'));
3465
+ }
3466
+ else if (tupleArgs) {
3467
+ values = live.map((r) => e.args.slice(0, nArgs).map((a) => evalExpr(a, rowEnv(r))));
3468
+ }
3469
+ else if (base === 'argmin' || base === 'argmax') {
3470
+ const pairs = live.map((r) => [evalExpr(e.args[0], rowEnv(r)), evalExpr(e.args[1], rowEnv(r))]);
3471
+ values = pairs.map((p) => p[0]);
3472
+ keys = pairs.map((p) => p[1]);
3473
+ }
3474
+ else {
3475
+ values = live.map((r) => evalExpr(e.args[0], rowEnv(r))).filter((v) => v !== null && v !== undefined);
3476
+ }
3477
+ if (e.distinct)
3478
+ values = buildState('uniq', values, undefined);
3479
+ const state = buildState(base, values, keys);
3480
+ if (agg.mode === 'state')
3481
+ return new AggState(stateName(base, params), state);
3482
+ const argType = e.args[0] && !star ? typeOf(e.args[0], env.scope) : 'UInt64';
3483
+ return finishState(base, params, state, argType);
3484
+ }
3485
+ // ── static checking (what ClickHouse's analyzer refuses before reading a row) ─────────────
3486
+ //
3487
+ // ClickHouse resolves every identifier, function and argument type when it ANALYZES a query, so an
3488
+ // unknown column, an unknown function or `'a' + 1` is its 400 whether or not the table holds rows
3489
+ // (and so at `tb push`, which runs each node through the analyzer). The executor runs this pass on
3490
+ // every SELECT once its FROM is bound: names resolve by exactly the rules `resolveId` applies, a
3491
+ // function must be one this engine implements (anything else is the honest `Unknown function`),
3492
+ // and the type rules below fire only when a String operand is CERTAIN (a literal, a declared
3493
+ // Data Source column, a CAST or a function whose result is String) — never on an inferred guess.
3494
+ /** Every scalar function `evalFunction` implements (lower-case), besides the conversion family. */
3495
+ const SCALAR_FUNCTIONS = new Set([
3496
+ 'abs', 'adddays', 'addhours', 'addminutes', 'addmonths', 'addseconds', 'addweeks', 'addyears', 'array', 'arrayall', 'arrayconcat',
3497
+ 'arraycount', 'arraydistinct', 'arrayelement', 'arrayexists', 'arrayfilter', 'arrayfirst', 'arrayintersect', 'arrayjoin', 'arraymap',
3498
+ 'arraypushback', 'arraypushfront', 'arrayreverse', 'arrayreversesort', 'arrayslice', 'arraysort', 'arraystringconcat', 'arraysum',
3499
+ 'assumenotnull', 'base64decode', 'base64encode', 'ceil', 'ceiling', 'char_length', 'coalesce', 'concat', 'cutquerystring', 'date_bin',
3500
+ 'date_diff', 'datediff', 'decodeurlcomponent', 'decodeurlformcomponent', 'divide', 'domain', 'domainwithoutwww', 'empty',
3501
+ 'encodeurlcomponent', 'encodeurlformcomponent', 'endswith', 'equals', 'extracturlparameter', 'extracturlparameternames', 'floor', 'formatdatetime',
3502
+ 'mapcontains', 'mapcontainskey', 'mapfilter', 'mapkeys', 'mapvalues',
3503
+ 'fromunixtimestamp', 'fromunixtimestamp64micro', 'fromunixtimestamp64milli', 'fromunixtimestamp64nano', 'greatest', 'has', 'hasall',
3504
+ 'hasany', 'hex', 'identity', 'if', 'ifnull', 'indexof', 'intdiv', 'isnotnull', 'isnull', 'jsonextractbool', 'jsonextractfloat',
3505
+ 'jsonextractint', 'jsonextractraw', 'jsonextractstring', 'jsonhas', 'lcase', 'least', 'leftpad', 'length', 'lengthutf8', 'locate',
3506
+ 'lower', 'lowerutf8', 'ltrim', 'match', 'materialize', 'mid', 'minus', 'modulo', 'multiif', 'multiply', 'notempty', 'notequals', 'now',
3507
+ 'now64', 'nullif', 'parsedatetime64besteffort', 'parsedatetimebesteffort', 'parsedatetimebesteffortornull', 'path', 'plus',
3508
+ 'position', 'pow', 'power', 'range', 'replace', 'replaceall', 'replaceone', 'replaceregexpall', 'replaceregexpone', 'reverse',
3509
+ 'rightpad', 'round', 'rtrim', 'splitbychar', 'splitbystring', 'sqrt', 'startswith', 'substr', 'substring', 'subtractdays',
3510
+ 'subtracthours', 'subtractminutes', 'subtractmonths', 'subtractseconds', 'subtractweeks', 'subtractyears', 'time_bucket',
3511
+ 'timestampdiff', 'todate', 'todatetime', 'todatetime64', 'todatetime64orzero', 'today', 'todayofmonth', 'todayofweek', 'todayofyear',
3512
+ 'tofloat32', 'tofloat64', 'tohour', 'tolowcardinality', 'tominute', 'tomonday', 'tomonth', 'toquarter', 'tosecond', 'tostartofday',
3513
+ 'tostartoffifteenminutes', 'tostartoffiveminute', 'tostartoffiveminutes', 'tostartofhour', 'tostartofinterval', 'tostartofminute',
3514
+ 'tostartofmonth', 'tostartofquarter', 'tostartofsecond', 'tostartofweek', 'tostartofyear', 'tostring', 'totimezone', 'totypename',
3515
+ 'tounixtimestamp', 'tounixtimestamp64micro', 'tounixtimestamp64milli', 'tounixtimestamp64nano', 'toyear', 'toyyyymm', 'toyyyymmdd',
3516
+ 'toyyyymmddhhmmss', 'trim', 'trimboth', 'trimleft', 'trimright', 'tuple', 'ucase', 'upper', 'upperutf8', 'yesterday',
3517
+ 'touuid', 'touuidornull',
3518
+ ]);
3519
+ const INTERVAL_FUNCTIONS = new Set(['millisecond', 'second', 'minute', 'hour', 'day', 'week', 'month', 'quarter', 'year']);
3520
+ /** Functions whose result is always a String. */
3521
+ const STRING_FUNCTIONS = new Set([
3522
+ 'tostring', 'concat', 'replace', 'replaceall', 'replaceone', 'replaceregexpall', 'replaceregexpone', 'lower', 'upper', 'lowerutf8',
3523
+ 'upperutf8', 'substring', 'substr', 'trim', 'trimboth', 'trimleft', 'trimright', 'encodeurlformcomponent', 'decodeurlformcomponent',
3524
+ 'encodeurlcomponent', 'decodeurlcomponent', 'extracturlparameter', 'jsonextractstring', 'jsonextractraw', 'formatdatetime', 'domain',
3525
+ 'domainwithoutwww', 'path', 'cutquerystring', 'leftpad', 'rightpad', 'base64encode', 'base64decode', 'hex', 'totypename',
3526
+ ]);
3527
+ export function isKnownFunction(name) {
3528
+ const n = name.toLowerCase();
3529
+ if (SCALAR_FUNCTIONS.has(n) || isAggregateName(n))
3530
+ return true;
3531
+ if (n.startsWith('tointerval'))
3532
+ return INTERVAL_FUNCTIONS.has(n.slice(10));
3533
+ return /^to(u?int(?:8|16|32|64|128|256)|float(?:32|64))(orzero|ornull)?$/.test(n);
3534
+ }
3535
+ /** Resolve an identifier the way `resolveId` does, without a row; an unresolvable name is the 400. */
3536
+ function checkId(parts, scope) {
3537
+ if (parts.length === 1) {
3538
+ const name = parts[0];
3539
+ if (scope.aliases.has(name) || findColumn(scope, name) || scope.withExprs.has(name))
3540
+ return;
3541
+ if (scope.outer)
3542
+ return checkId(parts, scope.outer);
3543
+ fail(`Missing columns: '${name}' while processing query`);
3544
+ }
3545
+ const qualifier = parts.slice(0, -1).join('.');
3546
+ const column = parts[parts.length - 1];
3547
+ const s = findSource(scope, qualifier);
3548
+ if (s !== undefined) {
3549
+ const src = scope.sources[s];
3550
+ if (src.index.has(column) || src.index.has(parts.join('.')))
3551
+ return;
3552
+ fail(`Missing columns: '${parts.join('.')}' while processing query`);
3553
+ }
3554
+ if (findColumn(scope, parts.join('.')))
3555
+ return;
3556
+ checkId([column], scope);
3557
+ }
3558
+ /** A qualified column reference (`t.col`): its source and column, or undefined. */
3559
+ function qualifiedColumn(scope, parts) {
3560
+ const q = findSource(scope, parts.slice(0, -1).join('.'));
3561
+ const c = q === undefined ? undefined : scope.sources[q].index.get(parts[parts.length - 1]);
3562
+ return q === undefined || c === undefined ? undefined : { s: q, c };
3563
+ }
3564
+ /** Is this expression's type CERTAINLY a String (never a guess)? */
3565
+ function certainlyString(e, scope, lambdas, seen = new Set()) {
3566
+ switch (e.k) {
3567
+ case 'lit': return e.type === 'String';
3568
+ case 'alias': return certainlyString(e.e, scope, lambdas, seen);
3569
+ case 'cast': return /^(String|FixedString)$/.test(typeHead(e.type));
3570
+ case 'fn': return STRING_FUNCTIONS.has(e.name.toLowerCase());
3571
+ case 'id': {
3572
+ if (e.parts.length === 1) {
3573
+ const name = e.parts[0];
3574
+ if (lambdas.has(name))
3575
+ return false;
3576
+ const alias = scope.aliases.get(name);
3577
+ if (alias && !seen.has(name)) {
3578
+ seen.add(name);
3579
+ return certainlyString(alias, scope, lambdas, seen);
3580
+ }
3581
+ }
3582
+ const s = e.parts.length === 1 ? findColumn(scope, e.parts[0]) : qualifiedColumn(scope, e.parts);
3583
+ if (!s)
3584
+ return false;
3585
+ const src = scope.sources[s.s];
3586
+ return src.declared === true && /^(String|FixedString|UUID)$/.test(typeHead(src.columns[s.c].type));
3587
+ }
3588
+ default: return false;
3589
+ }
3590
+ }
3591
+ const ARITHMETIC_NAMES = { '+': 'plus', '-': 'minus', '*': 'multiply', '/': 'divide', '%': 'modulo' };
3592
+ /**
3593
+ * The output name of a joined column. ClickHouse names a column of a joined (non-first) table
3594
+ * `<alias>.<column>` when an earlier table of the join has a column of the same name, and the bare
3595
+ * column otherwise — `SELECT e.id, d.id FROM ev e JOIN dim d …` answers `id` and `d.id`, and
3596
+ * `SELECT *` over the same join answers `id, …, d.id, …` (clickhouse-local 26.10).
3597
+ */
3598
+ function joinedColumnName(scope, s, column) {
3599
+ const src = scope.sources[s];
3600
+ const q = src.alias ?? src.name;
3601
+ if (s === 0 || !q)
3602
+ return column;
3603
+ return scope.sources.slice(0, s).some((o) => o.index.has(column)) ? `${q}.${column}` : column;
3604
+ }
3605
+ /** A projected identifier's output name (see `joinedColumnName`). */
3606
+ function idOutputName(scope, parts) {
3607
+ const column = parts[parts.length - 1];
3608
+ if (parts.length > 1) {
3609
+ const s = findSource(scope, parts.slice(0, -1).join('.'));
3610
+ if (s !== undefined && scope.sources[s].index.has(column))
3611
+ return joinedColumnName(scope, s, column);
3612
+ return column;
3613
+ }
3614
+ if (scope.aliases.has(column))
3615
+ return column;
3616
+ const at = findColumn(scope, column);
3617
+ return at ? joinedColumnName(scope, at.s, column) : column;
3618
+ }
3619
+ // ── the executor ────────────────────────────────────────────────────────────────────────────
3620
+ export class Executor {
3621
+ catalog;
3622
+ nowMs;
3623
+ validating;
3624
+ rowsRead = 0;
3625
+ bytesRead = 0;
3626
+ /** `validating`: publish-time analysis — subqueries in expressions are analyzed even when no row reaches them. */
3627
+ constructor(catalog, nowMs, validating = false) {
3628
+ this.catalog = catalog;
3629
+ this.nowMs = nowMs;
3630
+ this.validating = validating;
3631
+ }
3632
+ /** Resolve a FROM name: CTEs in scope first, then the catalog. */
3633
+ table(name, final, scope) {
3634
+ for (let s = scope; s; s = s.outer) {
3635
+ const cte = s.ctes?.get(name);
3636
+ if (cte)
3637
+ return this.run(cte, s);
3638
+ }
3639
+ const rel = this.catalog.table(name, final);
3640
+ if (!rel)
3641
+ throw new ClickHouseError(`Resource '${name}' not found`, 404);
3642
+ this.rowsRead += rel.rows.length;
3643
+ this.bytesRead += rel.rows.reduce((n, r) => n + stringifyJson(r.map(toOutput)).length, 0);
3644
+ return rel;
3645
+ }
3646
+ run(q, outer) {
3647
+ const parts = q.selects.map((s) => this.select(s, outer));
3648
+ if (parts.length === 1)
3649
+ return parts[0].rel;
3650
+ const first = parts[0].rel;
3651
+ for (const p of parts.slice(1)) {
3652
+ if (p.rel.columns.length !== first.columns.length)
3653
+ fail('UNION ALL: different number of columns in the queries');
3654
+ }
3655
+ return { columns: first.columns, rows: parts.flatMap((p) => p.rel.rows) };
3656
+ }
3657
+ /** Run and also report rows_before_limit (for the response envelope). */
3658
+ runTop(q) {
3659
+ if (q.selects.length === 1)
3660
+ return this.select(q.selects[0], undefined);
3661
+ return { rel: this.run(q) };
3662
+ }
3663
+ select(sel, outer) {
3664
+ const scope = {
3665
+ sources: [], usingKeys: new Set(), aliases: new Map(), withExprs: new Map(), withCache: new Map(),
3666
+ exec: this, subCache: new Map(), ...(outer ? { outer } : {}),
3667
+ };
3668
+ for (const w of sel.with) {
3669
+ if (w.query) {
3670
+ scope.ctes ??= new Map();
3671
+ scope.ctes.set(w.name, w.query);
3672
+ }
3673
+ else if (w.expr)
3674
+ scope.withExprs.set(w.name, w.expr);
3675
+ }
3676
+ // Aliases are query-wide (ClickHouse's analyzer), including ones nested in expressions.
3677
+ for (const item of sel.items)
3678
+ collectAliases(item, scope.aliases);
3679
+ for (const w of sel.with)
3680
+ if (w.expr)
3681
+ collectAliases(w.expr, scope.withExprs);
3682
+ for (const x of [sel.prewhere, sel.where, sel.having, ...sel.groupBy, ...sel.orderBy.map((o) => o.e)])
3683
+ if (x)
3684
+ collectAliases(x, scope.aliases);
3685
+ // FROM
3686
+ const rows = this.from(sel.from, scope);
3687
+ this.check(sel, scope);
3688
+ // arrayJoin in the SELECT list multiplies rows before filtering.
3689
+ const ajNodes = [];
3690
+ const findAj = (e) => {
3691
+ if (e.k === 'fn' && e.name.toLowerCase() === 'arrayjoin') {
3692
+ ajNodes.push(e);
3693
+ return;
3694
+ }
3695
+ forEachChild(e, findAj);
3696
+ };
3697
+ sel.items.forEach(findAj);
3698
+ let ajRows = rows.map((row) => ({ row }));
3699
+ if (ajNodes.length > 0) {
3700
+ const expanded = [];
3701
+ for (const { row } of ajRows) {
3702
+ let combos = [new Map()];
3703
+ for (const node of ajNodes) {
3704
+ const next = [];
3705
+ for (const combo of combos) {
3706
+ const env = { scope, row, aliasCache: new Map(), evaluating: new Set(), aj: combo };
3707
+ if (node.args.length !== 1)
3708
+ fail('arrayJoin requires exactly one argument');
3709
+ const arr = evalExpr(node.args[0], env);
3710
+ if (!Array.isArray(arr))
3711
+ fail(`arrayJoin: argument must be an array, got ${valueToText(arr)}`);
3712
+ for (const v of arr) {
3713
+ const m = new Map(combo);
3714
+ m.set(node, v);
3715
+ next.push(m);
3716
+ }
3717
+ }
3718
+ combos = next;
3719
+ }
3720
+ for (const aj of combos)
3721
+ expanded.push({ row, aj });
3722
+ }
3723
+ ajRows = expanded;
3724
+ }
3725
+ const envOf = (r) => ({ scope, row: r.row, aliasCache: new Map(), evaluating: new Set(), ...(r.aj ? { aj: r.aj } : {}) });
3726
+ for (const cond of [sel.prewhere, sel.where]) {
3727
+ if (!cond)
3728
+ continue;
3729
+ if (containsAggregate(cond, scope.aliases))
3730
+ fail('Aggregate function is found in WHERE or PREWHERE in query');
3731
+ ajRows = ajRows.filter((r) => truth(evalExpr(cond, envOf(r))) === true);
3732
+ }
3733
+ const aggregate = sel.groupBy.length > 0 || sel.items.some((i) => containsAggregate(i, scope.aliases)) || (sel.having !== undefined);
3734
+ // Expand * and t.* in place.
3735
+ const finalItems = [];
3736
+ for (const item of sel.items) {
3737
+ if (item.k === 'star') {
3738
+ const srcs = item.qualifier === undefined
3739
+ ? scope.sources.map((_, i) => i)
3740
+ : [findSource(scope, item.qualifier) ?? fail(`Unknown table expression identifier '${item.qualifier}'`)];
3741
+ for (const s of srcs) {
3742
+ const src = scope.sources[s];
3743
+ for (const c of src.columns) {
3744
+ if (item.qualifier === undefined && s > 0 && scope.usingKeys.has(c.name))
3745
+ continue;
3746
+ const q = src.alias ?? src.name;
3747
+ finalItems.push({ name: joinedColumnName(scope, s, c.name), e: { k: 'id', parts: q ? [q, c.name] : [c.name] } });
3748
+ }
3749
+ }
3750
+ continue;
3751
+ }
3752
+ finalItems.push({ name: item.k === 'id' ? idOutputName(scope, item.parts) : exprName(item), e: item });
3753
+ }
3754
+ if (finalItems.length === 0)
3755
+ fail('Empty list of columns in SELECT query');
3756
+ // an item's own alias does not name the item inside it (`groupArraySample(1)(p) AS p` reads the column p)
3757
+ const columns = finalItems.map((it) => ({ name: it.name, type: typeOf(it.e, scope, new Set([it.name])) }));
3758
+ let out = [];
3759
+ if (aggregate) {
3760
+ // Validate that every plain column in the output is grouped (ClickHouse's NOT_AN_AGGREGATE).
3761
+ // Positional GROUP BY is off by default in ClickHouse: a constant key groups everything.
3762
+ const groupKeys = sel.groupBy;
3763
+ checkGroupKeys(sel, scope.aliases);
3764
+ for (const it of finalItems)
3765
+ checkAggregateProjection(it.e, groupKeys, scope);
3766
+ checkAggregateOrder(sel, scope, finalItems.map((it) => it.name));
3767
+ const groups = new Map();
3768
+ const order = [];
3769
+ for (const r of ajRows) {
3770
+ const env = envOf(r);
3771
+ const key = groupKeys.map((g) => valueKey(evalExpr(g, env))).join('\u0001');
3772
+ let g = groups.get(key);
3773
+ if (!g) {
3774
+ g = [];
3775
+ groups.set(key, g);
3776
+ order.push(key);
3777
+ }
3778
+ g.push(r);
3779
+ }
3780
+ if (sel.groupBy.length === 0 && order.length === 0) {
3781
+ groups.set('', []);
3782
+ order.push('');
3783
+ }
3784
+ for (const key of order) {
3785
+ const members = groups.get(key);
3786
+ const first = members[0] ?? { row: this.defaultRow(scope) };
3787
+ const env = { scope, row: first.row, group: members.map((m) => m.row), aliasCache: new Map(), evaluating: new Set(), ...(first.aj ? { aj: first.aj } : {}) };
3788
+ if (sel.having && truth(evalExpr(sel.having, env)) !== true)
3789
+ continue;
3790
+ const values = finalItems.map((it) => evalExpr(it.e, env));
3791
+ const keys = sel.orderBy.map((o) => this.orderValue(o.e, env, finalItems, values));
3792
+ out.push({ values, keys });
3793
+ }
3794
+ }
3795
+ else {
3796
+ for (const r of ajRows) {
3797
+ const env = envOf(r);
3798
+ const values = finalItems.map((it) => evalExpr(it.e, env));
3799
+ const keys = sel.orderBy.map((o) => this.orderValue(o.e, env, finalItems, values));
3800
+ out.push({ values, keys });
3801
+ }
3802
+ }
3803
+ if (sel.distinct) {
3804
+ const seen = new Set();
3805
+ out = out.filter((o) => { const k = o.values.map(valueKey).join('\u0001'); if (seen.has(k))
3806
+ return false; seen.add(k); return true; });
3807
+ }
3808
+ if (sel.orderBy.length > 0) {
3809
+ const decorated = out.map((o, i) => ({ o, i }));
3810
+ decorated.sort((x, y) => {
3811
+ for (let k = 0; k < sel.orderBy.length; k++) {
3812
+ const spec = sel.orderBy[k];
3813
+ const a = x.o.keys[k];
3814
+ const b = y.o.keys[k];
3815
+ const an = a === null || a === undefined || (typeof a === 'number' && Number.isNaN(a));
3816
+ const bn = b === null || b === undefined || (typeof b === 'number' && Number.isNaN(b));
3817
+ if (an || bn) {
3818
+ if (an && bn)
3819
+ continue;
3820
+ return (an ? 1 : -1) * (spec.nullsFirst ? -1 : 1);
3821
+ }
3822
+ const c = compareValues(a, b);
3823
+ if (c !== 0)
3824
+ return spec.desc ? -c : c;
3825
+ }
3826
+ return x.i - y.i;
3827
+ });
3828
+ out = decorated.map((d) => d.o);
3829
+ }
3830
+ const before = out.length;
3831
+ const constEnv = { scope: { ...scope, sources: [] }, row: [], aliasCache: new Map(), evaluating: new Set() };
3832
+ const offset = sel.offset ? Math.max(0, Math.trunc(asNumber(evalExpr(sel.offset, constEnv)) ?? 0)) : 0;
3833
+ const limit = sel.limit ? Math.max(0, Math.trunc(asNumber(evalExpr(sel.limit, constEnv)) ?? 0)) : undefined;
3834
+ if (offset > 0 || limit !== undefined)
3835
+ out = out.slice(offset, limit === undefined ? undefined : offset + limit);
3836
+ return {
3837
+ rel: { columns, rows: out.map((o) => o.values) },
3838
+ ...(sel.limit || sel.offset ? { rowsBeforeLimit: before } : {}),
3839
+ };
3840
+ }
3841
+ /** The analyzer pass: every expression of the SELECT resolves (see "static checking"). */
3842
+ check(sel, scope) {
3843
+ const exprs = [...sel.items];
3844
+ for (const w of sel.with)
3845
+ if (w.expr)
3846
+ exprs.push(w.expr);
3847
+ for (const f of sel.from)
3848
+ if (f.join?.on)
3849
+ exprs.push(f.join.on);
3850
+ for (const x of [sel.prewhere, sel.where, sel.having, sel.limit, sel.offset, ...sel.groupBy, ...sel.orderBy.map((o) => o.e)])
3851
+ if (x)
3852
+ exprs.push(x);
3853
+ for (const e of exprs)
3854
+ this.checkExpr(e, scope, new Set());
3855
+ }
3856
+ checkExpr(e, scope, lambdas) {
3857
+ const sub = (x) => this.checkExpr(x, scope, lambdas);
3858
+ switch (e.k) {
3859
+ case 'id':
3860
+ if (e.parts.length === 1 && lambdas.has(e.parts[0]))
3861
+ return;
3862
+ checkId(e.parts, scope);
3863
+ return;
3864
+ case 'lambda':
3865
+ this.checkExpr(e.body, scope, new Set([...lambdas, ...e.params]));
3866
+ return;
3867
+ case 'subquery':
3868
+ if (this.validating)
3869
+ this.run(e.q, scope);
3870
+ return;
3871
+ case 'cast':
3872
+ if (!isModeledSchemaType(e.type))
3873
+ fail(`Unknown data type family: ${typeHead(e.type)}`);
3874
+ sub(e.e);
3875
+ return;
3876
+ case 'fn': {
3877
+ if (!isKnownFunction(e.name))
3878
+ fail(`Unknown function ${e.name}`);
3879
+ const lname = e.name.toLowerCase();
3880
+ const agg = parseAggregate(lname);
3881
+ if (agg && (agg.base === 'sum' || agg.base === 'avg') && agg.mode !== 'merge' && e.args[0] && certainlyString(e.args[0], scope, lambdas)) {
3882
+ fail(`Illegal type String of argument for aggregate function ${e.name}`);
3883
+ }
3884
+ e.params?.forEach(sub);
3885
+ for (const a of e.args)
3886
+ if (a.k !== 'star')
3887
+ sub(a);
3888
+ return;
3889
+ }
3890
+ case 'bin': {
3891
+ if (e.op === 'in' || e.op === 'notin') {
3892
+ sub(e.a);
3893
+ const rhs = e.b;
3894
+ if (rhs.k === 'id' && rhs.parts.length === 1 && !lambdas.has(rhs.parts[0]) && !findColumn(scope, rhs.parts[0]) && !scope.aliases.has(rhs.parts[0]) && !scope.withExprs.has(rhs.parts[0])) {
3895
+ if (this.validating)
3896
+ this.table(rhs.parts[0], false, scope);
3897
+ return;
3898
+ }
3899
+ sub(rhs);
3900
+ return;
3901
+ }
3902
+ const op = ARITHMETIC_NAMES[e.op];
3903
+ if (op) {
3904
+ const isInterval = (x) => x.k === 'interval' || (x.k === 'fn' && /^tointerval/i.test(x.name));
3905
+ if (!isInterval(e.a) && !isInterval(e.b) && (certainlyString(e.a, scope, lambdas) || certainlyString(e.b, scope, lambdas))) {
3906
+ fail(`Illegal types ${typeOf(e.a, scope)} and ${typeOf(e.b, scope)} of arguments of function ${op}`);
3907
+ }
3908
+ }
3909
+ sub(e.a);
3910
+ sub(e.b);
3911
+ return;
3912
+ }
3913
+ default: forEachChild(e, sub);
3914
+ }
3915
+ }
3916
+ orderValue(e, env, items, values) {
3917
+ // `ORDER BY <output alias>` reads the computed output column.
3918
+ if (e.k === 'id' && e.parts.length === 1) {
3919
+ const i = items.findIndex((it) => it.name === e.parts[0]);
3920
+ if (i >= 0)
3921
+ return values[i];
3922
+ }
3923
+ const same = items.findIndex((it) => sameExpr(it.e, e));
3924
+ if (same >= 0)
3925
+ return values[same];
3926
+ return evalExpr(e, env);
3927
+ }
3928
+ defaultRow(scope) {
3929
+ return scope.sources.map((s) => s.columns.map((c) => defaultOf(c.type)));
3930
+ }
3931
+ relationOf(item, scope) {
3932
+ if (item.source.kind === 'sub')
3933
+ return this.run(item.source.query, scope);
3934
+ return this.table(item.source.name, item.final, scope);
3935
+ }
3936
+ from(items, scope) {
3937
+ if (items.length === 0)
3938
+ return [[]];
3939
+ const first = items[0];
3940
+ const firstRel = this.relationOf(first, scope);
3941
+ scope.sources.push(makeSource(firstRel, first.alias, first.source.kind === 'table' ? first.source.name : undefined));
3942
+ let rows = firstRel.rows.map((r) => [r]);
3943
+ for (const item of items.slice(1)) {
3944
+ const rel = this.relationOf(item, scope);
3945
+ const src = makeSource(rel, item.alias, item.source.kind === 'table' ? item.source.name : undefined);
3946
+ const leftSources = scope.sources.slice();
3947
+ scope.sources.push(src);
3948
+ const join = item.join;
3949
+ const rightDefault = rel.columns.map((c) => defaultOf(c.type));
3950
+ const leftDefault = leftSources.map((s) => s.columns.map((c) => defaultOf(c.type)));
3951
+ if (join.kind === 'cross') {
3952
+ const next = [];
3953
+ for (const l of rows)
3954
+ for (const r of rel.rows)
3955
+ next.push([...l, r]);
3956
+ rows = next;
3957
+ continue;
3958
+ }
3959
+ if (join.using) {
3960
+ const using = join.using;
3961
+ for (const k of using)
3962
+ scope.usingKeys.add(k);
3963
+ const leftScope = { ...scope, sources: leftSources, aliases: new Map() };
3964
+ const leftKey = (l) => using.map((k) => {
3965
+ const col = findColumn(leftScope, k);
3966
+ if (!col)
3967
+ return fail(`USING column '${k}' is not found in the left table`);
3968
+ return l[col.s][col.c];
3969
+ });
3970
+ const rightIdx = using.map((k) => src.index.get(k) ?? fail(`USING column '${k}' is not found in the right table`));
3971
+ // Hash the right side on the USING key.
3972
+ const buckets = new Map();
3973
+ for (const r of rel.rows) {
3974
+ const key = rightIdx.map((i) => valueKey(r[i])).join('\u0001');
3975
+ let b = buckets.get(key);
3976
+ if (!b) {
3977
+ b = [];
3978
+ buckets.set(key, b);
3979
+ }
3980
+ b.push(r);
3981
+ }
3982
+ const probe = (l) => {
3983
+ const lk = leftKey(l);
3984
+ if (lk.some((v) => v === null))
3985
+ return [];
3986
+ const key = lk.map((v, i) => {
3987
+ const sample = rel.rows.find((r) => r[rightIdx[i]] !== null)?.[rightIdx[i]];
3988
+ return valueKeyAs(v, sample);
3989
+ }).join('\u0001');
3990
+ return buckets.get(key) ?? [];
3991
+ };
3992
+ rows = this.combine(rows, rel.rows, join, probe, rightDefault, leftDefault);
3993
+ continue;
3994
+ }
3995
+ const on = join.on;
3996
+ const env0 = { scope, row: [], aliasCache: new Map(), evaluating: new Set() };
3997
+ const matches = (l, r) => truth(evalExpr(on, { ...env0, row: [...l, r], aliasCache: new Map() })) === true;
3998
+ const probe = (l) => rel.rows.filter((r) => matches(l, r));
3999
+ rows = this.combine(rows, rel.rows, join, probe, rightDefault, leftDefault);
4000
+ }
4001
+ return rows;
4002
+ }
4003
+ combine(left, right, join, probe, rightDefault, leftDefault) {
4004
+ const out = [];
4005
+ const matchedRight = new Set();
4006
+ for (const l of left) {
4007
+ let hits = probe(l);
4008
+ if (join.strictness === 'any' && hits.length > 1)
4009
+ hits = [hits[0]];
4010
+ if (join.strictness === 'semi') {
4011
+ if (hits.length)
4012
+ out.push([...l, hits[0]]);
4013
+ continue;
4014
+ }
4015
+ if (join.strictness === 'anti') {
4016
+ if (!hits.length)
4017
+ out.push([...l, rightDefault]);
4018
+ continue;
4019
+ }
4020
+ if (hits.length === 0) {
4021
+ if (join.kind === 'left' || join.kind === 'full')
4022
+ out.push([...l, rightDefault]);
4023
+ continue;
4024
+ }
4025
+ for (const r of hits) {
4026
+ matchedRight.add(r);
4027
+ out.push([...l, r]);
4028
+ }
4029
+ }
4030
+ if (join.kind === 'right' || join.kind === 'full') {
4031
+ for (const r of right)
4032
+ if (!matchedRight.has(r))
4033
+ out.push([...leftDefault, r]);
4034
+ }
4035
+ return out;
4036
+ }
4037
+ }
4038
+ function forEachChild(e, fn) {
4039
+ switch (e.k) {
4040
+ case 'alias':
4041
+ fn(e.e);
4042
+ return;
4043
+ case 'fn':
4044
+ e.args.forEach(fn);
4045
+ return;
4046
+ case 'bin':
4047
+ fn(e.a);
4048
+ fn(e.b);
4049
+ return;
4050
+ case 'not':
4051
+ case 'neg':
4052
+ case 'isnull':
4053
+ fn(e.a);
4054
+ return;
4055
+ case 'between':
4056
+ fn(e.a);
4057
+ fn(e.lo);
4058
+ fn(e.hi);
4059
+ return;
4060
+ case 'case':
4061
+ if (e.operand)
4062
+ fn(e.operand);
4063
+ e.whens.forEach(([a, b]) => { fn(a); fn(b); });
4064
+ if (e.else)
4065
+ fn(e.else);
4066
+ return;
4067
+ case 'array':
4068
+ case 'tuple':
4069
+ e.items.forEach(fn);
4070
+ return;
4071
+ case 'index':
4072
+ fn(e.a);
4073
+ fn(e.i);
4074
+ return;
4075
+ case 'interval':
4076
+ fn(e.n);
4077
+ return;
4078
+ case 'cast':
4079
+ fn(e.e);
4080
+ return;
4081
+ case 'lambda':
4082
+ fn(e.body);
4083
+ return;
4084
+ default: return;
4085
+ }
4086
+ }
4087
+ /** GROUP BY keys are row expressions: an aggregate there (directly or through an alias) is refused. */
4088
+ function checkGroupKeys(sel, aliases) {
4089
+ for (const g of sel.groupBy) {
4090
+ if (containsAggregate(g, aliases))
4091
+ fail(`Aggregate function found in GROUP BY: ${exprText(g)}`);
4092
+ }
4093
+ }
4094
+ /** In an aggregate query ORDER BY and HAVING may only use what the grouping produces. */
4095
+ function checkAggregateOrder(sel, scope, outputNames = []) {
4096
+ const names = new Set([...outputNames, ...sel.items.filter((i) => i.k === 'alias').map((i) => i.name)]);
4097
+ for (const o of sel.orderBy) {
4098
+ if (o.e.k === 'id' && o.e.parts.length === 1 && names.has(o.e.parts[0]))
4099
+ continue;
4100
+ try {
4101
+ checkAggregateProjection(o.e, sel.groupBy, scope);
4102
+ }
4103
+ catch (e) {
4104
+ if (e instanceof ClickHouseError)
4105
+ throw new ClickHouseError(`${e.message} (in ORDER BY)`, e.status);
4106
+ throw e;
4107
+ }
4108
+ }
4109
+ }
4110
+ /** ClickHouse's NOT_AN_AGGREGATE: a plain column in an aggregate query must be a grouping key. */
4111
+ function checkAggregateProjection(e, keys, scope, seen = new Set()) {
4112
+ if (keys.some((k) => sameExpr(k, e)))
4113
+ return;
4114
+ // `expr AS name … GROUP BY name`: the aliased expression is itself the key.
4115
+ if (e.k === 'alias' && keys.some((k) => k.k === 'id' && k.parts.length === 1 && k.parts[0] === e.name))
4116
+ return;
4117
+ switch (e.k) {
4118
+ case 'fn':
4119
+ if (isAggregateName(e.name))
4120
+ return;
4121
+ e.args.forEach((a) => checkAggregateProjection(a, keys, scope, seen));
4122
+ return;
4123
+ case 'id': {
4124
+ if (e.parts.length === 1) {
4125
+ const name = e.parts[0];
4126
+ if (keys.some((k) => k.k === 'id' && k.parts.length === 1 && k.parts[0] === name))
4127
+ return;
4128
+ const alias = scope.aliases.get(name);
4129
+ if (alias && !seen.has(name)) {
4130
+ // An alias that IS a group key (by name or by expression) is fine.
4131
+ if (keys.some((k) => sameExpr(k, alias)))
4132
+ return;
4133
+ seen.add(name);
4134
+ checkAggregateProjection(alias, keys, scope, seen);
4135
+ seen.delete(name);
4136
+ return;
4137
+ }
4138
+ if (scope.withExprs.has(name))
4139
+ return;
4140
+ }
4141
+ const col = e.parts[e.parts.length - 1];
4142
+ if (keys.some((k) => k.k === 'id' && k.parts[k.parts.length - 1] === col))
4143
+ return;
4144
+ // Group keys that are aliases whose expression is this column.
4145
+ if (keys.some((k) => k.k === 'id' && k.parts.length === 1 && scope.aliases.get(k.parts[0]) && sameExpr(scope.aliases.get(k.parts[0]), e)))
4146
+ return;
4147
+ fail(`Column '${e.parts.join('.')}' is not under an aggregate function and not in GROUP BY keys`);
4148
+ return;
4149
+ }
4150
+ case 'lambda': return;
4151
+ case 'subquery': return;
4152
+ default: forEachChild(e, (c) => checkAggregateProjection(c, keys, scope, seen));
4153
+ }
4154
+ }
4155
+ /** Execute a parsed query against a catalog and shape Tinybird's result envelope. */
4156
+ export function executeSql(q, catalog, nowMs, opts = {}) {
4157
+ const exec = new Executor(catalog, nowMs, opts.validate === true);
4158
+ const { rel, rowsBeforeLimit } = exec.runTop(q);
4159
+ return relationResult(rel, exec, rowsBeforeLimit);
4160
+ }
4161
+ /** Run a query to a Relation (for materialized views and node chaining). */
4162
+ export function runSql(q, catalog, nowMs, opts = {}) {
4163
+ const exec = new Executor(catalog, nowMs, opts.validate === true);
4164
+ return exec.run(q);
4165
+ }
4166
+ function relationResult(rel, exec, rowsBeforeLimit) {
4167
+ const data = rel.rows.map((r) => {
4168
+ const o = {};
4169
+ rel.columns.forEach((c, i) => { o[c.name] = toOutput(r[i]); });
4170
+ return o;
4171
+ });
4172
+ return {
4173
+ meta: rel.columns.map((c) => ({ name: c.name, type: c.type })),
4174
+ data,
4175
+ rows: data.length,
4176
+ ...(rowsBeforeLimit !== undefined ? { rowsBeforeLimitAtLeast: rowsBeforeLimit } : {}),
4177
+ rowsRead: exec.rowsRead,
4178
+ bytesRead: exec.bytesRead,
4179
+ };
4180
+ }
4181
+ /** Every table name a query reads (FROM / JOIN / IN-subqueries), excluding its own CTEs. */
4182
+ export function usedTables(q) {
4183
+ const out = new Set();
4184
+ const ctes = new Set();
4185
+ const visitQuery = (query) => query.selects.forEach(visitSelect);
4186
+ const visitExpr = (e) => {
4187
+ if (e.k === 'subquery') {
4188
+ visitQuery(e.q);
4189
+ return;
4190
+ }
4191
+ if ((e.k === 'bin') && (e.op === 'in' || e.op === 'notin') && e.b.k === 'id' && e.b.parts.length === 1) {
4192
+ visitExpr(e.a);
4193
+ return;
4194
+ }
4195
+ forEachChild(e, visitExpr);
4196
+ };
4197
+ const visitSelect = (s) => {
4198
+ for (const w of s.with) {
4199
+ if (w.query) {
4200
+ ctes.add(w.name);
4201
+ visitQuery(w.query);
4202
+ }
4203
+ else if (w.expr)
4204
+ visitExpr(w.expr);
4205
+ }
4206
+ for (const f of s.from) {
4207
+ if (f.source.kind === 'table') {
4208
+ if (!ctes.has(f.source.name))
4209
+ out.add(f.source.name);
4210
+ }
4211
+ else
4212
+ visitQuery(f.source.query);
4213
+ if (f.join?.on)
4214
+ visitExpr(f.join.on);
4215
+ }
4216
+ s.items.forEach(visitExpr);
4217
+ [s.prewhere, s.where, s.having, s.limit, s.offset].forEach((x) => { if (x)
4218
+ visitExpr(x); });
4219
+ s.groupBy.forEach(visitExpr);
4220
+ s.orderBy.forEach((o) => visitExpr(o.e));
4221
+ };
4222
+ visitQuery(q);
4223
+ return [...out];
4224
+ }
4225
+ /** The left-most table of the first SELECT — a materialized view's trigger table. */
4226
+ export function primaryTable(q) {
4227
+ const first = q.selects[0]?.from[0];
4228
+ if (!first)
4229
+ return undefined;
4230
+ if (first.source.kind === 'table')
4231
+ return first.source.name;
4232
+ return primaryTable(first.source.query);
4233
+ }
4234
+ export function queryStatistics(result) {
4235
+ return {
4236
+ elapsed: Number((result.rowsRead * 0.000001 + 0.000123).toFixed(9)),
4237
+ rows_read: result.rowsRead,
4238
+ bytes_read: result.bytesRead,
4239
+ };
4240
+ }
4241
+ // ── response formats ────────────────────────────────────────────────────────────────────────
4242
+ /**
4243
+ * One CSV field as ClickHouse writes it: strings, dates, times, arrays and other compound values
4244
+ * double-quoted (`"` doubled inside), numbers and booleans bare, NULL as `\N`
4245
+ * (`format_csv_null_representation`), an array in its text form (`"['a','b']"`).
4246
+ */
4247
+ function csvCell(v, type) {
4248
+ if (v === null || v === undefined)
4249
+ return '\\N';
4250
+ const h = typeHead(type);
4251
+ const bare = isNumberType(type) || h === 'Bool' || h === 'Boolean' || h === 'Nothing';
4252
+ const text = Array.isArray(v) ? outputArrayText(v, arrayElementType(type)) : typeof v === 'object' ? JSON.stringify(v) : String(v);
4253
+ if (bare && !Array.isArray(v))
4254
+ return text;
4255
+ return `"${text.replace(/"/g, '""')}"`;
4256
+ }
4257
+ /** An output array's ClickHouse text form: `[1,2]`, `['a','b']`. */
4258
+ function outputArrayText(arr, elementType) {
4259
+ const quote = !(isNumberType(elementType) || /^(Bool|Boolean)$/.test(typeHead(elementType)));
4260
+ return `[${arr.map((x) => (x === null || x === undefined ? 'NULL' : Array.isArray(x) ? outputArrayText(x, arrayElementType(elementType)) : quote ? `'${String(x).replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'` : String(x))).join(',')}]`;
4261
+ }
4262
+ /**
4263
+ * Render a result in one of the modeled text formats (`.json` is handled by the caller):
4264
+ * ClickHouse's JSONEachRow and CSV / CSVWithNames, every row (and the header) ending in `\n`.
4265
+ */
4266
+ export function renderFormat(format, result) {
4267
+ if (format === 'ndjson')
4268
+ return result.data.map((r) => `${stringifyJson(r)}\n`).join('');
4269
+ const lines = result.data.map((r) => result.meta.map((m) => csvCell(r[m.name], m.type)).join(','));
4270
+ if (format === 'csvwithnames')
4271
+ lines.unshift(result.meta.map((m) => `"${m.name.replace(/"/g, '""')}"`).join(','));
4272
+ return lines.map((l) => `${l}\n`).join('');
4273
+ }
4274
+ /** Parse a SELECT and apply the checks ClickHouse makes before reading any data. */
4275
+ export function parseQuery(sql) {
4276
+ const q = parseSql(sql);
4277
+ // A function this engine does not implement is refused by name before any other check.
4278
+ const visitExpr = (e) => {
4279
+ if (e.k === 'fn' && !isKnownFunction(e.name))
4280
+ fail(`Unknown function ${e.name}`);
4281
+ if (e.k === 'subquery') {
4282
+ e.q.selects.forEach(visitSelect);
4283
+ return;
4284
+ }
4285
+ if (e.k === 'fn')
4286
+ e.params?.forEach(visitExpr);
4287
+ forEachChild(e, visitExpr);
4288
+ };
4289
+ const visitSelect = (sel) => {
4290
+ for (const w of sel.with) {
4291
+ if (w.expr)
4292
+ visitExpr(w.expr);
4293
+ if (w.query)
4294
+ w.query.selects.forEach(visitSelect);
4295
+ }
4296
+ for (const f of sel.from) {
4297
+ if (f.source.kind === 'sub')
4298
+ f.source.query.selects.forEach(visitSelect);
4299
+ if (f.join?.on)
4300
+ visitExpr(f.join.on);
4301
+ }
4302
+ [...sel.items, ...sel.groupBy, ...sel.orderBy.map((o) => o.e)].forEach(visitExpr);
4303
+ [sel.prewhere, sel.where, sel.having, sel.limit, sel.offset].forEach((x) => { if (x)
4304
+ visitExpr(x); });
4305
+ };
4306
+ q.selects.forEach(visitSelect);
4307
+ for (const sel of q.selects) {
4308
+ const aliases = new Map();
4309
+ sel.items.forEach((i) => collectAliases(i, aliases));
4310
+ const aggregate = sel.groupBy.length > 0 || sel.items.some((i) => containsAggregate(i, aliases));
4311
+ if (!aggregate)
4312
+ continue;
4313
+ const scope = { sources: [], usingKeys: new Set(), aliases, withExprs: new Map(), withCache: new Map(), exec: undefined, subCache: new Map() };
4314
+ for (const w of sel.with)
4315
+ if (w.expr)
4316
+ scope.withExprs.set(w.name, w.expr);
4317
+ checkGroupKeys(sel, aliases);
4318
+ for (const it of sel.items)
4319
+ if (it.k !== 'star')
4320
+ checkAggregateProjection(it, sel.groupBy, scope);
4321
+ checkAggregateOrder(sel, scope);
4322
+ }
4323
+ return q;
4324
+ }
4325
+ /** Run a query over ONE table's stored rows (any FROM name resolves to it). */
4326
+ export function executeQuery(q, columns, rows, nowMs = 0) {
4327
+ const rel = { columns: [...columns], rows: rows.map((r) => columns.map((c) => fromStored(r[c.name], c.type))) };
4328
+ return executeSql(q, { table: () => rel }, nowMs);
4329
+ }