@wavehouse/chtypes 0.5.2 → 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.
- package/CHANGELOG.md +26 -0
- package/README.md +34 -38
- package/dist/abi1/buildinfo.d.ts +40 -0
- package/dist/abi1/buildinfo.d.ts.map +1 -0
- package/dist/abi1/buildinfo.js +83 -0
- package/dist/abi1/buildinfo.js.map +1 -0
- package/dist/abi1/calls.gen.d.ts +55 -0
- package/dist/abi1/calls.gen.d.ts.map +1 -0
- package/dist/abi1/calls.gen.js +110 -0
- package/dist/abi1/calls.gen.js.map +1 -0
- package/dist/abi1/decls.gen.d.ts +115 -0
- package/dist/abi1/decls.gen.d.ts.map +1 -0
- package/dist/abi1/decls.gen.js +1613 -0
- package/dist/abi1/decls.gen.js.map +1 -0
- package/dist/abi1/errmap.gen.d.ts +18 -0
- package/dist/abi1/errmap.gen.d.ts.map +1 -0
- package/dist/abi1/errmap.gen.js +66 -0
- package/dist/abi1/errmap.gen.js.map +1 -0
- package/dist/abi1/errors.d.ts +107 -0
- package/dist/abi1/errors.d.ts.map +1 -0
- package/dist/abi1/errors.js +134 -0
- package/dist/abi1/errors.js.map +1 -0
- package/dist/abi1/handles.d.ts +31 -0
- package/dist/abi1/handles.d.ts.map +1 -0
- package/dist/abi1/handles.js +76 -0
- package/dist/abi1/handles.js.map +1 -0
- package/dist/abi1/index.d.ts +17 -0
- package/dist/abi1/index.d.ts.map +1 -0
- package/dist/abi1/index.js +17 -0
- package/dist/abi1/index.js.map +1 -0
- package/dist/abi1/libc.d.ts +54 -0
- package/dist/abi1/libc.d.ts.map +1 -0
- package/dist/abi1/libc.gen.d.ts +12 -0
- package/dist/abi1/libc.gen.d.ts.map +1 -0
- package/dist/abi1/libc.gen.js +75 -0
- package/dist/abi1/libc.gen.js.map +1 -0
- package/dist/abi1/libc.js +109 -0
- package/dist/abi1/libc.js.map +1 -0
- package/dist/abi1/loader.d.ts +99 -0
- package/dist/abi1/loader.d.ts.map +1 -0
- package/dist/abi1/loader.js +261 -0
- package/dist/abi1/loader.js.map +1 -0
- package/dist/abi1/raw.d.ts +104 -0
- package/dist/abi1/raw.d.ts.map +1 -0
- package/dist/abi1/raw.js +296 -0
- package/dist/abi1/raw.js.map +1 -0
- package/dist/abi1/strictjson.d.ts +19 -0
- package/dist/abi1/strictjson.d.ts.map +1 -0
- package/dist/abi1/strictjson.js +78 -0
- package/dist/abi1/strictjson.js.map +1 -0
- package/dist/abi1/vocab.gen.d.ts +169 -0
- package/dist/abi1/vocab.gen.d.ts.map +1 -0
- package/dist/abi1/vocab.gen.js +306 -0
- package/dist/abi1/vocab.gen.js.map +1 -0
- package/dist/cli.d.ts +25 -26
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +206 -218
- package/dist/cli.js.map +1 -1
- package/dist/documents.d.ts +195 -0
- package/dist/documents.d.ts.map +1 -0
- package/dist/documents.js +389 -0
- package/dist/documents.js.map +1 -0
- package/dist/env.d.ts +16 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +57 -0
- package/dist/env.js.map +1 -0
- package/dist/index.d.ts +19 -24
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -23
- package/dist/index.js.map +1 -1
- package/dist/json.d.ts +11 -89
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +16 -180
- package/dist/json.js.map +1 -1
- package/dist/library.d.ts +53 -309
- package/dist/library.d.ts.map +1 -1
- package/dist/library.js +77 -315
- package/dist/library.js.map +1 -1
- package/dist/ocifetch/constants.gen.d.ts +84 -0
- package/dist/ocifetch/constants.gen.d.ts.map +1 -0
- package/dist/ocifetch/constants.gen.js +92 -0
- package/dist/ocifetch/constants.gen.js.map +1 -0
- package/dist/ocifetch/dsse.d.ts +114 -0
- package/dist/ocifetch/dsse.d.ts.map +1 -0
- package/dist/ocifetch/dsse.js +331 -0
- package/dist/ocifetch/dsse.js.map +1 -0
- package/dist/ocifetch/ensure.d.ts +65 -0
- package/dist/ocifetch/ensure.d.ts.map +1 -0
- package/dist/ocifetch/ensure.js +775 -0
- package/dist/ocifetch/ensure.js.map +1 -0
- package/dist/ocifetch/errors.d.ts +92 -0
- package/dist/ocifetch/errors.d.ts.map +1 -0
- package/dist/ocifetch/errors.js +101 -0
- package/dist/ocifetch/errors.js.map +1 -0
- package/dist/ocifetch/goldens.d.ts +44 -0
- package/dist/ocifetch/goldens.d.ts.map +1 -0
- package/dist/ocifetch/goldens.js +62 -0
- package/dist/ocifetch/goldens.js.map +1 -0
- package/dist/ocifetch/http.d.ts +124 -0
- package/dist/ocifetch/http.d.ts.map +1 -0
- package/dist/ocifetch/http.js +549 -0
- package/dist/ocifetch/http.js.map +1 -0
- package/dist/ocifetch/index.d.ts +17 -0
- package/dist/ocifetch/index.d.ts.map +1 -0
- package/dist/ocifetch/index.js +14 -0
- package/dist/ocifetch/index.js.map +1 -0
- package/dist/ocifetch/layout.d.ts +126 -0
- package/dist/ocifetch/layout.d.ts.map +1 -0
- package/dist/ocifetch/layout.js +375 -0
- package/dist/ocifetch/layout.js.map +1 -0
- package/dist/ocifetch/localverify.d.ts +43 -0
- package/dist/ocifetch/localverify.d.ts.map +1 -0
- package/dist/ocifetch/localverify.js +164 -0
- package/dist/ocifetch/localverify.js.map +1 -0
- package/dist/ocifetch/lock.d.ts +39 -0
- package/dist/ocifetch/lock.d.ts.map +1 -0
- package/dist/ocifetch/lock.js +140 -0
- package/dist/ocifetch/lock.js.map +1 -0
- package/dist/ocifetch/oci.d.ts +100 -0
- package/dist/ocifetch/oci.d.ts.map +1 -0
- package/dist/ocifetch/oci.js +323 -0
- package/dist/ocifetch/oci.js.map +1 -0
- package/dist/ocifetch/referrers.d.ts +55 -0
- package/dist/ocifetch/referrers.d.ts.map +1 -0
- package/dist/ocifetch/referrers.js +132 -0
- package/dist/ocifetch/referrers.js.map +1 -0
- package/dist/ocifetch/types.d.ts +167 -0
- package/dist/ocifetch/types.d.ts.map +1 -0
- package/dist/ocifetch/types.js +88 -0
- package/dist/ocifetch/types.js.map +1 -0
- package/dist/ocifetch/unpack.d.ts +55 -0
- package/dist/ocifetch/unpack.d.ts.map +1 -0
- package/dist/ocifetch/unpack.js +205 -0
- package/dist/ocifetch/unpack.js.map +1 -0
- package/dist/registry.d.ts +38 -387
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +94 -819
- package/dist/registry.js.map +1 -1
- package/dist/schema.d.ts +77 -588
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +87 -549
- package/dist/schema.js.map +1 -1
- package/dist/settings.d.ts +27 -36
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +75 -25
- package/dist/settings.js.map +1 -1
- package/dist/setup.d.ts +37 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +60 -0
- package/dist/setup.js.map +1 -0
- package/dist/tar.d.ts +27 -11
- package/dist/tar.d.ts.map +1 -1
- package/dist/tar.js +54 -32
- package/dist/tar.js.map +1 -1
- package/package.json +2 -2
- package/dist/discover.d.ts +0 -142
- package/dist/discover.d.ts.map +0 -1
- package/dist/discover.js +0 -288
- package/dist/discover.js.map +0 -1
- package/dist/error-codes.d.ts +0 -80
- package/dist/error-codes.d.ts.map +0 -1
- package/dist/error-codes.js +0 -139
- package/dist/error-codes.js.map +0 -1
- package/dist/errors.d.ts +0 -284
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -321
- package/dist/errors.js.map +0 -1
- package/dist/fetch.d.ts +0 -382
- package/dist/fetch.d.ts.map +0 -1
- package/dist/fetch.js +0 -1498
- package/dist/fetch.js.map +0 -1
- package/dist/ffi.d.ts +0 -410
- package/dist/ffi.d.ts.map +0 -1
- package/dist/ffi.js +0 -1154
- package/dist/ffi.js.map +0 -1
- package/dist/format.d.ts +0 -128
- package/dist/format.d.ts.map +0 -1
- package/dist/format.js +0 -132
- package/dist/format.js.map +0 -1
- package/dist/numeric.d.ts +0 -20
- package/dist/numeric.d.ts.map +0 -1
- package/dist/numeric.js +0 -107
- package/dist/numeric.js.map +0 -1
- package/dist/paths.d.ts +0 -55
- package/dist/paths.d.ts.map +0 -1
- package/dist/paths.js +0 -87
- package/dist/paths.js.map +0 -1
- package/dist/results.d.ts +0 -495
- package/dist/results.d.ts.map +0 -1
- package/dist/results.js +0 -406
- package/dist/results.js.map +0 -1
- package/dist/transform.d.ts +0 -73
- package/dist/transform.d.ts.map +0 -1
- package/dist/transform.js +0 -533
- 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
|