@wavehouse/chtypes 0.1.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 (72) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +202 -0
  3. package/README.md +442 -0
  4. package/dist/cli.d.ts +43 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +303 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/discover.d.ts +134 -0
  9. package/dist/discover.d.ts.map +1 -0
  10. package/dist/discover.js +291 -0
  11. package/dist/discover.js.map +1 -0
  12. package/dist/errors.d.ts +204 -0
  13. package/dist/errors.d.ts.map +1 -0
  14. package/dist/errors.js +228 -0
  15. package/dist/errors.js.map +1 -0
  16. package/dist/fetch.d.ts +235 -0
  17. package/dist/fetch.d.ts.map +1 -0
  18. package/dist/fetch.js +842 -0
  19. package/dist/fetch.js.map +1 -0
  20. package/dist/ffi.d.ts +346 -0
  21. package/dist/ffi.d.ts.map +1 -0
  22. package/dist/ffi.js +913 -0
  23. package/dist/ffi.js.map +1 -0
  24. package/dist/format.d.ts +104 -0
  25. package/dist/format.d.ts.map +1 -0
  26. package/dist/format.js +108 -0
  27. package/dist/format.js.map +1 -0
  28. package/dist/index.d.ts +28 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +28 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/json.d.ts +127 -0
  33. package/dist/json.d.ts.map +1 -0
  34. package/dist/json.js +577 -0
  35. package/dist/json.js.map +1 -0
  36. package/dist/library.d.ts +225 -0
  37. package/dist/library.d.ts.map +1 -0
  38. package/dist/library.js +234 -0
  39. package/dist/library.js.map +1 -0
  40. package/dist/numeric.d.ts +20 -0
  41. package/dist/numeric.d.ts.map +1 -0
  42. package/dist/numeric.js +107 -0
  43. package/dist/numeric.js.map +1 -0
  44. package/dist/paths.d.ts +47 -0
  45. package/dist/paths.d.ts.map +1 -0
  46. package/dist/paths.js +78 -0
  47. package/dist/paths.js.map +1 -0
  48. package/dist/registry.d.ts +204 -0
  49. package/dist/registry.d.ts.map +1 -0
  50. package/dist/registry.js +409 -0
  51. package/dist/registry.js.map +1 -0
  52. package/dist/results.d.ts +326 -0
  53. package/dist/results.d.ts.map +1 -0
  54. package/dist/results.js +235 -0
  55. package/dist/results.js.map +1 -0
  56. package/dist/schema.d.ts +444 -0
  57. package/dist/schema.d.ts.map +1 -0
  58. package/dist/schema.js +448 -0
  59. package/dist/schema.js.map +1 -0
  60. package/dist/settings.d.ts +43 -0
  61. package/dist/settings.d.ts.map +1 -0
  62. package/dist/settings.js +33 -0
  63. package/dist/settings.js.map +1 -0
  64. package/dist/tar.d.ts +32 -0
  65. package/dist/tar.d.ts.map +1 -0
  66. package/dist/tar.js +310 -0
  67. package/dist/tar.js.map +1 -0
  68. package/dist/transform.d.ts +73 -0
  69. package/dist/transform.d.ts.map +1 -0
  70. package/dist/transform.js +527 -0
  71. package/dist/transform.js.map +1 -0
  72. package/package.json +62 -0
