@wavehouse/chtypes 0.5.2 → 1.0.2

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