@wavehouse/chtypes 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/LICENSE +202 -0
- package/README.md +442 -0
- package/dist/cli.d.ts +43 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +303 -0
- package/dist/cli.js.map +1 -0
- package/dist/discover.d.ts +134 -0
- package/dist/discover.d.ts.map +1 -0
- package/dist/discover.js +291 -0
- package/dist/discover.js.map +1 -0
- package/dist/errors.d.ts +204 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +228 -0
- package/dist/errors.js.map +1 -0
- package/dist/fetch.d.ts +235 -0
- package/dist/fetch.d.ts.map +1 -0
- package/dist/fetch.js +842 -0
- package/dist/fetch.js.map +1 -0
- package/dist/ffi.d.ts +346 -0
- package/dist/ffi.d.ts.map +1 -0
- package/dist/ffi.js +913 -0
- package/dist/ffi.js.map +1 -0
- package/dist/format.d.ts +104 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +108 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/json.d.ts +127 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +577 -0
- package/dist/json.js.map +1 -0
- package/dist/library.d.ts +225 -0
- package/dist/library.d.ts.map +1 -0
- package/dist/library.js +234 -0
- package/dist/library.js.map +1 -0
- package/dist/numeric.d.ts +20 -0
- package/dist/numeric.d.ts.map +1 -0
- package/dist/numeric.js +107 -0
- package/dist/numeric.js.map +1 -0
- package/dist/paths.d.ts +47 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +78 -0
- package/dist/paths.js.map +1 -0
- package/dist/registry.d.ts +204 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +409 -0
- package/dist/registry.js.map +1 -0
- package/dist/results.d.ts +326 -0
- package/dist/results.d.ts.map +1 -0
- package/dist/results.js +235 -0
- package/dist/results.js.map +1 -0
- package/dist/schema.d.ts +444 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +448 -0
- package/dist/schema.js.map +1 -0
- package/dist/settings.d.ts +43 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +33 -0
- package/dist/settings.js.map +1 -0
- package/dist/tar.d.ts +32 -0
- package/dist/tar.d.ts.map +1 -0
- package/dist/tar.js +310 -0
- package/dist/tar.js.map +1 -0
- package/dist/transform.d.ts +73 -0
- package/dist/transform.d.ts.map +1 -0
- package/dist/transform.js +527 -0
- package/dist/transform.js.map +1 -0
- package/package.json +62 -0
package/README.md
ADDED
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
# chtypes — TypeScript SDK
|
|
2
|
+
|
|
3
|
+
`@wavehouse/chtypes`: ClickHouse's own type system, schema validation,
|
|
4
|
+
DEFAULT/TTL logic and coercion, per ClickHouse version, behind the
|
|
5
|
+
`chs_*` C ABI (pre-1.0 only the names and the format integers are frozen —
|
|
6
|
+
signatures can still change by deliberate cycle, which is why the loader
|
|
7
|
+
checks `chs_abi_revision` before trusting them). One peer binding among
|
|
8
|
+
`{go,python,ts,rust}`; no
|
|
9
|
+
language is privileged. Nothing here reimplements a coercion rule — every
|
|
10
|
+
verdict comes from real ClickHouse code compiled from the pinned release; the
|
|
11
|
+
one derived answer is `transformed`, the silent-change report the product
|
|
12
|
+
exists for.
|
|
13
|
+
|
|
14
|
+
Node ≥ 22, ESM only. Native calls go through `ffi-rs` (prebuilt for
|
|
15
|
+
darwin-arm64/x64 and linux arm64/x64, gnu and musl — no build step).
|
|
16
|
+
|
|
17
|
+
## Install (pre-publish)
|
|
18
|
+
|
|
19
|
+
The package is not published yet. Wire it up by path, with pnpm (this repo's
|
|
20
|
+
package manager):
|
|
21
|
+
|
|
22
|
+
```jsonc
|
|
23
|
+
// your app's package.json
|
|
24
|
+
{ "dependencies": { "@wavehouse/chtypes": "file:../chtypes/ts" } }
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
or `pnpm link <repo>/ts`. Either way, build the package's `dist/`
|
|
28
|
+
first — that is what `package.json#exports` serves:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
cd <repo>/ts
|
|
32
|
+
pnpm install && pnpm build
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
(If a linked copy goes stale, note `pnpm install --force` does **not** relink;
|
|
36
|
+
remove `node_modules` and reinstall.)
|
|
37
|
+
|
|
38
|
+
**Artifact prerequisite.** The SDK loads per-version native artifacts; it does
|
|
39
|
+
nothing without them. Fetch a published one — verified, into the per-user cache
|
|
40
|
+
every SDK here defaults to — with the package's own command, or build one in
|
|
41
|
+
the core repository, which lands it in the same place:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
npx @wavehouse/chtypes fetch 25.8 # -> ~/.cache/chtypes/artifacts/<os>-<arch>/25.8/
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
(`scripts/fetch.sh` at the repository root is the reference implementation of
|
|
48
|
+
the same contract, `docs/fetch.md`; the command, the function and the lazy
|
|
49
|
+
fetch are in [Artifacts: fetch, verify, autofetch](#artifacts-fetch-verify-autofetch).)
|
|
50
|
+
|
|
51
|
+
A registry holds one subdirectory per version:
|
|
52
|
+
|
|
53
|
+
<registry>/25.8/
|
|
54
|
+
manifest.json # names the library file — the loader reads this, never guesses
|
|
55
|
+
libchtypes.dylib # (.so on Linux; the name comes from manifest.json)
|
|
56
|
+
CH_VERSION
|
|
57
|
+
unsafe_families.txt # the build's own refuse-list; empty is a valid list
|
|
58
|
+
|
|
59
|
+
`new Registry(dir?)` scans such a directory. Where it looks is the search path
|
|
60
|
+
of `docs/fetch.md` §1, in order: the explicit `dir`, `$CHTYPES_REGISTRY`, the
|
|
61
|
+
per-user cache (`defaultRegistryDir()`), then the reserved system locations
|
|
62
|
+
`/usr/local/share/chtypes/artifacts/<os>-<arch>` and `/opt/chtypes/artifacts/<os>-<arch>`.
|
|
63
|
+
The first directory holding artifacts is loaded at construction; a line it
|
|
64
|
+
lacks is loaded from the first later directory that has it when first asked
|
|
65
|
+
for (`registry.searchPath` lists every candidate; a named directory — the
|
|
66
|
+
argument or the variable — that does not exist is an error naming it, never a
|
|
67
|
+
silent fallback). For each artifact the
|
|
68
|
+
loader dlopens the file `manifest.json#library` names, asks the library to name
|
|
69
|
+
itself (`chs_clickhouse_version()` — nothing is inferred from paths), and
|
|
70
|
+
checks the ABI revision: `chs_abi_revision()` must equal this binding's
|
|
71
|
+
`ABI_REVISION` (currently 4). A different nonzero revision is **refused** —
|
|
72
|
+
calling through mismatched declarations is undefined; `0` means the artifact
|
|
73
|
+
predates the probe and individual missing symbols degrade to
|
|
74
|
+
`UnsupportedError` at call time, never to a load failure.
|
|
75
|
+
|
|
76
|
+
## Artifacts: fetch, verify, autofetch
|
|
77
|
+
|
|
78
|
+
The fetch/verify contract every SDK implements identically is
|
|
79
|
+
[`docs/fetch.md`](../docs/fetch.md); this package carries it as a `bin` and
|
|
80
|
+
as a function.
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
npx @wavehouse/chtypes fetch 25.8 # one line, verified, into the per-user cache
|
|
84
|
+
npx @wavehouse/chtypes fetch --all # every line the release publishes for this host
|
|
85
|
+
npx @wavehouse/chtypes fetch 25.8 --platform linux-arm64 --dest /opt/chtypes/artifacts/linux-arm64
|
|
86
|
+
npx @wavehouse/chtypes fetch 25.8 --lock chtypes.lock # record what was installed (file + sha256)
|
|
87
|
+
npx @wavehouse/chtypes fetch 25.8 --frozen # refuse anything chtypes.lock does not pin
|
|
88
|
+
npx @wavehouse/chtypes verify # re-hash every installed line against its manifest
|
|
89
|
+
npx @wavehouse/chtypes list # what is installed, and what the release offers
|
|
90
|
+
npx @wavehouse/chtypes where # the registry directory fetch writes to
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`fetch` prints the installed directory alone on stdout (progress on stderr),
|
|
94
|
+
so `dir="$(npx @wavehouse/chtypes fetch 25.8)"` composes. Exit codes: 0 ok ·
|
|
95
|
+
1 verification failed · 2 usage · 3 source unreachable · 4 not published for
|
|
96
|
+
this platform/line. A line (`25.8`) resolves to the one patch the release
|
|
97
|
+
publishes for it; an exact patch (`25.8.28.1-lts`) is a hard requirement.
|
|
98
|
+
Sources: `--tag v1.2.0` picks a frozen release on `CHTYPES_ARTIFACTS_URL`
|
|
99
|
+
(default `https://artifacts.wavehouse.dev`; the rolling `artifacts` release
|
|
100
|
+
otherwise); `--url <base>` is any other base — a mirror, a `file://` path or
|
|
101
|
+
a plain directory. `--offline` never touches the source: installed and
|
|
102
|
+
verified is fine, anything else is `CHTYPES_SOURCE_UNREACHABLE`. Fetch writes
|
|
103
|
+
to the first of `--dest`, `$CHTYPES_REGISTRY`, the per-user cache; never to a
|
|
104
|
+
system location.
|
|
105
|
+
|
|
106
|
+
**The verification chain**, in order — nothing else is a verdict, not an exit
|
|
107
|
+
code, not a `Content-Length`: (0) `SHA256SUMS.sig`, an ed25519 signature over
|
|
108
|
+
the bytes of `SHA256SUMS`, verified with the embedded release key
|
|
109
|
+
(`RELEASE_PUBLIC_KEYS`, key id `deb275922dbff76e`) or with the keys in
|
|
110
|
+
`CHTYPES_TRUSTED_KEYS=<hex>[,<hex>…]`, which *replace* it — an unsigned or
|
|
111
|
+
mis-signed release is `CHTYPES_ARTIFACT_UNTRUSTED` and nothing is downloaded
|
|
112
|
+
around it (`CHTYPES_ALLOW_UNSIGNED=1` skips this step with one loud warning
|
|
113
|
+
naming the source, never silently); (1) `index.json` names the asset and its
|
|
114
|
+
sha256; (2) the now-authentic `SHA256SUMS` must agree; (3) the tarball is
|
|
115
|
+
hashed before it is unpacked; (4) `manifest.json` inside must agree with the
|
|
116
|
+
index, and the installed library is re-hashed in place after the atomic
|
|
117
|
+
rename. Any mismatch is `CHTYPES_ARTIFACT_CORRUPT` and nothing is installed.
|
|
118
|
+
Installed-and-verified is a no-op (`--force` re-downloads). Once files are in
|
|
119
|
+
a registry directory the loader trusts the directory, as a runtime trusts
|
|
120
|
+
`node_modules` — pass `verifyChecksums: true` to re-hash at load.
|
|
121
|
+
|
|
122
|
+
The same thing as a function:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import { ensure } from '@wavehouse/chtypes';
|
|
126
|
+
|
|
127
|
+
const r = await ensure('25.8'); // idempotent; r.dir is <registry>/25.8, r.installed says whether bytes moved
|
|
128
|
+
await ensure('25.8', { lock: 'chtypes.lock', frozen: true }); // CHTYPES_ARTIFACT_PINNED on drift
|
|
129
|
+
await ensure('25.8', { url: 'file:///srv/mirror', trustedKeys: [hex] }); // a mirror signed by someone else
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Every flag is a property of `EnsureOptions` (`dest`, `platform`, `tag`, `url`,
|
|
133
|
+
`lock`, `frozen`, `force`, `offline`, `trustedKeys`, `allowUnsigned`,
|
|
134
|
+
`onProgress`, `signal`); `ensureAll`, `verifyInstalled` and `listArtifacts`
|
|
135
|
+
are `--all`, `verify` and `list`. Concurrent `ensure`s of one line in one
|
|
136
|
+
process share one fetch.
|
|
137
|
+
|
|
138
|
+
**Lazy fetch on first open is opt-in** — `new Registry(dir, { autofetch: true })`
|
|
139
|
+
or `CHTYPES_AUTOFETCH=1` — because a production process must not begin a
|
|
140
|
+
250 MB download inside a request. `Registry#for()` is synchronous and never
|
|
141
|
+
fetches; `Registry#open()` is its async twin: with autofetch on, a line no
|
|
142
|
+
directory on the search path holds is fetched through `ensure` first (into the
|
|
143
|
+
directory fetch writes to, once per process per line even under concurrent
|
|
144
|
+
opens), then loaded.
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
const registry = new Registry(undefined, { autofetch: true, fetch: { tag: 'v1.2.0' } });
|
|
148
|
+
const lib = await registry.open('25.8'); // fetched on first use, loaded from then on
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**The one error.** A line no directory on the search path holds is an
|
|
152
|
+
`ArtifactMissingError` — a `RegistryError` with `code === 'CHTYPES_ARTIFACT_MISSING'`
|
|
153
|
+
and this message, verbatim apart from the bracketed parts:
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
chtypes: no artifact for ClickHouse <line> (<os>-<arch>). Looked in: <dir1>, <dir2>, ….
|
|
157
|
+
Install it: npx @wavehouse/chtypes fetch <line>
|
|
158
|
+
or set CHTYPES_AUTOFETCH=1 to fetch on first use.
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The fetch verdicts are `FetchError` subclasses carrying the shared codes:
|
|
162
|
+
`ArtifactUntrustedError` (`CHTYPES_ARTIFACT_UNTRUSTED`), `ArtifactCorruptError`
|
|
163
|
+
(`CHTYPES_ARTIFACT_CORRUPT`), `ArtifactPinnedError` (`CHTYPES_ARTIFACT_PINNED`),
|
|
164
|
+
`ArtifactUnpublishedError` (`CHTYPES_ARTIFACT_UNPUBLISHED`) and
|
|
165
|
+
`SourceUnreachableError` (`CHTYPES_SOURCE_UNREACHABLE`). Artifacts are Elastic
|
|
166
|
+
License 2.0, separate from this package's Apache 2.0; `LICENSE` and `NOTICE`
|
|
167
|
+
ship in every release and `fetch` says so once.
|
|
168
|
+
|
|
169
|
+
## Quickstart
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
import { Format, Registry, UnsupportedError } from '@wavehouse/chtypes';
|
|
173
|
+
|
|
174
|
+
const registry = new Registry(); // $CHTYPES_REGISTRY, else the per-user cache
|
|
175
|
+
const lib = registry.for('25.8'); // minor line or exact patch both resolve
|
|
176
|
+
|
|
177
|
+
// Compile under the deployment's declared settings profile — a type gate
|
|
178
|
+
// declared here binds at compile, exactly as that server's CREATE TABLE does.
|
|
179
|
+
using schema = lib.compileDdl('id LowCardinality(UInt32), x UInt8, ts DateTime DEFAULT now()', {
|
|
180
|
+
settings: { allow_suspicious_low_cardinality_types: '1' },
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const good = schema.row(Format.JSONEachRow, Buffer.from('{"id": 7, "x": 256}'));
|
|
184
|
+
console.log(good.outcome); // accepted
|
|
185
|
+
console.log(good.transformed[0]?.reason); // overflow_wrap — 256 stored as 0, silently
|
|
186
|
+
console.log(good.substituted[0]?.column); // ts — send it explicitly in the real INSERT
|
|
187
|
+
|
|
188
|
+
const bad = schema.row(Format.JSONEachRow, Buffer.from('{"id": 7, "x": '));
|
|
189
|
+
console.log(bad.outcome, bad.errCode, bad.errMsg); // rejected <ClickHouse's own code + message>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
A bad **row** is never an exception — the verdict is `RowResult#outcome`
|
|
193
|
+
(`accepted` | `rejected` | `accepted_poisoned` | `unsupported`). Exceptions are
|
|
194
|
+
for schema-level answers (`compileDdl`, `setEngine`, `setTtl`,
|
|
195
|
+
`validateType`), and there the class *is* the verdict: `SchemaError` = the
|
|
196
|
+
server refused (a real ClickHouse code), `UnsupportedError` = chtypes declines
|
|
197
|
+
to guess — fall back to the server. The two are peers; a decline never
|
|
198
|
+
satisfies `instanceof SchemaError`.
|
|
199
|
+
|
|
200
|
+
## API reference
|
|
201
|
+
|
|
202
|
+
Callables, mapped to the C ABI (`spec/c-abi.md`; `spec/bindings.md` is the
|
|
203
|
+
cross-SDK shape):
|
|
204
|
+
|
|
205
|
+
| Symbol | C function | Params | Returns | Errors |
|
|
206
|
+
|---|---|---|---|---|
|
|
207
|
+
| `new Registry(dir?, options?)` | per artifact: dlopen, `chs_clickhouse_version`, `chs_abi_revision`, `chs_init` | registry dir — the head of the search path (it, `$CHTYPES_REGISTRY`, the per-user cache, the system locations); `{timezone='UTC', verifyChecksums, autofetch, fetch}` | `Registry` | `RegistryError` (a named directory that does not exist, no artifacts anywhere, unreadable directory, checksum, manifest mismatch, load failure); `ChtypesError` (ABI-revision mismatch, `chs_init` failure) |
|
|
208
|
+
| `Registry#versions()` | — derived | — | minor lines, oldest first | never throws |
|
|
209
|
+
| `Registry#libraries()` | — derived | — | every loaded `Library`, in release order | never throws |
|
|
210
|
+
| `Registry#for(version)` | — derived (lookup; loads a line from a later search-path directory on demand) | minor line or exact patch | `Library` | `ArtifactMissingError` (a `RegistryError`, `code` `CHTYPES_ARTIFACT_MISSING`, names every directory looked in); `RegistryError` (a directory holds the line but it does not load); never a nearest-version fallback, never a fetch |
|
|
211
|
+
| `Registry#open(version)` | — `for()` behind a promise; with `autofetch`, `ensure` first | minor line or exact patch | `Promise<Library>` | `ArtifactMissingError` (autofetch off); the `FetchError` verdicts; `RegistryError` (fetched, does not load) |
|
|
212
|
+
| `Registry#has(version)` | — derived | version string | `boolean` | never throws |
|
|
213
|
+
| `Registry#close()` / `Symbol.dispose` | `chs_shutdown` per library | — | — | see `Library#shutdown` |
|
|
214
|
+
| `Library#validateType(expr)` | `chs_validate_type` | type expression | canonical spelling (use verbatim) | `SchemaError` (server refusal, e.g. 50); `UnsupportedError` (unsafe family / missing symbol) |
|
|
215
|
+
| `Library#compileDdl(ddl, {settings, mode}?)` | `chs_schema_compile` + the six `chs_schema_column_*` getters | column-declaration list (not a CREATE TABLE); optional declared profile; `mode` default `CompileMode.Declared` | `Schema` | `SchemaError` (positive code: bad DDL, unknown setting name 115, declared gate refusing 455/44, Enum-DEFAULT domain 691); `UnsupportedError` (mode ≠ 0, refused DEFAULT, admission budget) |
|
|
216
|
+
| `Library#hasCompileSettings()` | probe via `chs_schema_compile` | — | `boolean` (true on every artifact this repo builds) | passes through unexpected errors |
|
|
217
|
+
| `Library#setDefaultSettings(settings)` | `chs_set_default_settings` | process-wide seed | — | `ChtypesError` (refused wholesale with the server's 115 + hint; reentrancy; JS `number` value); `UnsupportedError` (missing symbol) |
|
|
218
|
+
| `Library#referenceType(expr)` | `chs_reference_type` | type expression | widened reference type, `''` if none | `UnsupportedError` (missing symbol) |
|
|
219
|
+
| `Library#registeredFamilies()` | `chs_registered_families` | — | family names | `UnsupportedError` (missing symbol) |
|
|
220
|
+
| `Library#functionFlags()` | `chs_function_flags` | — | the volatility TSV audit, verbatim | `UnsupportedError` (missing symbol) |
|
|
221
|
+
| `Library#shutdown()` | `chs_shutdown` | — | — | `ChtypesError` (reentrancy); missing symbol ignored |
|
|
222
|
+
| `Schema#columns` | `chs_schema_column_count/_name/_type/_default_kind/_default_expr/_default_is_literal` (at compile) | — | `ColumnInfo[]`, canonicalised, declaration order | — |
|
|
223
|
+
| `Schema#setEngine(engine, orderBy, {mergeTreeSettings}?)` | `chs_schema_engine` | engine, sorting key, MergeTree-namespace `SETTINGS` | — | sign of rc decides: `SchemaError` (rc > 0 — server refused, today 115 unknown MergeTree setting name); `UnsupportedError` (rc < 0 — unmodelled engine/key, non-default MergeTree value, guarded exception, missing symbol) |
|
|
224
|
+
| `Schema#setTtl(ttl)` | `chs_schema_ttl` | rows-TTL expression | — | `UnsupportedError` (any nonzero rc: refused TTL form, guarded exception, missing symbol) |
|
|
225
|
+
| `Schema#row(format, raw, settings?)` | `chs_row` | format code, row **bytes**, per-call settings | `RowResult` (verdict in `outcome`, never thrown) | `ChtypesError` (closed schema, `number` setting); `UnsupportedError` (missing symbol) |
|
|
226
|
+
| `Schema#rows(format, body, settings?)` | `chs_rows` | format code, body **bytes**, per-call settings | `BatchResult` (`engineRows` is the stored truth when present) | `ChtypesError` (closed schema, `number` setting); `UnsupportedError` (missing symbol — same degradation as `row`) |
|
|
227
|
+
| `Schema#compileFilter(expr, {params}?)` | `chs_filter_compile` | one boolean expression; `params` binds `{name:Type}` — values are STRINGS, never hand-escaped | `Filter` (WHERE-side semantics by construction) | `SchemaError` (47 unknown identifier, **456** unbound param, **457** unparseable param value); `UnsupportedError` (clock reads, missing symbol) |
|
|
228
|
+
| `Filter#rows(format, body, settings?)` | `chs_filter_rows` | same contract as `Schema#rows` | `FilterResult` — per-row verdicts; `'error'`/`'decline'` are NOT answers, fail closed | `ChtypesError` (closed filter, `number` setting) |
|
|
229
|
+
| `Schema#parseBlock(format, body, settings?)` | `chs_block_parse` | parse a body ONCE; per-row parse failures live IN the block | `Block`, evaluable by K filters with no re-parse | `SchemaError` (115 unknown setting, framing, decode fault — no block, no partial answers); `UnsupportedError` (missing symbol) |
|
|
230
|
+
| `Filter#eval(block)` | `chs_filter_eval` | an already-parsed `Block` | the same `FilterResult` `rows` answers — `eval(parseBlock(body))` ≡ `rows(body)`; a cross-schema pair answers `'rejected'`/1002 | `ChtypesError` (closed handles, cross-library pair) |
|
|
231
|
+
| `Filter#close()` / `Block#close()` / `Symbol.dispose` | `chs_filter_free` / `chs_block_free` | — | — (`Schema#close` frees open filters and blocks FIRST — the C-required order) | never throw; idempotent |
|
|
232
|
+
| `Schema#close()` / `Symbol.dispose` | `chs_schema_free` | — | — | never throws; idempotent |
|
|
233
|
+
| `minorOf(version)` | — derived | version string | minor line | never throws |
|
|
234
|
+
| `compareMinor(a, b)` | — derived | two minor lines | numeric order (25.10 > 25.8) | never throws |
|
|
235
|
+
| `ensure(line, opts?)` / `ensureAll(opts?)` | — no C call (the `docs/fetch.md` chain) | line or exact patch; `EnsureOptions` | `Promise<EnsureResult>` / `Promise<EnsureResult[]>` | `ArtifactUntrustedError`, `ArtifactCorruptError`, `ArtifactPinnedError`, `ArtifactUnpublishedError`, `SourceUnreachableError` (each with its `code`); `ChtypesError` (bad spelling, platform key, conflicting options) |
|
|
236
|
+
| `verifyInstalled(dest?, platform?)` / `listArtifacts(opts?)` | — no C call | registry dir / `EnsureOptions` | `Promise<InstalledArtifact[]>` / `Promise<ListResult>` | `verifyInstalled` never throws; `listArtifacts` the `FetchError` verdicts unless `offline` |
|
|
237
|
+
| `registrySearchPath(explicit?, platform?)` / `fetchDestination(explicit?, platform?)` / `hostPlatform()` | — derived (`docs/fetch.md` §1) | path/env probing | `string[]` / `string` / `string` | never throw |
|
|
238
|
+
| `resolveRegistryDir(explicit?)` / `defaultRegistryDir()` / `looksLikeRegistry(dir)` | — derived | path/env probing | resolved dir / `string` / `boolean` | never throw |
|
|
239
|
+
| `formatName(f)` | — derived | format code | constant name | never throws |
|
|
240
|
+
| `encodeSettings(settings?)` | — derived | settings map | `settings_json` text | `ChtypesError` (a JS `number` value) |
|
|
241
|
+
| `parseVersionResult` / `parseChangedSettingsResult` / `parseColumnsResult` / `reconstructDdl` | — no C call (discovery kit) | query result bytes / discovered columns | version / settings map / columns / DDL string | `ChtypesError` on malformed input |
|
|
242
|
+
| `isLossyReason(reason)` | — derived | reason string | `boolean` (false for exactly 4 reasons) | never throws |
|
|
243
|
+
| `parseDocument` / `parseJsonValue` / `repairBareDenormals` / `rawBytes` / `rawText` / `isValidUtf8` | — derived (byte-exact JSON kit) | bytes | `Json` / `Json\|null` / repaired bytes / `Buffer` / `string` / `boolean` | `parseDocument` throws `ChtypesError` on a malformed document; the rest never throw |
|
|
244
|
+
| `nativeStats()` | — diagnostic | — | `{stringsTaken, stringsFreed}` | never throws |
|
|
245
|
+
|
|
246
|
+
Types and constants: `Format` (the `chs_format` integers 0–9, frozen),
|
|
247
|
+
`CompileMode.Declared` (= C `CHS_COMPILE_DECLARED` = 0), `CODE_UNSUPPORTED`
|
|
248
|
+
(−2, the wire sentinel — carried by results, never by an error object),
|
|
249
|
+
`ABI_REVISION` (4), `Outcome`, `RowResult`, `BatchResult`, `FilterResult`,
|
|
250
|
+
`Verdict`, `Value`, `Transform`, `Substitution`, `Computed`, `ColumnDoc`,
|
|
251
|
+
`ColumnInfo`, `EngineOptions`, `CompileOptions`, `CompileFilterOptions`,
|
|
252
|
+
`Settings`, `SettingValue`, `Reason`, `ServerProfile`, `DiscoveredColumn`,
|
|
253
|
+
`Manifest`, `RegistryOptions`, `EnsureOptions`, `EnsureResult`, `FetchEvent`,
|
|
254
|
+
`InstalledArtifact`, `ListResult`, `LockFile`, `ArtifactErrorCode`,
|
|
255
|
+
`RELEASE_PUBLIC_KEYS`, `Json`, `JsonKind`, and the error classes
|
|
256
|
+
`ChtypesError` (base), `RegistryError`, `ArtifactMissingError` (a
|
|
257
|
+
`RegistryError` with `code`), `FetchError` and its five verdict subclasses,
|
|
258
|
+
`SchemaError`, `UnsupportedError` (a **peer** of `SchemaError`, deliberately
|
|
259
|
+
not a subclass).
|
|
260
|
+
|
|
261
|
+
## Filters, query parameters and the block twin (ABI revisions 3–4)
|
|
262
|
+
|
|
263
|
+
`Schema#compileFilter(expr)` compiles one boolean expression against the
|
|
264
|
+
schema's physical columns and `Filter#rows(...)` answers per-row verdicts
|
|
265
|
+
with **WHERE-side** semantics (`x = 256` over `UInt8` promotes — false for
|
|
266
|
+
every row — it never wraps), computed by ClickHouse's own comparison
|
|
267
|
+
functions. Four verdicts: `'true'`/`'false'` are answers; `'error'` (the
|
|
268
|
+
predicate threw — a real server fails the whole query) and `'decline'` (this
|
|
269
|
+
library declines) are NOT, and an enforcing caller MUST fail closed on both.
|
|
270
|
+
**Enforcement gate**: no read-side security may be enforced on this surface
|
|
271
|
+
until the WHERE-truth rig gates green — until then it is shadow/replay only
|
|
272
|
+
(`spec/c-abi.md` §Filters).
|
|
273
|
+
|
|
274
|
+
**Params (revision 4).** The expression may contain `{name:Type}` query
|
|
275
|
+
parameters, bound with `compileFilter(expr, { params: { name: 'value' } })`
|
|
276
|
+
— values are **strings**, exactly as the server's own parameter channels
|
|
277
|
+
carry them. Substitution is the server's own `ReplaceQueryParameterVisitor`:
|
|
278
|
+
each value is deserialized by the DECLARED type's own reader and injected as
|
|
279
|
+
a typed literal AFTER SQL parsing, so a value is never SQL text and **must
|
|
280
|
+
never be hand-escaped into the expression** — injection safety is by
|
|
281
|
+
construction, and a hostile value (`' OR 1=1 --`) compares as exactly that
|
|
282
|
+
literal. An UNBOUND parameter is the server's own **456** ("Substitution
|
|
283
|
+
`name` is not set"), an unparseable value the server's own **457** — both
|
|
284
|
+
`SchemaError`, verbatim; a bound name the expression never uses is ignored.
|
|
285
|
+
The compiled handle bakes the values in — identity is per
|
|
286
|
+
(schema, expr, params) — so **a caller compiling filters from
|
|
287
|
+
tenant-influenced values MUST bound its cache and its compile rate**: a
|
|
288
|
+
bounded LRU keyed on (schema generation, expr, params-hash) plus a
|
|
289
|
+
per-principal compile throttle; an unbounded cache is a memory DoS and an
|
|
290
|
+
unmetered compile path is a CPU DoS (`spec/c-abi.md` §Filters, normative).
|
|
291
|
+
|
|
292
|
+
**Choose the brace type for the value's domain.** A bound value is
|
|
293
|
+
deserialized by the brace type's OWN reader (`deserializeTextEscaped`),
|
|
294
|
+
which **wraps** an out-of-domain integer — `{p:UInt8}` given `"256"` binds
|
|
295
|
+
`0` and matches every genuine zero (measured, uniform 24.8–26.7,
|
|
296
|
+
server-matched) — while the same constant written as a literal in the
|
|
297
|
+
expression PROMOTES (`x = 256` over `UInt8` is simply never true). A
|
|
298
|
+
too-narrow parameter type therefore silently matches the wrong rows: size
|
|
299
|
+
the type for the tenant-supplied domain (`{p:UInt64}`, `{p:String}`) or
|
|
300
|
+
validate the value before binding it. Malformed integer spellings refuse
|
|
301
|
+
loudly with the server's own **457** (`"-1"`, `"+7"`, `"007"` as `UInt8`);
|
|
302
|
+
an empty string refuses with **32**. If one name is bound twice at the C
|
|
303
|
+
boundary, the LAST binding wins — the server's own `insert_or_assign` rule
|
|
304
|
+
(unreachable through this SDK's unique-keyed object, stated for
|
|
305
|
+
completeness).
|
|
306
|
+
|
|
307
|
+
One naming trap, the transport's rather than this library's: on a real
|
|
308
|
+
server's **TCP** channel a parameter *named* `limit` or `offset` fails at
|
|
309
|
+
the protocol layer with code 26 even when unused (params ride in a Settings
|
|
310
|
+
block there); HTTP is fine, and this library matches the HTTP/substitution
|
|
311
|
+
semantics. Avoid those two names for anything that will ever cross TCP.
|
|
312
|
+
|
|
313
|
+
**The block twin (revision 4).** `Schema#parseBlock(format, body, settings)`
|
|
314
|
+
parses a body ONCE into a `Block` — the parse half of `Filter#rows` — and
|
|
315
|
+
`Filter#eval(block)` answers the same `FilterResult` with no re-parse: the
|
|
316
|
+
live-SSE hot path is K filters × 1 event, and the re-parse is shed.
|
|
317
|
+
Normative equivalence: `eval(parseBlock(body))` ≡ `rows(body)` for every
|
|
318
|
+
verdict class (volatile DEFAULTs resolve against the PARSE call's clock
|
|
319
|
+
instant — pin `chtypes_now_epoch_nanos` for cross-call identity).
|
|
320
|
+
Evaluation is a pure function of (filter, block), takes no settings, and
|
|
321
|
+
never consumes or mutates the block. Filter and block MUST come from the
|
|
322
|
+
SAME schema: a mismatched same-library pair answers `'rejected'`/1002
|
|
323
|
+
loudly, and a cross-library pair throws `ChtypesError` before any C call. A
|
|
324
|
+
`Block` follows the filter's lifetime rules exactly (`Schema#close` frees
|
|
325
|
+
open blocks first, and `using` nests naturally: block, filter, schema).
|
|
326
|
+
Parse-once does not open enforcement earlier: the twin is under the same
|
|
327
|
+
enforcement gate.
|
|
328
|
+
|
|
329
|
+
## Settings and precedence
|
|
330
|
+
|
|
331
|
+
Settings ride four channels; on the row path the leftmost channel that names a
|
|
332
|
+
setting wins:
|
|
333
|
+
|
|
334
|
+
per-call map > handle compile profile > library defaults (setDefaultSettings) > ClickHouse defaults
|
|
335
|
+
|
|
336
|
+
One documented exception: a **type gate** named in the compile profile
|
|
337
|
+
(`allow_suspicious_low_cardinality_types`, `allow_experimental_json_type`, …)
|
|
338
|
+
binds at compile and then **outranks the per-call map** for that handle's life
|
|
339
|
+
— exactly as a real server checks a gate at CREATE and never re-reads it per
|
|
340
|
+
INSERT.
|
|
341
|
+
|
|
342
|
+
Rules that cost real bugs:
|
|
343
|
+
|
|
344
|
+
- **Values are strings (or bigints).** A JS `number` is rejected at runtime:
|
|
345
|
+
`chtypes_now_epoch_nanos` is a 19-digit nanosecond epoch, which does not
|
|
346
|
+
survive an IEEE double — sent as a JSON number the setting is *silently
|
|
347
|
+
ignored*. Use `BigInt(…)` or a string; `bigint` crosses via `toString()`.
|
|
348
|
+
- **An unknown setting name rejects the whole call with the server's own
|
|
349
|
+
code 115** (with its did-you-mean hint), on every channel.
|
|
350
|
+
`setDefaultSettings` refuses **wholesale** — nothing committed.
|
|
351
|
+
- The six `chtypes_*` keys are the whole reserved namespace; anything else
|
|
352
|
+
spelled `chtypes_*` is an unknown name (115). Per-call:
|
|
353
|
+
`chtypes_now_epoch_nanos` (pin the batch instant),
|
|
354
|
+
`chtypes_clock_offset_nanos` (measured server−client offset),
|
|
355
|
+
`chtypes_max_clock_skew_nanos` (refuse volatile-DEFAULT substitution past
|
|
356
|
+
the budget). Process-wide, via `setDefaultSettings` only:
|
|
357
|
+
`chtypes_default_eval_memory_bytes` (default 256 MiB),
|
|
358
|
+
`chtypes_default_eval_wall_nanos` (default 1 s),
|
|
359
|
+
`chtypes_custom_settings_prefixes` (mirror of the server's
|
|
360
|
+
`custom_settings_prefixes`; ships as `SQL_`).
|
|
361
|
+
- A row whose `unsupportedSettings` list is non-empty is promoted to
|
|
362
|
+
`unsupported` (a KNOWN name whose value this build declines) — never scored
|
|
363
|
+
as agreement.
|
|
364
|
+
|
|
365
|
+
## Discovery
|
|
366
|
+
|
|
367
|
+
**The library never connects to ClickHouse.** It ships three SQL query
|
|
368
|
+
constants; your app runs them at connect time with whatever client it already
|
|
369
|
+
has, and declares what it learned. Never ask the customer — ask their server.
|
|
370
|
+
|
|
371
|
+
| constant | feeds |
|
|
372
|
+
|---|---|
|
|
373
|
+
| `QUERY_SERVER_VERSION` | `parseVersionResult` → `registry.for(version)` |
|
|
374
|
+
| `QUERY_CHANGED_SETTINGS` | `parseChangedSettingsResult` → the compile profile AND per-call settings |
|
|
375
|
+
| `QUERY_TABLE_COLUMNS` | `parseColumnsResult` → `reconstructDdl` → `compileDdl` (send `param_db`/`param_table`) |
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
const profile: ServerProfile = {
|
|
379
|
+
version: parseVersionResult(await run(QUERY_SERVER_VERSION)),
|
|
380
|
+
settings: parseChangedSettingsResult(await run(QUERY_CHANGED_SETTINGS)),
|
|
381
|
+
};
|
|
382
|
+
// cache per deployment/tenant, then:
|
|
383
|
+
const lib = registry.for(profile.version);
|
|
384
|
+
const schema = lib.compileDdl(ddl, { settings: profile.settings }); // CREATE-time
|
|
385
|
+
const res = schema.rows(Format.JSONEachRow, body, profile.settings); // per-call
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
The parsers read the JSONEachRow bytes through this package's own byte-exact
|
|
389
|
+
reader — `position` survives quoted and bare spellings without a float
|
|
390
|
+
round-trip, and `default_kind`/`default_expression` are carried (dropping them
|
|
391
|
+
silently loses DEFAULT/MATERIALIZED semantics). A typo'd setting name in the
|
|
392
|
+
declared profile fails the **compile** with the server's own 115 — caught at
|
|
393
|
+
declare time, not swallowed.
|
|
394
|
+
|
|
395
|
+
## Multi-version use and thread-safety
|
|
396
|
+
|
|
397
|
+
- One `Registry` loads every version side by side. Each artifact is dlopened
|
|
398
|
+
`RTLD_LOCAL` (that is the whole mechanism letting two builds that both
|
|
399
|
+
define `DB::DataTypeFactory` share a process); cost ≈ 120 MB resident per
|
|
400
|
+
loaded version.
|
|
401
|
+
- `for()` resolves a minor line (`"25.8"`) or an exact patch
|
|
402
|
+
(`"25.8.28.1-lts"`); an unknown patch inside a loaded minor resolves to the
|
|
403
|
+
line. Failure is `ArtifactMissingError`, naming every directory looked in
|
|
404
|
+
— never a fallback to the nearest version. Version behaviour is
|
|
405
|
+
non-monotonic; never infer one version's answer from another's.
|
|
406
|
+
- Every call is **synchronous on the JS thread**; ordinary single-threaded
|
|
407
|
+
Node code needs no locking. `setDefaultSettings` and `shutdown` carry a
|
|
408
|
+
reentrancy tripwire (the `NativeLibrary#inCall` counter) and refuse loudly
|
|
409
|
+
if another chtypes call is on the stack — they replace process-global state
|
|
410
|
+
the row path reads by reference.
|
|
411
|
+
- **`worker_threads` is the boundary.** Two JS threads share one dlopen'd
|
|
412
|
+
image and one set of C globals, which no per-isolate counter can see. Seed
|
|
413
|
+
default settings BEFORE starting workers, or serialise the seed yourself;
|
|
414
|
+
do not share a `Schema` across workers.
|
|
415
|
+
- Teardown: `registry.close()` / `library.shutdown()` (or `using` /
|
|
416
|
+
`Symbol.dispose`) join the DEFAULT evaluator's background threads —
|
|
417
|
+
required before any explicit unload; an ordinary process is covered by
|
|
418
|
+
`atexit`.
|
|
419
|
+
|
|
420
|
+
## Known limitations
|
|
421
|
+
|
|
422
|
+
- The error model's third arm is deliberate: `unsupported` means "a real
|
|
423
|
+
server might well have accepted this; chtypes will not guess" — fall back
|
|
424
|
+
to the server (`spec/c-abi.md` §Error model). Unmodelled engines, sorting
|
|
425
|
+
keys and TTL forms (WHERE/GROUP BY TTLs, TO DISK/VOLUME, RECOMPRESS,
|
|
426
|
+
clock-reading TTLs) decline rather than guess.
|
|
427
|
+
- **EPHEMERAL columns:** a format stream carries no column list, so an INSERT
|
|
428
|
+
that names an EPHEMERAL column in an explicit column list cannot be
|
|
429
|
+
previewed here — detect via `Schema#columns` (`defaultKind ===
|
|
430
|
+
'EPHEMERAL'`) and decline that intersection, never mispreview
|
|
431
|
+
(`spec/bindings.md` §EPHEMERAL).
|
|
432
|
+
- **macOS artifacts are a dev floor, not an oracle:** `long double` is
|
|
433
|
+
53-bit, so float parses diverge from real servers (Linux matches 395/395 of
|
|
434
|
+
the float corpus; macOS 0/395). Float expectations must come from a Linux
|
|
435
|
+
artifact or a live server.
|
|
436
|
+
- Everything here is **insert-side** coercion. Never fold a `WHERE`-clause
|
|
437
|
+
constant through `row`/`rows` — comparison rules differ; refuse an operand
|
|
438
|
+
outside the column type's domain instead (`spec/bindings.md` §Constants are
|
|
439
|
+
not payloads).
|
|
440
|
+
- Binary formats (`RowBinary` family, `Native`, `Buffers`) parse only on
|
|
441
|
+
artifacts new enough to carry the reader — probe the artifact; earlier ones
|
|
442
|
+
answer 73/117 exactly as their servers do.
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `chtypes` — the one CLI surface every SDK carries (docs/fetch.md §6),
|
|
4
|
+
* spelled identically in all four:
|
|
5
|
+
*
|
|
6
|
+
* chtypes fetch <line>... [--all] [--platform <os-arch>] [--dest <dir>]
|
|
7
|
+
* [--tag <t> | --url <base>] [--lock <file>] [--frozen]
|
|
8
|
+
* [--force] [--offline]
|
|
9
|
+
* chtypes verify [--dest <dir>] re-hash every installed line against its manifest
|
|
10
|
+
* chtypes list [--dest <dir>] what is installed, and what the release offers
|
|
11
|
+
* chtypes where the registry directory fetch would write to
|
|
12
|
+
*
|
|
13
|
+
* Exit codes: 0 ok · 1 verification failed · 2 usage · 3 source unreachable ·
|
|
14
|
+
* 4 not published for this platform/line. Progress goes to stderr; `fetch`
|
|
15
|
+
* prints the installed directory alone on stdout, so it composes:
|
|
16
|
+
*
|
|
17
|
+
* dir="$(npx @wavehouse/chtypes fetch 25.8)"
|
|
18
|
+
*
|
|
19
|
+
* `runCli` is exported so the suite can drive the command in-process; the
|
|
20
|
+
* file runs `main` only when it is the process's entry point (the `bin`).
|
|
21
|
+
*/
|
|
22
|
+
/** The §6 exit codes. */
|
|
23
|
+
export declare const EXIT: {
|
|
24
|
+
readonly ok: 0;
|
|
25
|
+
readonly verificationFailed: 1;
|
|
26
|
+
readonly usage: 2;
|
|
27
|
+
readonly sourceUnreachable: 3;
|
|
28
|
+
readonly unpublished: 4;
|
|
29
|
+
};
|
|
30
|
+
/** Where the command writes; the suite captures both. */
|
|
31
|
+
export interface CliIo {
|
|
32
|
+
readonly stdout: (text: string) => void;
|
|
33
|
+
readonly stderr: (text: string) => void;
|
|
34
|
+
/** Whether stderr is a terminal (progress is redrawn in place only then). */
|
|
35
|
+
readonly tty?: boolean | undefined;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Run the command with `argv` (the arguments after the program name) and
|
|
39
|
+
* return its exit code. Never throws for a user-facing failure: every §7
|
|
40
|
+
* verdict is printed to stderr and mapped to its exit code.
|
|
41
|
+
*/
|
|
42
|
+
export declare function runCli(argv: readonly string[], io?: CliIo): Promise<number>;
|
|
43
|
+
//# sourceMappingURL=cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;GAmBG;AAUH,yBAAyB;AACzB,eAAO,MAAM,IAAI;aACf,EAAE,EAAE,CAAC;aACL,kBAAkB,EAAE,CAAC;aACrB,KAAK,EAAE,CAAC;aACR,iBAAiB,EAAE,CAAC;aACpB,WAAW,EAAE,CAAC;CACN,CAAC;AAEX,yDAAyD;AACzD,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,6EAA6E;IAC7E,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACpC;AAmCD;;;;GAIG;AACH,wBAAsB,MAAM,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,EAAE,GAAE,KAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CAuC5F"}
|