package/dist/ffi.js ADDED
@@ -0,0 +1,913 @@
1
+ /**
2
+ * The native layer: one `dlopen`'d chtypes artifact behind the frozen `chs_*` C
3
+ * ABI (spec/c-abi.md). Everything about ownership and lifetime lives here so no
4
+ * caller ever has to remember it.
5
+ *
6
+ * ------------------------------------------------------------------ memory
7
+ * Every `char *` a `chs_*` function returns is malloc'd by the library and MUST
8
+ * be released with **that same library's** `chs_free` — in a multi-version
9
+ * process the frees must never cross. There is exactly one place in this package
10
+ * that turns a returned pointer into a JS string, `NativeLibrary#takeString`,
11
+ * and it frees in a `finally`. Nothing else may touch an owned pointer.
12
+ *
13
+ * Borrowed `const char *` returns (`chs_clickhouse_version`, the column getters)
14
+ * are declared as `DataType.String`, which copies without freeing — correct,
15
+ * because freeing them would be a bug.
16
+ *
17
+ * -------------------------------------------------------------------- FFI
18
+ * This binds through **ffi-rs** (libffi, prebuilt N-API per platform) rather
19
+ * than koffi, and the reason is measured rather than aesthetic:
20
+ *
21
+ * koffi runs every native call on its own preallocated stack — its arm64
22
+ * trampoline does `add sp, x1, #136` before `blr` (see
23
+ * koffi/src/koffi/src/abi_arm64_asm.S). ClickHouse's `checkStackSize()` reads
24
+ * the *pthread* stack bounds and compares them against the current frame, so
25
+ * with koffi every code path that calls it throws TOO_DEEP_RECURSION (306):
26
+ * "Stack size too large. Stack address: 0x16a6dc000, frame address:
27
+ * 0x1065fce10, stack size: 1687024112, maximum stack size: 8388608".
28
+ * Measured on darwin-arm64/25.8: `chs_schema_compile("b Nullable(String)
29
+ * DEFAULT 'x'")` fails under koffi (sync and async, at every
30
+ * `sync_stack_size`) and succeeds under ffi-rs, which calls on the caller's
31
+ * own stack. Every DEFAULT, TTL and expression path is affected, i.e. the
32
+ * product's headline behaviour, so the FFI had to change.
33
+ *
34
+ * ffi-rs ships prebuilt binaries for darwin-arm64/x64 and linux arm64/x64
35
+ * (gnu and musl) — the shipping platform included — and needs no build step.
36
+ */
37
+ import { DataType, PointerType, createExternalBuffer, createPointer, define, freePointer, isNullPointer, load, open, restorePointer, wrapPointer, } from 'ffi-rs';
38
+ import { realpathSync } from 'node:fs';
39
+ import { ABI_REVISION, ChtypesError, schemaErrorFor, UnsupportedError } from './errors.js';
40
+ import { DOC_ALL, EXPORT_NONE } from './format.js';
41
+ const { External, String: Str, I32, U64, Void, U8Array } = DataType;
42
+ /** Counters a test can assert on: every taken string must be freed. */
43
+ const stats = { stringsTaken: 0, stringsFreed: 0 };
44
+ /**
45
+ * Leak-audit counters over every owned `char *` this process has taken from
46
+ * any loaded artifact. `stringsTaken === stringsFreed` when no chtypes call is
47
+ * on the stack; a test asserts exactly that (Level 1 conformance: zero growth
48
+ * over a few thousand calls). Diagnostic only — a snapshot, never live state.
49
+ *
50
+ * @returns a copy of the counters at the moment of the call. Never throws.
51
+ */
52
+ export function nativeStats() {
53
+ return { ...stats };
54
+ }
55
+ const MISSING_SYMBOL = /Cannot find "(.+?)" function in shared library/;
56
+ function missingSymbol(err) {
57
+ const m = err instanceof Error ? MISSING_SYMBOL.exec(err.message) : null;
58
+ return m === null ? null : (m[1] ?? '');
59
+ }
60
+ /**
61
+ * A symbol the artifact does not export means "this artifact predates the
62
+ * feature" and MUST degrade to `unsupported` at call time — never to a load
63
+ * failure (spec/artifact.md §Loading, step 5).
64
+ */
65
+ function unsupportedIfMissing(err, message) {
66
+ if (missingSymbol(err) !== null)
67
+ throw new UnsupportedError(message);
68
+ throw err;
69
+ }
70
+ function asBuffer(bytes) {
71
+ if (typeof bytes === 'string') {
72
+ throw new ChtypesError('chtypes: row bodies must be a byte sequence, not a string: binary formats ' +
73
+ 'contain NUL bytes and text rows can carry invalid UTF-8 on purpose ' +
74
+ '(spec/bindings.md §Values a binding must accept and reject).');
75
+ }
76
+ return Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
77
+ }
78
+ const ptrSlot = () => createPointer({ paramsType: [U64], paramsValue: [0] });
79
+ const intSlot = () => createPointer({ paramsType: [I32], paramsValue: [0] });
80
+ const readPtr = (s) => restorePointer({ retType: [External], paramsValue: s })[0];
81
+ const readInt = (s) => Number(restorePointer({ retType: [I32], paramsValue: s })[0] ?? 0);
82
+ const dropPtrSlot = (s) => freePointer({ paramsType: [U64], paramsValue: s, pointerType: PointerType.RsPointer });
83
+ const dropIntSlot = (s) => freePointer({ paramsType: [I32], paramsValue: s, pointerType: PointerType.RsPointer });
84
+ // ------------------------------------------------------------ the symbol table
85
+ function declare(library) {
86
+ const d = (retType, paramsType) => ({ library, retType, paramsType });
87
+ return define({
88
+ // Mandatory four (spec/artifact.md §Loading, step 4). chs_schema_compile is
89
+ // the ONE consolidated compile entry point — settings_json and mode are
90
+ // always part of its signature now, not a separate `_v2` overload.
91
+ chs_clickhouse_version: d(Str, []),
92
+ chs_abi_revision: d(I32, []),
93
+ chs_init: d(I32, [Str, Str, External]),
94
+ chs_schema_compile: d(External, [Str, Str, I32, External, External]),
95
+ // Revision 3: export_format (int), doc_flags (unsigned — I32 carries the
96
+ // three defined bits and any refused ones identically), out_bytes
97
+ // (chs_bytes * — passed as a 16-byte scratch buffer, see `rows`).
98
+ chs_rows: d(External, [External, I32, U8Array, U64, Str, I32, I32, U8Array]),
99
+ // Everything else is optional and degrades to `unsupported` at call time.
100
+ chs_free: d(Void, [External]),
101
+ chs_shutdown: d(Void, []),
102
+ chs_set_default_settings: d(I32, [Str, External]),
103
+ chs_validate_type: d(I32, [Str, External, External, External]),
104
+ chs_schema_free: d(Void, [External]),
105
+ // Also consolidated: merge_tree_settings_json is always part of the
106
+ // signature, not a separate `_v2` overload.
107
+ chs_schema_engine: d(I32, [External, Str, Str, Str, External]),
108
+ chs_schema_ttl: d(I32, [External, Str, External]),
109
+ chs_schema_column_count: d(I32, [External]),
110
+ chs_schema_column_name: d(Str, [External, I32]),
111
+ chs_schema_column_type: d(Str, [External, I32]),
112
+ chs_schema_column_default_kind: d(Str, [External, I32]),
113
+ chs_schema_column_default_expr: d(Str, [External, I32]),
114
+ chs_schema_column_default_is_literal: d(I32, [External, I32]),
115
+ chs_row: d(External, [External, I32, U8Array, U64, Str]),
116
+ chs_reference_type: d(External, [Str]),
117
+ chs_registered_families: d(External, []),
118
+ chs_function_flags: d(External, []),
119
+ // Revision 3: the filter trio. Optional like everything above — a missing
120
+ // symbol degrades to `unsupported` at call time.
121
+ // Revision 4: chs_filter_compile carries params_json ({name:Type} query
122
+ // parameters — a JSON object of name -> value STRING, "{}" for none). The
123
+ // ABI-revision gate is what guarantees this 5-argument declaration
124
+ // describes the loaded artifact: a rev-3 artifact (4-argument shape) is
125
+ // refused at load, and a rev-0 artifact predates the symbol entirely.
126
+ chs_filter_compile: d(External, [External, Str, Str, External, External]),
127
+ chs_filter_free: d(Void, [External]),
128
+ chs_filter_rows: d(External, [External, I32, U8Array, U64, Str]),
129
+ // Revision 4: the block twin (spec/c-abi.md §Blocks) — parse a body once,
130
+ // evaluate K filters against the block. Optional, same degradation rule.
131
+ chs_block_parse: d(External, [External, I32, U8Array, U64, Str, External, External]),
132
+ chs_block_free: d(Void, [External]),
133
+ chs_filter_eval: d(External, [External, External]),
134
+ // Reached through the artifact's own dependency graph (it links libc), and
135
+ // only used to bound the copy of an owned C string.
136
+ strlen: d(U64, [External]),
137
+ // Also libc, via the same graph: the copy out of the export channel's
138
+ // library-owned buffer, which arrives as a raw address inside the
139
+ // chs_bytes struct rather than as a JsExternal (see `rows`).
140
+ memcpy: d(Void, [U8Array, U64, U64]),
141
+ });
142
+ }
143
+ const loaded = new Map();
144
+ /** One loaded artifact. Loading the same path twice returns the same object. */
145
+ export class NativeLibrary {
146
+ fns;
147
+ haveStrlen;
148
+ initTimezone = null;
149
+ /** Cached answer of `hasCompileSettings` — one probe per loaded library. */
150
+ compileSettingsProbe = null;
151
+ /**
152
+ * How many calls into the library are on the stack right now — the reentrancy
153
+ * tripwire for the ABI's one genuinely dangerous entry point.
154
+ *
155
+ * `chs_set_default_settings` REPLACES a process-global that `chs_row` /
156
+ * `chs_rows` / `chs_schema_compile` read **by reference**, so
157
+ * `spec/c-abi.md` §Thread-safety requires it be serialized against every
158
+ * other call. Go enforces that with an `RWMutex` and Python with an
159
+ * `_RWLock`, because both have real threads inside a foreign call.
160
+ *
161
+ * **Node's exposure is nil, and this counter is the proof rather than the
162
+ * assumption.** Every `chs_*` call in this file is synchronous — ffi-rs is
163
+ * called without `runInNewThread`, nothing here awaits, and the library
164
+ * never calls back into JS — so one JS thread cannot be inside `chs_rows`
165
+ * when it enters `setDefaultSettings`. The only way it could is a genuine
166
+ * re-entry, and that is exactly what this counts. A tripwire that never
167
+ * fires is the point: if a later change makes a call async, or hands the
168
+ * library a JS callback, the seed stops being safe and this says so loudly
169
+ * instead of corrupting a row.
170
+ *
171
+ * Not a lock. It cannot make a `worker_threads` setup safe — separate JS
172
+ * threads share one dlopen'd image and one set of C globals, and only one of
173
+ * them would see this counter. `spec/bindings.md` §Concurrency says so.
174
+ */
175
+ inCall = 0;
176
+ path;
177
+ version;
178
+ /**
179
+ * The chs_* ABI revision this ARTIFACT was built from, or 0 when it predates
180
+ * `chs_abi_revision`. A library that exists reports either `ABI_REVISION` or
181
+ * 0 — a different nonzero revision is refused in the constructor.
182
+ */
183
+ abiRevision;
184
+ /** The realpath `open()` registered the image under — the `library` name for
185
+ * raw `load` calls that step outside the declared symbol table (the
186
+ * free-by-address in `rows`'s export path). */
187
+ key;
188
+ constructor(path, key) {
189
+ this.path = path;
190
+ this.key = key;
191
+ this.fns = declare(key);
192
+ // The library names itself; nothing is inferred from the path or file name.
193
+ this.version = this.fns.chs_clickhouse_version([]);
194
+ if (typeof this.version !== 'string' || this.version === '') {
195
+ throw new ChtypesError(`chtypes: ${path} did not report a ClickHouse version`);
196
+ }
197
+ // The ABI identity gate (spec/c-abi.md §ABI identity). ffi-rs resolves
198
+ // symbols at call time, so absence surfaces as its missing-symbol throw —
199
+ // which here means "the artifact predates the probe" (revision 0), keeping
200
+ // the per-symbol degradation rules. A DIFFERENT nonzero revision is a
201
+ // positive statement that these declarations do not describe this artifact,
202
+ // and calling through them would be undefined.
203
+ let abiRevision = 0;
204
+ try {
205
+ abiRevision = Number(this.fns.chs_abi_revision([]));
206
+ }
207
+ catch (err) {
208
+ if (missingSymbol(err) === null)
209
+ throw err;
210
+ }
211
+ this.abiRevision = abiRevision;
212
+ if (abiRevision !== 0 && abiRevision !== ABI_REVISION) {
213
+ throw new ChtypesError(`chtypes: ${path} reports ABI revision ${abiRevision}, this binding speaks ` +
214
+ `${ABI_REVISION}; refusing to call through mismatched declarations`);
215
+ }
216
+ // Assert the string-taking path against a known answer before trusting it on
217
+ // owned pointers: `chs_clickhouse_version` returns a borrowed static string,
218
+ // so probing it costs nothing and frees nothing.
219
+ this.haveStrlen = this.probeStrlen(key);
220
+ }
221
+ probeStrlen(key) {
222
+ try {
223
+ const ptr = load({
224
+ library: key,
225
+ funcName: 'chs_clickhouse_version',
226
+ retType: External,
227
+ paramsType: [],
228
+ paramsValue: [],
229
+ });
230
+ if (isNullPointer(ptr))
231
+ return false;
232
+ const n = Number(this.fns.strlen([ptr]));
233
+ return n === Buffer.byteLength(this.version, 'utf8') && createExternalBuffer(ptr, n).toString('utf8') === this.version;
234
+ }
235
+ catch {
236
+ return false;
237
+ }
238
+ }
239
+ static open(path) {
240
+ // dlopen is refcounted, and `chs_init` must run exactly once per loaded
241
+ // library, so two Registry instances over one directory must share one
242
+ // NativeLibrary rather than initialising it twice.
243
+ const key = realpathSync(path);
244
+ const existing = loaded.get(key);
245
+ if (existing !== undefined)
246
+ return existing;
247
+ open({ library: key, path });
248
+ const lib = new NativeLibrary(path, key);
249
+ loaded.set(key, lib);
250
+ return lib;
251
+ }
252
+ static isLoaded(path) {
253
+ try {
254
+ return loaded.has(realpathSync(path));
255
+ }
256
+ catch {
257
+ return false;
258
+ }
259
+ }
260
+ /**
261
+ * Copy an owned `char *` into **bytes** and release it with **this** library's
262
+ * `chs_free`. The only place that ever owns a returned pointer, and the only
263
+ * place that decides what a returned pointer becomes.
264
+ *
265
+ * It must be bytes, not a string. A result document carries ClickHouse's own
266
+ * rendering of stored values, and a `String` / `FixedString` /
267
+ * `AggregateFunction` column holds arbitrary bytes: a UTF-8 decode here would
268
+ * replace every invalid byte with U+FFFD before any caller could see the
269
+ * value, and the binding would then report bytes the table does not hold
270
+ * (spec/c-abi.md §Per-column fields; the reference implementation crosses this
271
+ * boundary with `C.GoString`, which is a byte copy, and keeps
272
+ * `json.RawMessage` from there on). The copy is bounded by the `strlen` this
273
+ * class already probes — bytes, not a latin1 round trip, because a `Buffer`
274
+ * needs no round trip at all.
275
+ */
276
+ takeBytes(ptr) {
277
+ if (isNullPointer(ptr))
278
+ return null;
279
+ stats.stringsTaken++;
280
+ try {
281
+ if (this.haveStrlen) {
282
+ const n = Number(this.fns.strlen([ptr]));
283
+ // A zero-length C string still has to come back as empty rather than
284
+ // null: an empty error message is not the absence of an error message.
285
+ if (n === 0)
286
+ return Buffer.alloc(0);
287
+ // createExternalBuffer WRAPS the library's own allocation, so the copy is
288
+ // not an optimisation to skip: `chs_free` runs in the finally below and
289
+ // the caller would be reading freed memory.
290
+ return Buffer.from(createExternalBuffer(ptr, n));
291
+ }
292
+ // Degraded mode, only for an artifact whose dependency graph gives no
293
+ // `strlen`: ffi-rs's own C-string reader copies through UTF-8, so a
294
+ // non-UTF-8 stored value cannot survive it. No artifact lands
295
+ // here (every artifact links libc), and `probeStrlen` proves the fast path
296
+ // against a known answer before it is trusted.
297
+ const text = restorePointer({ retType: [Str], paramsValue: wrapPointer([ptr]) })[0] ?? '';
298
+ return Buffer.from(text, 'utf8');
299
+ }
300
+ finally {
301
+ this.fns.chs_free([ptr]);
302
+ stats.stringsFreed++;
303
+ }
304
+ }
305
+ /**
306
+ * `takeBytes` decoded as UTF-8, for the paths whose payload is a message, a
307
+ * canonical type or a family list rather than a stored value.
308
+ *
309
+ * The reference implementation reaches the same place by a different route: it
310
+ * keeps ClickHouse's bytes in a Go string and lets `encoding/json` substitute
311
+ * U+FFFD when the message is marshalled out. Decoding one step earlier here
312
+ * gives the same text at the surface, and no result-document value passes
313
+ * through this method.
314
+ */
315
+ takeString(ptr) {
316
+ const bytes = this.takeBytes(ptr);
317
+ return bytes === null ? null : bytes.toString('utf8');
318
+ }
319
+ /**
320
+ * Run one library call with the reentrancy counter raised. See `inCall`.
321
+ * Counting rather than flagging so nested calls unwind correctly if a future
322
+ * change ever introduces one.
323
+ */
324
+ entered(call) {
325
+ this.inCall++;
326
+ try {
327
+ return call();
328
+ }
329
+ finally {
330
+ this.inCall--;
331
+ }
332
+ }
333
+ /**
334
+ * The guard the two process-state mutators take. Throws rather than waiting:
335
+ * there is nothing to wait FOR on a single JS thread — a call on the stack
336
+ * below this one can only be a re-entry, and re-entering the library to
337
+ * replace the settings list a caller further down is still reading is the
338
+ * use-after-free the ABI's serialization rule exists to prevent.
339
+ */
340
+ refuseIfInCall(what) {
341
+ if (this.inCall > 0) {
342
+ throw new ChtypesError(`chtypes: ${what} was called from inside another chtypes call on ${this.path}; ` +
343
+ `it replaces process-global state the row path reads by reference and ` +
344
+ `must be serialized against every other call (spec/c-abi.md §Thread-safety)`);
345
+ }
346
+ }
347
+ // ------------------------------------------------------------- process setup
348
+ /**
349
+ * `chs_init(timezone, unsafe_families, out_err)`, **exactly once** per loaded
350
+ * library — dlopen is refcounted, so two registries over one directory share
351
+ * this object and the second `init` must be a no-op rather than a second
352
+ * process setup.
353
+ *
354
+ * The timezone defaults to UTC and is never read from the environment: without
355
+ * it the host's TZ leaks into results. `unsafeFamilies` is the artifact's own
356
+ * generated refuse-list, and empty is a valid list rather than a missing file.
357
+ * `out_err` is ClickHouse's own message on the one reachable failure — an
358
+ * unknown timezone — and is folded into the thrown error's message; a bare
359
+ * code cannot say which name was rejected.
360
+ */
361
+ init(timezone, unsafeFamilies) {
362
+ if (this.initTimezone !== null) {
363
+ if (this.initTimezone !== timezone) {
364
+ throw new ChtypesError(`chtypes: ${this.path} is already loaded with timezone ${this.initTimezone}; ` +
365
+ `one loaded library gets one chs_init, so ${timezone} cannot be honoured`);
366
+ }
367
+ return;
368
+ }
369
+ const errSlot = ptrSlot();
370
+ try {
371
+ const rc = Number(this.fns.chs_init([timezone, unsafeFamilies, ...errSlot]));
372
+ if (rc !== 0) {
373
+ // The one reachable failure is an unknown timezone; out_err is the only
374
+ // way to say which name was rejected (spec/c-abi.md chs_init).
375
+ const message = this.takeString(readPtr(errSlot)) ?? '';
376
+ throw new ChtypesError(`chtypes: chs_init failed for ${this.path} (rc ${rc})${message === '' ? '' : `: ${message}`}`);
377
+ }
378
+ this.initTimezone = timezone;
379
+ }
380
+ finally {
381
+ dropPtrSlot(errSlot);
382
+ }
383
+ }
384
+ /**
385
+ * `chs_set_default_settings(settings_json, out_err)`. On refusal, `out_err`
386
+ * carries the server's own message — including its did-you-mean hint on an
387
+ * unknown-name 115, the whole diagnostic value of that code — folded into
388
+ * the thrown error's message.
389
+ */
390
+ setDefaultSettings(settingsJson) {
391
+ this.refuseIfInCall('setDefaultSettings');
392
+ const errSlot = ptrSlot();
393
+ try {
394
+ let rc;
395
+ try {
396
+ rc = Number(this.fns.chs_set_default_settings([settingsJson, ...errSlot]));
397
+ }
398
+ catch (err) {
399
+ return unsupportedIfMissing(err, 'this artifact predates chs_set_default_settings (rebuild it)');
400
+ }
401
+ if (rc !== 0) {
402
+ // out_err carries the server's own message here, including its
403
+ // did-you-mean hint on a 115 — the whole diagnostic value of that code.
404
+ const message = this.takeString(readPtr(errSlot)) ?? '';
405
+ throw new ChtypesError(`chtypes: chs_set_default_settings failed for ${this.path} (rc ${rc})${message === '' ? '' : `: ${message}`}`);
406
+ }
407
+ }
408
+ finally {
409
+ dropPtrSlot(errSlot);
410
+ }
411
+ }
412
+ /**
413
+ * Joins the DEFAULT evaluator's background threads. `chs_init` registers it
414
+ * with `atexit`, so an ordinary process needs no call; a test that must not
415
+ * depend on `atexit` does. Idempotent.
416
+ */
417
+ shutdown() {
418
+ this.refuseIfInCall('shutdown');
419
+ try {
420
+ this.fns.chs_shutdown([]);
421
+ }
422
+ catch {
423
+ // An artifact without chs_shutdown predates the hang it fixes.
424
+ }
425
+ }
426
+ // -------------------------------------------------------------------- types
427
+ validateType(expr) {
428
+ const canonSlot = ptrSlot();
429
+ const codeSlot = intSlot();
430
+ const errSlot = ptrSlot();
431
+ try {
432
+ let rc;
433
+ try {
434
+ rc = this.entered(() => Number(this.fns.chs_validate_type([expr, ...canonSlot, ...codeSlot, ...errSlot])));
435
+ }
436
+ catch (err) {
437
+ return unsupportedIfMissing(err, 'this artifact predates chs_validate_type (rebuild it)');
438
+ }
439
+ const canonical = this.takeString(readPtr(canonSlot)) ?? '';
440
+ const message = this.takeString(readPtr(errSlot)) ?? '';
441
+ return { canonical, code: readInt(codeSlot), message, ok: rc === 0 };
442
+ }
443
+ finally {
444
+ dropPtrSlot(canonSlot);
445
+ dropIntSlot(codeSlot);
446
+ dropPtrSlot(errSlot);
447
+ }
448
+ }
449
+ referenceType(expr) {
450
+ try {
451
+ return this.takeString(this.fns.chs_reference_type([expr])) ?? '';
452
+ }
453
+ catch (err) {
454
+ return unsupportedIfMissing(err, 'this artifact predates chs_reference_type (rebuild it)');
455
+ }
456
+ }
457
+ registeredFamilies() {
458
+ let text;
459
+ try {
460
+ text = this.takeString(this.fns.chs_registered_families([]));
461
+ }
462
+ catch (err) {
463
+ return unsupportedIfMissing(err, 'this artifact predates chs_registered_families (rebuild it)');
464
+ }
465
+ return (text ?? '').split('\n').filter((line) => line !== '');
466
+ }
467
+ /** The function-volatility TSV audit, verbatim. Requires `chs_init`. */
468
+ functionFlags() {
469
+ try {
470
+ return this.takeString(this.fns.chs_function_flags([])) ?? '';
471
+ }
472
+ catch (err) {
473
+ return unsupportedIfMissing(err, 'this artifact predates chs_function_flags (rebuild it)');
474
+ }
475
+ }
476
+ // ------------------------------------------------------------------ schemas
477
+ /**
478
+ * Compile a column-declaration list, optionally under a DECLARED settings
479
+ * profile fixed into the handle exactly as a real CREATE TABLE fixes its
480
+ * settings into the table (spec/c-abi.md §Compile-time vs per-call
481
+ * settings). `settingsJson` "{}" plus `mode` 0 (`CHS_COMPILE_DECLARED`) is
482
+ * structurally the plain compile — the settings-free path this build has
483
+ * always taken. Throws `SchemaError` when the SERVER refuses — an unknown
484
+ * setting name carries its own code 115 — and `UnsupportedError` when this
485
+ * build declines, e.g. a mode other than 0.
486
+ */
487
+ schemaCompile(ddl, settingsJson, mode) {
488
+ const codeSlot = intSlot();
489
+ const errSlot = ptrSlot();
490
+ try {
491
+ const handle = this.entered(() => this.fns.chs_schema_compile([ddl, settingsJson, mode, ...codeSlot, ...errSlot]));
492
+ if (!isNullPointer(handle))
493
+ return handle;
494
+ const message = this.takeString(readPtr(errSlot)) ?? '';
495
+ // No column is attributed here, and that is deliberate. Go's `dlopen`'d
496
+ // path (`multiversion.go`), Python and Rust all pass no column on this
497
+ // call, so `chtypes: [-2] …` is the spelling three of the four SDKs
498
+ // render and this one used to be alone in printing
499
+ // `chtypes: column "b": [-2] …`. It was a GUESS — the longest declared
500
+ // name that happened to appear in the server's text — and the library's
501
+ // own message already names the column when it knows one ("DEFAULT for
502
+ // column b exceeded the admission memory budget"), so the guess added no
503
+ // information and cost uniformity. Measured before removing it: the
504
+ // conformance wire is unaffected, because the driver puts `err.detail`
505
+ // (the bare message) in `scope`, never `err.message` — 6,411 recorded
506
+ // `unsupported` records across 7 versions, zero scope differences from
507
+ // the reference column, and zero column-attributed renders anywhere in
508
+ // the recorded output. See playground/README.md finding 5.
509
+ throw schemaErrorFor(readInt(codeSlot), message);
510
+ }
511
+ finally {
512
+ dropIntSlot(codeSlot);
513
+ dropPtrSlot(errSlot);
514
+ }
515
+ }
516
+ /**
517
+ * Does this artifact export the consolidated, settings-aware
518
+ * `chs_schema_compile`? True on every artifact this repo builds — the
519
+ * symbol is one of the mandatory four (spec/artifact.md §Loading, step 4).
520
+ * The probe stays for a Registry that may someday load a third-party-built
521
+ * artifact that lacks it.
522
+ *
523
+ * The reference loader answers this with a load-time `dlsym`
524
+ * (`go/chtypes/multiversion.go`, `HasCompileSettings`); ffi-rs
525
+ * resolves symbols at call time, so this asks with a call the ABI defines as
526
+ * refused loudly and cheaply: `mode 1` with a non-empty profile answers `-2`
527
+ * and creates no handle on every artifact that exports the symbol
528
+ * (spec/c-abi.md §Compile-time vs per-call settings, rule 2), while an
529
+ * artifact without the symbol throws ffi-rs's missing-symbol error. Cached:
530
+ * one refused compile per loaded library.
531
+ *
532
+ * False means `compileDdl` with a non-empty `settings` and `setEngine` with
533
+ * non-empty `mergeTreeSettings` throw an `UnsupportedError` —
534
+ * never a crash and never a silent settings-free compile of a
535
+ * differently-shaped table.
536
+ */
537
+ hasCompileSettings() {
538
+ if (this.compileSettingsProbe !== null)
539
+ return this.compileSettingsProbe;
540
+ const codeSlot = intSlot();
541
+ const errSlot = ptrSlot();
542
+ try {
543
+ const handle = this.fns.chs_schema_compile([
544
+ 'x UInt8',
545
+ '{"flatten_nested":"1"}',
546
+ 1,
547
+ ...codeSlot,
548
+ ...errSlot,
549
+ ]);
550
+ // Refused as designed — free the message it came with. Defensively free
551
+ // the handle too: an artifact that ever accepted this call would still
552
+ // have proven the symbol, and must not leak a schema doing it.
553
+ if (!isNullPointer(handle))
554
+ this.fns.chs_schema_free([handle]);
555
+ this.takeString(readPtr(errSlot));
556
+ this.compileSettingsProbe = true;
557
+ }
558
+ catch (err) {
559
+ if (missingSymbol(err) === null)
560
+ throw err;
561
+ this.compileSettingsProbe = false;
562
+ }
563
+ finally {
564
+ dropIntSlot(codeSlot);
565
+ dropPtrSlot(errSlot);
566
+ }
567
+ return this.compileSettingsProbe;
568
+ }
569
+ schemaFree(handle) {
570
+ try {
571
+ this.fns.chs_schema_free([handle]);
572
+ }
573
+ catch {
574
+ // Nothing sane to do while releasing; the process is going away anyway.
575
+ }
576
+ }
577
+ /**
578
+ * Declare the table engine, optionally with its MergeTree-namespace
579
+ * settings (the `SETTINGS` clause after the engine — `allow_nullable_key`
580
+ * and friends, a namespace `DB::Settings` cannot carry). `mergeTreeSettingsJson`
581
+ * "{}" is structurally the plain engine declaration this build has always
582
+ * made.
583
+ *
584
+ * Error mapping per spec/c-abi.md: the SIGN of rc decides the KIND of
585
+ * answer. A positive rc is a real ClickHouse code — the server's own
586
+ * refusal of this DDL, which can therefore never exist — and crosses
587
+ * verbatim as a `SchemaError` with that code and the server's message
588
+ * (`115`, with its "Maybe you meant ..." hint, is today's only instance).
589
+ * A negative rc is this library declining and is an `UnsupportedError`: a
590
+ * decline must never be presented as a rejection the product invented, and
591
+ * a rejection must never be hidden behind a decline. A missing symbol
592
+ * degrades to `UnsupportedError`, never a crash.
593
+ */
594
+ schemaEngine(handle, engine, orderBy, mergeTreeSettingsJson) {
595
+ const errSlot = ptrSlot();
596
+ try {
597
+ let rc;
598
+ try {
599
+ rc = this.entered(() => Number(this.fns.chs_schema_engine([handle, engine, orderBy, mergeTreeSettingsJson, ...errSlot])));
600
+ }
601
+ catch (err) {
602
+ return unsupportedIfMissing(err, 'this artifact predates engine support (rebuild it)');
603
+ }
604
+ if (rc === 0)
605
+ return;
606
+ const message = this.takeString(readPtr(errSlot)) ?? '';
607
+ // The SIGN of rc is the rule. Positive is a real ClickHouse error code:
608
+ // the server's own engine validation REFUSED this DDL, it can never
609
+ // exist, and the tenant has to be told — a SchemaError carrying the
610
+ // server's code and message (which holds the "Maybe you meant ..."
611
+ // hint). Negative is this library declining (-2 "I will not guess", -1 a
612
+ // guarded exception) and becomes an UnsupportedError. schemaErrorFor
613
+ // keys on the sign, not on the literal 115, so a code a future era
614
+ // returns here cannot silently be demoted to a decline.
615
+ throw schemaErrorFor(rc, message);
616
+ }
617
+ finally {
618
+ dropPtrSlot(errSlot);
619
+ }
620
+ }
621
+ schemaTtl(handle, ttl) {
622
+ const errSlot = ptrSlot();
623
+ try {
624
+ let rc;
625
+ try {
626
+ rc = this.entered(() => Number(this.fns.chs_schema_ttl([handle, ttl, ...errSlot])));
627
+ }
628
+ catch (err) {
629
+ return unsupportedIfMissing(err, 'this artifact predates TTL support (rebuild it)');
630
+ }
631
+ if (rc === 0)
632
+ return;
633
+ throw new UnsupportedError(this.takeString(readPtr(errSlot)) ?? '');
634
+ }
635
+ finally {
636
+ dropPtrSlot(errSlot);
637
+ }
638
+ }
639
+ /**
640
+ * Column introspection is all-or-nothing: the six getters shipped together, so
641
+ * a missing one means the whole group is absent and the column list stays empty
642
+ * rather than partially populated.
643
+ */
644
+ columns(handle) {
645
+ try {
646
+ const n = Number(this.fns.chs_schema_column_count([handle]));
647
+ const out = [];
648
+ for (let i = 0; i < n; i++) {
649
+ out.push({
650
+ name: this.fns.chs_schema_column_name([handle, i]),
651
+ type: this.fns.chs_schema_column_type([handle, i]),
652
+ defaultKind: this.fns.chs_schema_column_default_kind([handle, i]),
653
+ defaultExpr: this.fns.chs_schema_column_default_expr([handle, i]),
654
+ defaultIsLiteral: Number(this.fns.chs_schema_column_default_is_literal([handle, i])) !== 0,
655
+ });
656
+ }
657
+ return out;
658
+ }
659
+ catch (err) {
660
+ if (missingSymbol(err) !== null)
661
+ return [];
662
+ throw err;
663
+ }
664
+ }
665
+ // --------------------------------------------------------------------- rows
666
+ /**
667
+ * `chs_row`: one row body. Returns the raw result document **as bytes** — a
668
+ * stored value can be any byte sequence, so the document can be too.
669
+ */
670
+ row(handle, format, raw, settingsJson) {
671
+ const body = asBuffer(raw);
672
+ let ptr;
673
+ try {
674
+ ptr = this.entered(() => this.fns.chs_row([handle, format, body, body.length, settingsJson]));
675
+ }
676
+ catch (err) {
677
+ return unsupportedIfMissing(err, 'this artifact predates chs_row (rebuild it)');
678
+ }
679
+ // A NULL return is how the reference reports the same thing through cgo.
680
+ const doc = this.takeBytes(ptr);
681
+ if (doc === null)
682
+ throw new UnsupportedError('this artifact predates chs_row (rebuild it)');
683
+ return doc;
684
+ }
685
+ /**
686
+ * `chs_rows`: a whole request body — ONE call, whatever the caller asked
687
+ * for (spec/c-abi.md §Rows; revision 3). `exportFormat` is `EXPORT_NONE`
688
+ * (-1, the default — no bytes) or an `enum chs_format` value the artifact
689
+ * can serialize; `docFlags` selects the document groups (`DOC_ALL`
690
+ * reproduces the revision-2 document byte-for-byte). Returns the raw result
691
+ * document as bytes, plus the export channel:
692
+ *
693
+ * `payload === null` the library emitted nothing — no export requested,
694
+ * the export DECLINED (the document's
695
+ * `export_declined` says why), or a call-level
696
+ * verdict preempted it. The ABI's `{NULL, 0}`.
697
+ * zero-length Buffer EMITTED-EMPTY: an accepted batch with zero accepted
698
+ * rows — `data` non-NULL, `len` 0. An answer, not a
699
+ * decline.
700
+ * bytes the batch's accepted rows, serialized once by the
701
+ * vendored writer; slice per the document's
702
+ * `row_spans`.
703
+ *
704
+ * Ownership: the `chs_bytes` out-param is a 16-byte scratch struct
705
+ * (`{char *data; size_t len}`, both little-endian on every supported
706
+ * platform); the library's malloc'd `data` is COPIED here and immediately
707
+ * freed with THIS library's `chs_free` — no ownership ever escapes this
708
+ * method. The copy runs through libc `memcpy` because the address arrives
709
+ * as a struct field rather than a `JsExternal`, and the free goes through a
710
+ * raw `load` of `chs_free` with the address passed by value — the C ABI
711
+ * passes a `u64` and a pointer identically on darwin-arm64 and
712
+ * linux-arm64/x64.
713
+ *
714
+ * A NULL out-param is legal at the C level only when no export is
715
+ * requested; this binding always passes the scratch struct, which the
716
+ * library initializes to `{NULL, 0}` at entry — same document either way,
717
+ * and one less branch to get wrong.
718
+ *
719
+ * Degrades exactly as `row` does (the asymmetry was a 2026-08-26 fix): a
720
+ * missing symbol and a NULL return are both the ABI's "the loaded artifact
721
+ * does not export the function" (spec/c-abi.md §Rows) and surface as the
722
+ * DECLINE type, never a crash and never a generic error a caller cannot
723
+ * handle as the decline it is. `chs_rows` is one of the mandatory four, so
724
+ * with repo-built artifacts the branch is unreachable — the type still has
725
+ * to be the honest one.
726
+ */
727
+ rows(handle, format, body, settingsJson, exportFormat = EXPORT_NONE, docFlags = DOC_ALL) {
728
+ const buf = asBuffer(body);
729
+ // The chs_bytes out-param: {char *data; size_t len}, 16 bytes, zeroed.
730
+ const outBytes = Buffer.alloc(16);
731
+ let ptr;
732
+ try {
733
+ ptr = this.entered(() => this.fns.chs_rows([
734
+ handle,
735
+ format,
736
+ buf,
737
+ buf.length,
738
+ settingsJson,
739
+ exportFormat,
740
+ docFlags,
741
+ outBytes,
742
+ ]));
743
+ }
744
+ catch (err) {
745
+ return unsupportedIfMissing(err, 'this artifact predates chs_rows (rebuild it)');
746
+ }
747
+ // Copy-then-free the export buffer FIRST, whatever the document says: the
748
+ // bytes are library-owned malloc'd memory and this is the one place that
749
+ // ever sees the address.
750
+ let payload = null;
751
+ const dataAddr = outBytes.readBigUInt64LE(0);
752
+ if (dataAddr !== 0n) {
753
+ const len = Number(outBytes.readBigUInt64LE(8));
754
+ if (len > 0) {
755
+ payload = Buffer.alloc(len);
756
+ this.fns.memcpy([payload, Number(dataAddr), len]);
757
+ }
758
+ else {
759
+ payload = Buffer.alloc(0);
760
+ }
761
+ load({
762
+ library: this.key,
763
+ funcName: 'chs_free',
764
+ retType: DataType.Void,
765
+ paramsType: [DataType.U64],
766
+ paramsValue: [Number(dataAddr)],
767
+ });
768
+ stats.stringsTaken++;
769
+ stats.stringsFreed++;
770
+ }
771
+ const doc = this.takeBytes(ptr);
772
+ if (doc === null)
773
+ throw new UnsupportedError('this artifact predates chs_rows (rebuild it)');
774
+ return { doc, payload };
775
+ }
776
+ // ------------------------------------------------------------------ filters
777
+ /**
778
+ * `chs_filter_compile`: one boolean expression over a compiled schema's
779
+ * PHYSICAL columns, compiled by the same TreeRewriter + ExpressionAnalyzer
780
+ * pipeline the CONSTRAINT CHECK path runs (spec/c-abi.md §Filters).
781
+ *
782
+ * The error split is rule 12's: a NULL handle with a positive code is the
783
+ * server's own refusal (`SchemaError`, code and message verbatim — unknown
784
+ * identifier 47, unknown function, an analyzer-raised NO_COMMON_TYPE, and
785
+ * since revision 4 the server's own parameter refusals: 456 for an unbound
786
+ * `{name:Type}`, 457 for an unparseable value); a negative code is this
787
+ * library declining (`UnsupportedError` — clock reads, server-constants).
788
+ * A missing symbol degrades to the decline type: the artifact predates the
789
+ * trio.
790
+ *
791
+ * `paramsJson` (revision 4) is the `{name:Type}` bindings, a JSON object
792
+ * of name -> value STRING; `"{}"` declares none.
793
+ */
794
+ filterCompile(schema, expr, paramsJson) {
795
+ const codeSlot = intSlot();
796
+ const errSlot = ptrSlot();
797
+ try {
798
+ let handle;
799
+ try {
800
+ handle = this.entered(() => this.fns.chs_filter_compile([schema, expr, paramsJson, ...codeSlot, ...errSlot]));
801
+ }
802
+ catch (err) {
803
+ return unsupportedIfMissing(err, 'this artifact predates chs_filter_compile (rebuild it)');
804
+ }
805
+ if (!isNullPointer(handle))
806
+ return handle;
807
+ const message = this.takeString(readPtr(errSlot)) ?? '';
808
+ throw schemaErrorFor(readInt(codeSlot), message);
809
+ }
810
+ finally {
811
+ dropIntSlot(codeSlot);
812
+ dropPtrSlot(errSlot);
813
+ }
814
+ }
815
+ /** `chs_filter_free`. Never throws: nothing sane to do while releasing. */
816
+ filterFree(handle) {
817
+ try {
818
+ this.fns.chs_filter_free([handle]);
819
+ }
820
+ catch {
821
+ // An artifact without the symbol never produced a handle to free.
822
+ }
823
+ }
824
+ /**
825
+ * `chs_filter_rows`: evaluate the filter over a body of rows. Returns the
826
+ * raw filter document as bytes; a missing symbol or NULL return degrades to
827
+ * the decline type like every optional entry point.
828
+ */
829
+ filterRows(handle, format, body, settingsJson) {
830
+ const buf = asBuffer(body);
831
+ let ptr;
832
+ try {
833
+ ptr = this.entered(() => this.fns.chs_filter_rows([handle, format, buf, buf.length, settingsJson]));
834
+ }
835
+ catch (err) {
836
+ return unsupportedIfMissing(err, 'this artifact predates chs_filter_rows (rebuild it)');
837
+ }
838
+ const doc = this.takeBytes(ptr);
839
+ if (doc === null)
840
+ throw new UnsupportedError('this artifact predates chs_filter_rows (rebuild it)');
841
+ return doc;
842
+ }
843
+ // ------------------------------------------------------------------ blocks
844
+ /**
845
+ * `chs_block_parse` (revision 4): parse a body ONCE into a block — the
846
+ * parse half of `chs_filter_rows`, exported so K filters can evaluate one
847
+ * event with no re-parse (spec/c-abi.md §Blocks). A call-level failure
848
+ * (unknown setting 115, framing, a binary decode fault) returns NULL with
849
+ * ClickHouse's own code/message — a malformed body yields no block and no
850
+ * partial answers; the sign of the code picks the error class, exactly as
851
+ * `filterCompile`. A missing symbol degrades to the decline type.
852
+ */
853
+ blockParse(schema, format, body, settingsJson) {
854
+ const buf = asBuffer(body);
855
+ const codeSlot = intSlot();
856
+ const errSlot = ptrSlot();
857
+ try {
858
+ let handle;
859
+ try {
860
+ handle = this.entered(() => this.fns.chs_block_parse([
861
+ schema,
862
+ format,
863
+ buf,
864
+ buf.length,
865
+ settingsJson,
866
+ ...codeSlot,
867
+ ...errSlot,
868
+ ]));
869
+ }
870
+ catch (err) {
871
+ return unsupportedIfMissing(err, 'this artifact predates chs_block_parse (rebuild it)');
872
+ }
873
+ if (!isNullPointer(handle))
874
+ return handle;
875
+ const message = this.takeString(readPtr(errSlot)) ?? '';
876
+ throw schemaErrorFor(readInt(codeSlot), message);
877
+ }
878
+ finally {
879
+ dropIntSlot(codeSlot);
880
+ dropPtrSlot(errSlot);
881
+ }
882
+ }
883
+ /** `chs_block_free`. Never throws: nothing sane to do while releasing. */
884
+ blockFree(handle) {
885
+ try {
886
+ this.fns.chs_block_free([handle]);
887
+ }
888
+ catch {
889
+ // An artifact without the symbol never produced a handle to free.
890
+ }
891
+ }
892
+ /**
893
+ * `chs_filter_eval` (revision 4): evaluate one compiled filter over an
894
+ * already-parsed block — a pure function of (filter, block), no settings.
895
+ * Returns the same raw filter document `chs_filter_rows` returns; a
896
+ * cross-schema (filter, block) pair answers a rejected document (1002)
897
+ * from the C layer, loudly, never undefined behaviour.
898
+ */
899
+ filterEval(filter, block) {
900
+ let ptr;
901
+ try {
902
+ ptr = this.entered(() => this.fns.chs_filter_eval([filter, block]));
903
+ }
904
+ catch (err) {
905
+ return unsupportedIfMissing(err, 'this artifact predates chs_filter_eval (rebuild it)');
906
+ }
907
+ const doc = this.takeBytes(ptr);
908
+ if (doc === null)
909
+ throw new UnsupportedError('this artifact predates chs_filter_eval (rebuild it)');
910
+ return doc;
911
+ }
912
+ }
913
+ //# sourceMappingURL=ffi.js.map