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