@volter/world-core 2.0.37 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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,697 @@
1
+ // ClickHouse® VALUES as the kernel's ClickHouse engine carries them (Tinybird's pipes, PostHog's HogQL): the runtime value classes
2
+ // (DateTime, Date, Interval, aggregate states), time-zone arithmetic, the type grammar's defaults,
3
+ // ordering/equality, and the conversions between an engine value, its stored JSON form and its
4
+ // JSON-output form.
5
+ //
6
+ // Semantics transcribed from ClickHouse's documentation:
7
+ // • DateTime / DateTime64 are an instant plus a display time zone; the server zone (Tinybird's)
8
+ // is UTC. Calendar functions (`toStartOfDay(t, tz)`, `+ INTERVAL n DAY`, `dateDiff('month', …)`)
9
+ // work on the wall clock of the zone; `toStartOfMonth` returns a `Date`.
10
+ // • JSON output renders DateTime as `YYYY-MM-DD hh:mm:ss` and DateTime64(n) with n fractional
11
+ // digits, in the value's zone; Date as `YYYY-MM-DD`.
12
+ // • A non-Nullable column's default (what `join_use_nulls = 0` fills for a non-matching side and
13
+ // what an un-grouped aggregate over no rows answers) is 0 / '' / epoch / [].
14
+
15
+ /**
16
+ * A DateTime or DateTime64 value: an instant held as exact integer ticks — whole milliseconds since
17
+ * the epoch (`ms`) plus the nanoseconds inside that millisecond (`ns`, 0…999 999) — its scale (the
18
+ * `p` of `DateTime64(p)`, 0 for DateTime) and a display zone. ClickHouse stores DateTime64(p) as an
19
+ * Int64 count of 10^-p second ticks, so every value is truncated to its scale on construction
20
+ * (`makeDT`) and compares exactly.
21
+ */
22
+ export class DT {
23
+ constructor(readonly ms: number, readonly scale: number, readonly tz?: string, readonly ns = 0) {}
24
+ withTz(tz: string | undefined, scale = this.scale): DT {
25
+ return makeDT(this.ms, this.ns, scale, tz);
26
+ }
27
+ }
28
+
29
+ /** A DT truncated to `scale` digits (ClickHouse's DateTime64 tick), with `ns` normalised into `ms`. */
30
+ export function makeDT(ms: number, ns: number, scale: number, tz?: string): DT {
31
+ let m = Math.floor(ms);
32
+ let n = Math.round(ns + (ms - m) * 1e6);
33
+ if (n >= 1e6 || n < 0) { const carry = Math.floor(n / 1e6); m += carry; n -= carry * 1e6; }
34
+ if (scale >= 9) return new DT(m, scale, tz, n);
35
+ if (scale >= 3) { const step = 10 ** (9 - scale); return new DT(m, scale, tz, n - (n % step)); }
36
+ const step = 10 ** (3 - scale);
37
+ return new DT(Math.floor(m / step) * step, scale, tz, 0);
38
+ }
39
+
40
+ /** Two instants' order, exactly (ms first, then the sub-millisecond nanoseconds). */
41
+ export function compareDT(a: DT, b: DT): number {
42
+ if (a.ms !== b.ms) return a.ms < b.ms ? -1 : 1;
43
+ return a.ns === b.ns ? 0 : a.ns < b.ns ? -1 : 1;
44
+ }
45
+
46
+ // ── exact integers ──────────────────────────────────────────────────────────────────────────
47
+ // ClickHouse's Int64/UInt64/Int128/… are exact. An engine integer is a JS number while it is a
48
+ // safe integer and a bigint beyond ±2^53 (one representation per value, so keys and equality
49
+ // agree); stored JSON carries such a value as its decimal string.
50
+
51
+ /** A bigint as the engine carries it: a number when safe, else the bigint. */
52
+ export function normInt(v: bigint): number | bigint {
53
+ return v >= -9007199254740991n && v <= 9007199254740991n ? Number(v) : v;
54
+ }
55
+ const INT_WIDTH: Record<string, { bits: bigint; signed: boolean }> = {
56
+ Int8: { bits: 8n, signed: true }, Int16: { bits: 16n, signed: true }, Int32: { bits: 32n, signed: true },
57
+ Int64: { bits: 64n, signed: true }, Int128: { bits: 128n, signed: true }, Int256: { bits: 256n, signed: true },
58
+ UInt8: { bits: 8n, signed: false }, UInt16: { bits: 16n, signed: false }, UInt32: { bits: 32n, signed: false },
59
+ UInt64: { bits: 64n, signed: false }, UInt128: { bits: 128n, signed: false }, UInt256: { bits: 256n, signed: false },
60
+ };
61
+ /** Wrap an integer into a sized ClickHouse integer type (two's complement, as CAST/toIntN do). */
62
+ export function wrapInt(v: bigint, type: string): number | bigint {
63
+ const w = INT_WIDTH[typeHead(type)];
64
+ if (!w) return normInt(v);
65
+ const mod = 1n << w.bits;
66
+ let r = ((v % mod) + mod) % mod;
67
+ if (w.signed && r >= mod >> 1n) r -= mod;
68
+ return normInt(r);
69
+ }
70
+ /** An engine value as an exact integer (a float truncates toward zero, as ClickHouse's cast does). */
71
+ export function toBigInt(v: unknown, type = 'Int64'): bigint {
72
+ if (typeof v === 'bigint') return v;
73
+ if (typeof v === 'boolean') return v ? 1n : 0n;
74
+ if (typeof v === 'number') return Number.isFinite(v) ? BigInt(Math.trunc(v)) : 0n;
75
+ if (v instanceof DT) return BigInt(Math.floor(v.ms / 1000));
76
+ if (v instanceof DDate) return BigInt(v.days);
77
+ if (typeof v === 'string') {
78
+ if (/^[+-]?\d+$/.test(v)) return BigInt(v);
79
+ throw new ClickHouseError(`Cannot parse string '${v}' as ${baseType(type).base}: syntax error`);
80
+ }
81
+ throw new ClickHouseError(`Cannot convert ${valueToText(v)} to ${baseType(type).base}`);
82
+ }
83
+ /** An integer as stored JSON: a number when safe, its decimal string otherwise. */
84
+ function storedInt(v: number | bigint): number | string {
85
+ return typeof v === 'bigint' ? v.toString() : v;
86
+ }
87
+
88
+ /** Is this an integer type of 64 bits or more (the ones `output_format_json_quote_64bit_integers` quotes)? */
89
+ export function isWideIntegerType(type: string): boolean {
90
+ return /^U?Int(64|128|256)$/.test(typeHead(type));
91
+ }
92
+
93
+ const BIG_MARK = '\u0000tb-bigint:';
94
+ /** JSON.stringify that writes a bigint as a bare JSON number (ClickHouse's unquoted 64-bit output). */
95
+ export function stringifyJson(value: unknown): string {
96
+ const text = JSON.stringify(value, (_k, v) => (typeof v === 'bigint' ? `${BIG_MARK}${v.toString()}` : v));
97
+ return text === undefined ? text as unknown as string : text.replace(/"\\u0000tb-bigint:(-?\d+)"/g, '$1');
98
+ }
99
+
100
+ /** A Date value: a calendar day (days since 1970-01-01), no zone. */
101
+ export class DDate {
102
+ constructor(readonly days: number) {}
103
+ }
104
+
105
+ export type IntervalUnit = 'millisecond' | 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year';
106
+ export class Interval {
107
+ constructor(readonly unit: IntervalUnit, readonly n: number) {}
108
+ }
109
+
110
+ /** A `Map(K, V)` value: its entries in the order they were written (a ClickHouse Map keeps its entries as an array of
111
+ * pairs, https://clickhouse.com/docs/sql-reference/data-types/map). Stored as a JSON object of the entries. */
112
+ export class ChMap {
113
+ constructor(readonly entries: Array<[unknown, unknown]>) {}
114
+ }
115
+
116
+ /** An aggregate function's intermediate state (`minState(x)` → `AggregateFunction(min, T)`). */
117
+ export class AggState {
118
+ constructor(readonly fn: string, readonly value: unknown) {}
119
+ }
120
+
121
+ export const SERVER_TZ = 'UTC';
122
+
123
+ // ── time zones ──────────────────────────────────────────────────────────────────────────────
124
+
125
+ const FORMATTERS = new Map<string, Intl.DateTimeFormat>();
126
+ function formatter(tz: string): Intl.DateTimeFormat {
127
+ let f = FORMATTERS.get(tz);
128
+ if (!f) {
129
+ try {
130
+ f = new Intl.DateTimeFormat('en-US', {
131
+ timeZone: tz, hourCycle: 'h23', year: 'numeric', month: '2-digit', day: '2-digit',
132
+ hour: '2-digit', minute: '2-digit', second: '2-digit',
133
+ });
134
+ } catch {
135
+ throw new ClickHouseError(`Invalid time zone: ${tz}`);
136
+ }
137
+ FORMATTERS.set(tz, f);
138
+ }
139
+ return f;
140
+ }
141
+
142
+ /** A query the twin refuses, carrying the status + message the vendor answers with. */
143
+ export class ClickHouseError extends Error {
144
+ readonly status: number;
145
+ constructor(message: string, status = 400) {
146
+ super(message);
147
+ this.name = 'ClickHouseError';
148
+ this.status = status;
149
+ }
150
+ }
151
+ export { ClickHouseError as EngineError };
152
+
153
+ export type Wall = { y: number; mo: number; d: number; h: number; mi: number; s: number; ms: number };
154
+
155
+ /** The wall-clock fields of an instant in a zone. */
156
+ export function wallOf(ms: number, tz = SERVER_TZ): Wall {
157
+ const floorSec = Math.floor(ms / 1000);
158
+ const frac = ms - floorSec * 1000;
159
+ if (tz === 'UTC' || tz === 'Etc/UTC' || tz === 'GMT') {
160
+ const d = new Date(floorSec * 1000);
161
+ return { y: d.getUTCFullYear(), mo: d.getUTCMonth() + 1, d: d.getUTCDate(), h: d.getUTCHours(), mi: d.getUTCMinutes(), s: d.getUTCSeconds(), ms: frac };
162
+ }
163
+ const parts = formatter(tz).formatToParts(new Date(floorSec * 1000));
164
+ const get = (t: string) => Number(parts.find((p) => p.type === t)?.value ?? '0');
165
+ return { y: get('year'), mo: get('month'), d: get('day'), h: get('hour'), mi: get('minute'), s: get('second'), ms: frac };
166
+ }
167
+
168
+ /** The zone's UTC offset at an instant, in ms (local − UTC). */
169
+ export function offsetMs(ms: number, tz = SERVER_TZ): number {
170
+ const w = wallOf(ms, tz);
171
+ const asUtc = Date.UTC(w.y, w.mo - 1, w.d, w.h, w.mi, w.s, w.ms);
172
+ return asUtc - ms;
173
+ }
174
+
175
+ /**
176
+ * The instant a wall-clock time names in a zone. ClickHouse's DateLUT (`makeDateTime`) adds the
177
+ * time of day to the day's start and, from the offset-change moment on, subtracts the change: a
178
+ * wall time inside a spring-forward gap therefore lands BEFORE the gap (`2024-03-10 02:30:00` in
179
+ * America/New_York reads back as `01:30:00` — clickhouse-local 26.10 answers the same), and a
180
+ * repeated fall-back hour resolves to its first occurrence. Two offset probes reproduce both.
181
+ */
182
+ export function instantOf(w: Wall, tz = SERVER_TZ): number {
183
+ const local = Date.UTC(w.y, w.mo - 1, w.d, w.h, w.mi, w.s, w.ms);
184
+ let guess = local - offsetMs(local, tz);
185
+ guess = local - offsetMs(guess, tz);
186
+ return guess;
187
+ }
188
+
189
+ export function daysFromCivil(y: number, mo: number, d: number): number {
190
+ return Math.floor(Date.UTC(y, mo - 1, d) / 86_400_000);
191
+ }
192
+ export function civilFromDays(days: number): { y: number; mo: number; d: number } {
193
+ const dt = new Date(days * 86_400_000);
194
+ return { y: dt.getUTCFullYear(), mo: dt.getUTCMonth() + 1, d: dt.getUTCDate() };
195
+ }
196
+
197
+ // ── parsing ─────────────────────────────────────────────────────────────────────────────────
198
+
199
+ const DT_TEXT = /^\s*(\d{4})-(\d{1,2})-(\d{1,2})(?:[ T](\d{1,2}):(\d{2})(?::(\d{2})(?:\.(\d{1,9}))?)?)?\s*(Z|[+-]\d{2}(?::?\d{2})?)?\s*$/i;
200
+
201
+ /** Parse ClickHouse datetime text (and the ISO forms Tinybird's ingest accepts) in a zone, exactly:
202
+ * whole milliseconds plus the nanoseconds a DateTime64(4…9) fraction carries. */
203
+ export function parseDateTimeInstant(text: string, tz = SERVER_TZ): { ms: number; ns: number } | undefined {
204
+ const m = DT_TEXT.exec(text);
205
+ if (!m) {
206
+ if (/^\s*\d{9,11}(\.\d+)?\s*$/.test(text)) {
207
+ const [whole = '0', frac = ''] = text.trim().split('.');
208
+ const f9 = frac.padEnd(9, '0').slice(0, 9);
209
+ return { ms: Number(whole) * 1000 + Number(f9.slice(0, 3)), ns: Number(f9.slice(3)) };
210
+ }
211
+ return undefined;
212
+ }
213
+ const f9 = (m[7] ?? '').padEnd(9, '0');
214
+ const ns = Number(f9.slice(3));
215
+ const w: Wall = { y: Number(m[1]), mo: Number(m[2]), d: Number(m[3]), h: Number(m[4] ?? 0), mi: Number(m[5] ?? 0), s: Number(m[6] ?? 0), ms: Number(f9.slice(0, 3)) };
216
+ if (m[8]) {
217
+ const zone = m[8].toUpperCase();
218
+ const base = Date.UTC(w.y, w.mo - 1, w.d, w.h, w.mi, w.s, w.ms);
219
+ if (zone === 'Z') return { ms: base, ns };
220
+ const zm = /^([+-])(\d{2}):?(\d{2})?$/.exec(zone)!;
221
+ const off = (Number(zm[2]) * 60 + Number(zm[3] ?? 0)) * 60_000 * (zm[1] === '-' ? -1 : 1);
222
+ return { ms: base - off, ns };
223
+ }
224
+ return { ms: instantOf(w, tz), ns };
225
+ }
226
+
227
+ export function parseDateText(text: string): number | undefined {
228
+ const m = /^\s*(\d{4})-(\d{1,2})-(\d{1,2})/.exec(text);
229
+ if (!m) return undefined;
230
+ return daysFromCivil(Number(m[1]), Number(m[2]), Number(m[3]));
231
+ }
232
+
233
+ // ── formatting ──────────────────────────────────────────────────────────────────────────────
234
+
235
+ const pad = (n: number, w = 2) => String(Math.trunc(Math.abs(n))).padStart(w, '0');
236
+
237
+ export function formatDate(d: DDate): string {
238
+ const c = civilFromDays(d.days);
239
+ return `${pad(c.y, 4)}-${pad(c.mo)}-${pad(c.d)}`;
240
+ }
241
+
242
+ export function formatDT(v: DT): string {
243
+ const w = wallOf(v.ms, v.tz);
244
+ const base = `${pad(w.y, 4)}-${pad(w.mo)}-${pad(w.d)} ${pad(w.h)}:${pad(w.mi)}:${pad(w.s)}`;
245
+ if (v.scale <= 0) return base;
246
+ const frac = `${String(Math.floor(w.ms)).padStart(3, '0')}${String(v.ns).padStart(6, '0')}`.padEnd(v.scale, '0').slice(0, v.scale);
247
+ return `${base}.${frac}`;
248
+ }
249
+
250
+ /** A DT as a Data Source stores it: wall-clock text in the column's zone, with the zone's offset
251
+ * when that zone is not UTC (so an instant inside a repeated DST hour survives a round trip). */
252
+ export function formatStoredDT(v: DT): string {
253
+ const text = formatDT(v);
254
+ const zone = v.tz ?? SERVER_TZ;
255
+ if (zone === 'UTC' || zone === 'Etc/UTC' || zone === 'GMT') return text;
256
+ const off = offsetMs(v.ms, zone) / 60_000;
257
+ return `${text}${off < 0 ? '-' : '+'}${pad(Math.floor(Math.abs(off) / 60))}:${pad(Math.abs(off) % 60)}`;
258
+ }
259
+
260
+ /** `formatDateTime(t, fmt)` — ClickHouse's MySQL-style specifiers. */
261
+ export function formatDateTimePattern(v: DT | DDate, fmt: string, tz?: string): string {
262
+ const dt = v instanceof DDate ? new DT(instantOf({ ...civilFromDays(v.days), h: 0, mi: 0, s: 0, ms: 0 }, tz ?? SERVER_TZ), 0, tz ?? SERVER_TZ) : v;
263
+ const zone = tz ?? dt.tz ?? SERVER_TZ;
264
+ const w = wallOf(dt.ms, zone);
265
+ const off = offsetMs(dt.ms, zone) / 60_000;
266
+ const days = daysFromCivil(w.y, w.mo, w.d);
267
+ const dow = ((days % 7) + 7 + 4) % 7; // 1970-01-01 was a Thursday
268
+ const doy = days - daysFromCivil(w.y, 1, 1) + 1;
269
+ const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
270
+ const DAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
271
+ let out = '';
272
+ for (let i = 0; i < fmt.length; i++) {
273
+ const ch = fmt[i]!;
274
+ if (ch !== '%' || i + 1 >= fmt.length) { out += ch; continue; }
275
+ const f = fmt[++i]!;
276
+ switch (f) {
277
+ case 'Y': out += pad(w.y, 4); break;
278
+ case 'y': out += pad(w.y % 100); break;
279
+ case 'C': out += pad(Math.floor(w.y / 100)); break;
280
+ case 'm': out += pad(w.mo); break;
281
+ case 'd': out += pad(w.d); break;
282
+ case 'e': out += String(w.d).padStart(2, ' '); break;
283
+ case 'F': out += `${pad(w.y, 4)}-${pad(w.mo)}-${pad(w.d)}`; break;
284
+ case 'D': out += `${pad(w.mo)}/${pad(w.d)}/${pad(w.y % 100)}`; break;
285
+ case 'H': out += pad(w.h); break;
286
+ case 'I': case 'h': out += pad(((w.h + 11) % 12) + 1); break;
287
+ case 'i': out += pad(w.mi); break;
288
+ case 'M': out += MONTHS[w.mo - 1]; break;
289
+ case 'S': case 's': out += pad(w.s); break;
290
+ case 'T': out += `${pad(w.h)}:${pad(w.mi)}:${pad(w.s)}`; break;
291
+ case 'R': out += `${pad(w.h)}:${pad(w.mi)}`; break;
292
+ case 'p': out += w.h < 12 ? 'AM' : 'PM'; break;
293
+ case 'j': out += pad(doy, 3); break;
294
+ case 'u': out += String(dow === 0 ? 7 : dow); break;
295
+ case 'w': out += String(dow); break;
296
+ case 'a': out += DAYS[dow]!.slice(0, 3); break;
297
+ case 'b': out += MONTHS[w.mo - 1]!.slice(0, 3); break;
298
+ case 'f': out += String(Math.floor(w.ms)).padStart(3, '0').padEnd(6, '0'); break;
299
+ case 'z': out += `${off < 0 ? '-' : '+'}${pad(Math.floor(Math.abs(off) / 60))}${pad(Math.abs(off) % 60)}`; break;
300
+ case 'n': out += '\n'; break;
301
+ case 't': out += '\t'; break;
302
+ case '%': out += '%'; break;
303
+ default: throw new ClickHouseError(`formatDateTime: unsupported format specifier %${f}`);
304
+ }
305
+ }
306
+ return out;
307
+ }
308
+
309
+ // ── types ───────────────────────────────────────────────────────────────────────────────────
310
+
311
+ /** Peel `LowCardinality(…)` and `Nullable(…)`. */
312
+ export function baseType(type: string): { base: string; nullable: boolean } {
313
+ let t = type.trim();
314
+ let nullable = false;
315
+ for (let guard = 0; guard < 4; guard++) {
316
+ const low = /^LowCardinality\((.*)\)$/s.exec(t);
317
+ if (low) { t = low[1]!.trim(); continue; }
318
+ const nul = /^Nullable\((.*)\)$/s.exec(t);
319
+ if (nul) { nullable = true; t = nul[1]!.trim(); continue; }
320
+ break;
321
+ }
322
+ return { base: t, nullable };
323
+ }
324
+
325
+ export function typeHead(type: string): string {
326
+ return baseType(type).base.replace(/\(.*$/s, '').trim();
327
+ }
328
+
329
+ /** Split a parameterised type's top-level arguments: `Array(Nullable(String))` → [`Nullable(String)`]. */
330
+ export function typeArgs(type: string): string[] {
331
+ const { base } = baseType(type);
332
+ const open = base.indexOf('(');
333
+ if (open < 0 || !base.endsWith(')')) return [];
334
+ const inner = base.slice(open + 1, -1);
335
+ const out: string[] = [];
336
+ let depth = 0;
337
+ let cur = '';
338
+ let quote = false;
339
+ for (const ch of inner) {
340
+ if (ch === "'") quote = !quote;
341
+ if (!quote && ch === '(') depth++;
342
+ if (!quote && ch === ')') depth--;
343
+ if (!quote && ch === ',' && depth === 0) { out.push(cur.trim()); cur = ''; continue; }
344
+ cur += ch;
345
+ }
346
+ if (cur.trim()) out.push(cur.trim());
347
+ return out;
348
+ }
349
+
350
+ export function isNullableType(type: string): boolean {
351
+ return baseType(type).nullable;
352
+ }
353
+
354
+ const INTS = new Set(['Int8', 'Int16', 'Int32', 'Int64', 'Int128', 'Int256']);
355
+ const UINTS = new Set(['UInt8', 'UInt16', 'UInt32', 'UInt64', 'UInt128', 'UInt256', 'Bool', 'Boolean']);
356
+ const FLOATS = new Set(['Float32', 'Float64']);
357
+
358
+ export function isIntegerType(type: string): boolean {
359
+ const h = typeHead(type);
360
+ return INTS.has(h) || UINTS.has(h);
361
+ }
362
+ export function isUnsignedType(type: string): boolean {
363
+ return UINTS.has(typeHead(type));
364
+ }
365
+ export function isFloatType(type: string): boolean {
366
+ const h = typeHead(type);
367
+ return FLOATS.has(h) || h.startsWith('Decimal');
368
+ }
369
+ export function isNumberType(type: string): boolean {
370
+ return isIntegerType(type) || isFloatType(type);
371
+ }
372
+
373
+ /** The zone a `DateTime('tz')` / `DateTime64(n, 'tz')` type names, if any. */
374
+ export function typeZone(type: string): string | undefined {
375
+ const args = typeArgs(type);
376
+ const zone = args.find((a) => /^'.*'$/.test(a));
377
+ return zone ? zone.slice(1, -1) : undefined;
378
+ }
379
+ export function typeScale(type: string): number {
380
+ const h = typeHead(type);
381
+ if (h === 'DateTime64') {
382
+ const first = typeArgs(type)[0];
383
+ return first !== undefined && /^\d+$/.test(first) ? Number(first) : 3;
384
+ }
385
+ return 0;
386
+ }
387
+
388
+ /** A non-Nullable column's default value. */
389
+ export function defaultOf(type: string): unknown {
390
+ const { base, nullable } = baseType(type);
391
+ if (nullable) return null;
392
+ const h = base.replace(/\(.*$/s, '').trim();
393
+ if (INTS.has(h) || UINTS.has(h) || FLOATS.has(h) || h.startsWith('Decimal')) return h === 'Bool' || h === 'Boolean' ? false : 0;
394
+ if (h === 'String' || h === 'FixedString' || h === 'UUID' || h === 'IPv4' || h === 'IPv6' || h.startsWith('Enum')) {
395
+ if (h === 'UUID') return '00000000-0000-0000-0000-000000000000';
396
+ return '';
397
+ }
398
+ if (h === 'DateTime' || h === 'DateTime64') return new DT(0, typeScale(base), typeZone(base));
399
+ if (h === 'Date' || h === 'Date32') return new DDate(0);
400
+ if (h === 'Array') return [];
401
+ if (h === 'Map') return new ChMap([]);
402
+ if (h === 'AggregateFunction' || h === 'SimpleAggregateFunction') return null;
403
+ if (h === 'Nothing') return null;
404
+ return '';
405
+ }
406
+
407
+ // ── aggregate states ────────────────────────────────────────────────────────────────────────
408
+ // An `AggregateFunction(fn, T…)` cell holds what `fnMerge` needs to finish the aggregate over any
409
+ // union of states: the running value for min/max/any/anyLast/sum/count, [sum, count] for avg, the
410
+ // distinct values for uniq*/groupUniqArray, every value for groupArray/quantile*/median*/topK and
411
+ // [arg, key] for argMin/argMax. Stored JSON is `{"$agg": fn, "value": …}` with T's stored form.
412
+
413
+ export type AggKind = 'scalar' | 'sum' | 'avg' | 'set' | 'list' | 'arg';
414
+ export function aggKind(fn: string): AggKind | undefined {
415
+ const base = fn.replace(/\(.*$/s, '').toLowerCase();
416
+ if (['min', 'max', 'any', 'anylast'].includes(base)) return 'scalar';
417
+ if (base === 'sum' || base === 'count') return 'sum';
418
+ if (base === 'avg') return 'avg';
419
+ if (['uniq', 'uniqexact', 'uniqcombined', 'uniqcombined64', 'uniqhll12', 'groupuniqarray', 'countdistinct'].includes(base)) return 'set';
420
+ if (base === 'grouparray' || base === 'grouparraysample' || base === 'topk' || base.startsWith('quantile') || base.startsWith('median')) return 'list';
421
+ if (base === 'argmin' || base === 'argmax') return 'arg';
422
+ return undefined;
423
+ }
424
+
425
+ function stateFromStored(v: { $agg: string; value: unknown }, base: string): AggState {
426
+ const args = typeArgs(base);
427
+ const t1 = args[1] ?? 'String';
428
+ const t2 = args[2] ?? 'String';
429
+ const kind = aggKind(v.$agg);
430
+ const raw = v.value;
431
+ if (raw === null || raw === undefined) return new AggState(v.$agg, null);
432
+ switch (kind) {
433
+ case 'sum': return new AggState(v.$agg, typeof raw === 'string' ? normInt(BigInt(raw)) : raw);
434
+ case 'avg': return new AggState(v.$agg, Array.isArray(raw) ? [Number(raw[0]), Number(raw[1])] : null);
435
+ case 'set': case 'list': return new AggState(v.$agg, Array.isArray(raw) ? raw.map((x) => fromStored(x, t1)) : []);
436
+ case 'arg': return new AggState(v.$agg, Array.isArray(raw) ? [fromStored(raw[0], t1), fromStored(raw[1], t2)] : null);
437
+ default: return new AggState(v.$agg, fromStored(raw, t1));
438
+ }
439
+ }
440
+ function stateToStored(s: AggState, base: string): unknown {
441
+ const args = typeArgs(base);
442
+ const t1 = args[1] ?? 'String';
443
+ const t2 = args[2] ?? 'String';
444
+ const v = s.value;
445
+ if (v === null || v === undefined) return { $agg: s.fn, value: null };
446
+ switch (aggKind(s.fn)) {
447
+ case 'sum': return { $agg: s.fn, value: typeof v === 'bigint' ? v.toString() : v };
448
+ case 'avg': return { $agg: s.fn, value: v };
449
+ case 'set': case 'list': return { $agg: s.fn, value: (v as unknown[]).map((x) => toStored(x, t1)) };
450
+ case 'arg': { const [a, k] = v as [unknown, unknown]; return { $agg: s.fn, value: [toStored(a, t1), toStored(k, t2)] }; }
451
+ default: return { $agg: s.fn, value: toStored(v, t1) };
452
+ }
453
+ }
454
+
455
+ /** A stored (JSON) cell → the engine value for its column type. */
456
+ export function fromStored(value: unknown, type: string): unknown {
457
+ if (value === undefined) return defaultOf(type);
458
+ if (value === null) return baseType(type).nullable ? null : defaultOf(type);
459
+ const { base } = baseType(type);
460
+ const h = base.replace(/\(.*$/s, '').trim();
461
+ if (h === 'DateTime' || h === 'DateTime64') {
462
+ if (value instanceof DT) return value;
463
+ const scale = typeScale(base);
464
+ const zone = typeZone(base);
465
+ // Text without an explicit offset is wall-clock time in the COLUMN's zone (ClickHouse parses
466
+ // a `DateTime('America/New_York')` value in that zone).
467
+ const at = typeof value === 'number' ? { ms: value * 1000, ns: 0 } : parseDateTimeInstant(String(value), zone ?? SERVER_TZ);
468
+ return makeDT(at?.ms ?? 0, at?.ns ?? 0, scale, zone);
469
+ }
470
+ if (h === 'Date' || h === 'Date32') {
471
+ if (value instanceof DDate) return value;
472
+ return new DDate(parseDateText(String(value)) ?? 0);
473
+ }
474
+ if (h === 'Array') {
475
+ const inner = typeArgs(base)[0] ?? 'String';
476
+ return Array.isArray(value) ? value.map((v) => fromStored(v, inner)) : [];
477
+ }
478
+ if (h === 'Map') {
479
+ if (value instanceof ChMap) return value;
480
+ const [kt, vt] = typeArgs(base);
481
+ const pairs: Array<[unknown, unknown]> = Array.isArray(value) ? (value as Array<[unknown, unknown]>) : typeof value === 'object' ? Object.entries(value as Record<string, unknown>) : [];
482
+ return new ChMap(pairs.map(([k, v]) => [fromStored(k, kt ?? 'String'), fromStored(v, vt ?? 'String')]));
483
+ }
484
+ if (h === 'AggregateFunction') {
485
+ if (value instanceof AggState) return value;
486
+ if (typeof value === 'object' && value !== null && '$agg' in (value as Record<string, unknown>)) {
487
+ return stateFromStored(value as { $agg: string; value: unknown }, base);
488
+ }
489
+ return null;
490
+ }
491
+ if (h === 'Bool' || h === 'Boolean') return value === true || value === 1 || value === 'true';
492
+ if (INTS.has(h) || UINTS.has(h)) {
493
+ if (typeof value === 'number') return value;
494
+ if (typeof value === 'bigint') return normInt(value);
495
+ if (typeof value === 'string' && /^[+-]?\d+$/.test(value.trim())) return normInt(BigInt(value.trim()));
496
+ return Number(value);
497
+ }
498
+ if (FLOATS.has(h)) return typeof value === 'number' ? value : Number(value);
499
+ return value;
500
+ }
501
+
502
+ /**
503
+ * An engine value → the JSON a Data Source stores for its column type (a materialized view's
504
+ * insert). The value is CAST to the column type the way ClickHouse converts an INSERT … SELECT
505
+ * block: integers wrap to the column's width (`CAST(300 AS UInt8)` = 44) and a string that is not
506
+ * an integer is ClickHouse's parse error, which fails the insert.
507
+ */
508
+ export function toStored(value: unknown, type: string): unknown {
509
+ const { base, nullable } = baseType(type);
510
+ const h = base.replace(/\(.*$/s, '').trim();
511
+ if (value === null || value === undefined) return nullable ? null : toStored(defaultOf(type), base);
512
+ if (h === 'DateTime' || h === 'DateTime64') {
513
+ const v = castDateTime(value, typeScale(base), typeZone(base));
514
+ return formatStoredDT(new DT(v.ms, v.scale, typeZone(base) ?? SERVER_TZ, v.ns));
515
+ }
516
+ if (h === 'Date' || h === 'Date32') {
517
+ if (value instanceof DDate) return formatDate(value);
518
+ if (value instanceof DT) { const w = wallOf(value.ms, value.tz); return formatDate(new DDate(daysFromCivil(w.y, w.mo, w.d))); }
519
+ return String(value);
520
+ }
521
+ if (h === 'Array') {
522
+ const inner = typeArgs(base)[0] ?? 'String';
523
+ return Array.isArray(value) ? value.map((v) => toStored(v, inner)) : [];
524
+ }
525
+ if (h === 'Map') {
526
+ const vt = typeArgs(base)[1] ?? 'String';
527
+ return value instanceof ChMap ? Object.fromEntries(value.entries.map(([k, v]) => [valueToText(k), toStored(v, vt)])) : {};
528
+ }
529
+ if (h === 'AggregateFunction') {
530
+ if (value instanceof AggState) return stateToStored(value, base);
531
+ return null;
532
+ }
533
+ if (h === 'Bool' || h === 'Boolean') return value === true || value === 1;
534
+ if (INTS.has(h) || UINTS.has(h)) return storedInt(wrapInt(toBigInt(typeof value === 'string' ? value.trim() : value, base), base));
535
+ if (FLOATS.has(h)) return typeof value === 'number' ? value : Number(value);
536
+ if (h === 'String' || h === 'FixedString') return valueToText(value);
537
+ return value;
538
+ }
539
+
540
+ export function castDateTime(value: unknown, scale: number, tz?: string): DT {
541
+ if (value instanceof DT) return makeDT(value.ms, value.ns, scale, tz ?? value.tz);
542
+ if (value instanceof DDate) {
543
+ const c = civilFromDays(value.days);
544
+ return new DT(instantOf({ ...c, h: 0, mi: 0, s: 0, ms: 0 }, tz ?? SERVER_TZ), scale, tz);
545
+ }
546
+ if (typeof value === 'bigint') return makeDT(Number(value) * 1000, 0, scale, tz);
547
+ if (typeof value === 'number') return makeDT(value * 1000, 0, scale, tz);
548
+ if (typeof value === 'string') {
549
+ const at = parseDateTimeInstant(value, tz ?? SERVER_TZ);
550
+ if (at === undefined) throw new ClickHouseError(`Cannot parse DateTime from String '${value}'`);
551
+ return makeDT(at.ms, at.ns, scale, tz);
552
+ }
553
+ throw new ClickHouseError(`Cannot convert ${typeof value} to DateTime`);
554
+ }
555
+
556
+ /** `toString(x)` — ClickHouse's text form of a value. */
557
+ export function valueToText(value: unknown): string {
558
+ if (value === null || value === undefined) return '';
559
+ if (typeof value === 'string') return value;
560
+ if (typeof value === 'boolean') return value ? 'true' : 'false';
561
+ if (typeof value === 'bigint') return value.toString();
562
+ if (typeof value === 'number') return Number.isFinite(value) ? String(value) : Number.isNaN(value) ? 'nan' : value > 0 ? 'inf' : '-inf';
563
+ if (value instanceof DT) return formatDT(value);
564
+ if (value instanceof DDate) return formatDate(value);
565
+ if (Array.isArray(value)) return `[${value.map(quotedText).join(',')}]`;
566
+ if (value instanceof ChMap) return `{${value.entries.map(([k, v]) => `${quotedText(k)}:${quotedText(v)}`).join(',')}}`;
567
+ if (value instanceof AggState) return '';
568
+ return JSON.stringify(value);
569
+ }
570
+ /** An element inside an array's text form: strings, dates and times are single-quoted and escaped. */
571
+ export function quotedText(v: unknown): string {
572
+ if (v === null || v === undefined) return 'NULL';
573
+ if (typeof v === 'string') return `'${v.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
574
+ if (v instanceof DT || v instanceof DDate) return `'${valueToText(v)}'`;
575
+ return valueToText(v);
576
+ }
577
+
578
+ /** An engine value → the JSON a `.json` response carries (a bigint stays exact). */
579
+ export function toOutput(value: unknown): unknown {
580
+ if (value === null || value === undefined) return null;
581
+ if (value instanceof DT) return formatDT(value);
582
+ if (value instanceof DDate) return formatDate(value);
583
+ if (value instanceof AggState) return '';
584
+ if (value instanceof Interval) return value.n;
585
+ if (Array.isArray(value)) return value.map(toOutput);
586
+ // a Map is a JSON object in a JSON answer (ClickHouse's JSON format writes Map as an object)
587
+ if (value instanceof ChMap) return Object.fromEntries(value.entries.map(([k, v]) => [valueToText(k), toOutput(v)]));
588
+ if (typeof value === 'number' && !Number.isFinite(value)) return null;
589
+ return value;
590
+ }
591
+
592
+ // ── comparison ──────────────────────────────────────────────────────────────────────────────
593
+
594
+ function asInstant(v: unknown): DT | undefined {
595
+ if (v instanceof DT) return v;
596
+ if (v instanceof DDate) return new DT(v.days * 86_400_000, 0);
597
+ return undefined;
598
+ }
599
+
600
+ /**
601
+ * Compare two non-null values ClickHouse-style. A string meeting a DateTime/Date is parsed in the
602
+ * other side's zone (ClickHouse converts the literal to the column's type).
603
+ */
604
+ export function compareValues(a: unknown, b: unknown): number {
605
+ if (a instanceof DT || b instanceof DT || a instanceof DDate || b instanceof DDate) {
606
+ let x = asInstant(a);
607
+ let y = asInstant(b);
608
+ if (x === undefined) x = coerceToInstant(a, b);
609
+ if (y === undefined) y = coerceToInstant(b, a);
610
+ if (a instanceof DDate && b instanceof DT) x = castDateTime(a, 0, b.tz);
611
+ if (b instanceof DDate && a instanceof DT) y = castDateTime(b, 0, a.tz);
612
+ return compareDT(x, y);
613
+ }
614
+ if (typeof a === 'boolean') a = a ? 1 : 0;
615
+ if (typeof b === 'boolean') b = b ? 1 : 0;
616
+ const numA = typeof a === 'number' || typeof a === 'bigint';
617
+ const numB = typeof b === 'number' || typeof b === 'bigint';
618
+ if (numA && numB) {
619
+ if (Number.isNaN(a) || Number.isNaN(b)) return Number.isNaN(a) ? (Number.isNaN(b) ? 0 : 1) : -1;
620
+ return (a as number) < (b as number) ? -1 : (a as number) > (b as number) ? 1 : 0;
621
+ }
622
+ if (numA && typeof b === 'string') { const n = parseNumberText(b); if (n !== undefined) return compareValues(a, n); }
623
+ if (typeof a === 'string' && numB) { const n = parseNumberText(a); if (n !== undefined) return compareValues(n, b); }
624
+ if (a instanceof ChMap && b instanceof ChMap) return compareValues(a.entries.flat(), b.entries.flat());
625
+ if (Array.isArray(a) && Array.isArray(b)) {
626
+ for (let i = 0; i < Math.min(a.length, b.length); i++) {
627
+ const c = a[i] === null || b[i] === null ? (a[i] === null ? (b[i] === null ? 0 : 1) : -1) : compareValues(a[i], b[i]);
628
+ if (c !== 0) return c;
629
+ }
630
+ return a.length - b.length;
631
+ }
632
+ const sa = typeof a === 'string' ? a : valueToText(a);
633
+ const sb = typeof b === 'string' ? b : valueToText(b);
634
+ // ClickHouse compares strings bytewise.
635
+ const ba = Buffer.from(sa, 'utf8');
636
+ const bb = Buffer.from(sb, 'utf8');
637
+ return Buffer.compare(ba, bb);
638
+ }
639
+ /** Numeric text as an exact engine number (an integer beyond 2^53 as a bigint). */
640
+ function parseNumberText(s: string): number | bigint | undefined {
641
+ const t = s.trim();
642
+ if (t === '') return undefined;
643
+ if (/^[+-]?\d+$/.test(t)) return normInt(BigInt(t));
644
+ const n = Number(t);
645
+ return Number.isNaN(n) ? undefined : n;
646
+ }
647
+
648
+ function coerceToInstant(v: unknown, other: unknown): DT {
649
+ const tz = other instanceof DT ? other.tz : undefined;
650
+ if (typeof v === 'string') {
651
+ if (other instanceof DDate) {
652
+ const d = parseDateText(v);
653
+ if (d === undefined) throw new ClickHouseError(`Cannot parse Date from String '${v}'`);
654
+ return new DT(d * 86_400_000, 0);
655
+ }
656
+ const at = parseDateTimeInstant(v, tz ?? SERVER_TZ);
657
+ if (at === undefined) throw new ClickHouseError(`Cannot parse DateTime from String '${v}'`);
658
+ return new DT(at.ms, 9, tz, at.ns);
659
+ }
660
+ if (typeof v === 'number' || typeof v === 'bigint') return other instanceof DDate ? new DT(Number(v) * 86_400_000, 0) : makeDT(Number(v) * 1000, 0, 9);
661
+ throw new ClickHouseError(`Cannot compare ${valueToText(v)} with a DateTime`);
662
+ }
663
+
664
+ export function valuesEqual(a: unknown, b: unknown): boolean {
665
+ return compareValues(a, b) === 0;
666
+ }
667
+
668
+ /** A hash key under which equal values collide (for GROUP BY, JOIN, IN, uniq). */
669
+ export function valueKey(v: unknown): string {
670
+ if (v === null || v === undefined) return 'N';
671
+ if (typeof v === 'boolean') return `n:${v ? 1 : 0}`;
672
+ if (typeof v === 'number' || typeof v === 'bigint') return `n:${v}`;
673
+ if (typeof v === 'string') return `s:${v}`;
674
+ if (v instanceof DT) return v.ns ? `t:${v.ms}.${v.ns}` : `t:${v.ms}`;
675
+ if (v instanceof DDate) return `t:${v.days * 86_400_000}`;
676
+ if (Array.isArray(v)) return `a:[${v.map(valueKey).join(',')}]`;
677
+ if (v instanceof ChMap) return `m:{${v.entries.map(([k, x]) => `${valueKey(k)}=${valueKey(x)}`).join(',')}}`;
678
+ if (v instanceof AggState) return `g:${v.fn}:${valueKey(v.value)}`;
679
+ return `o:${JSON.stringify(v)}`;
680
+ }
681
+
682
+ /** A string literal meeting a DateTime in a hash key must hash as the instant. */
683
+ export function valueKeyAs(v: unknown, like: unknown): string {
684
+ if (typeof v === 'string' && (like instanceof DT || like instanceof DDate)) {
685
+ try {
686
+ const at = coerceToInstant(v, like);
687
+ return at.ns ? `t:${at.ms}.${at.ns}` : `t:${at.ms}`;
688
+ } catch {
689
+ return valueKey(v);
690
+ }
691
+ }
692
+ if (typeof v === 'string' && (typeof like === 'number' || typeof like === 'bigint')) {
693
+ const n = parseNumberText(v);
694
+ if (n !== undefined) return valueKey(n);
695
+ }
696
+ return valueKey(v);
697
+ }