@wavehouse/chtypes 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +202 -0
  3. package/README.md +442 -0
  4. package/dist/cli.d.ts +43 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +303 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/discover.d.ts +134 -0
  9. package/dist/discover.d.ts.map +1 -0
  10. package/dist/discover.js +291 -0
  11. package/dist/discover.js.map +1 -0
  12. package/dist/errors.d.ts +204 -0
  13. package/dist/errors.d.ts.map +1 -0
  14. package/dist/errors.js +228 -0
  15. package/dist/errors.js.map +1 -0
  16. package/dist/fetch.d.ts +235 -0
  17. package/dist/fetch.d.ts.map +1 -0
  18. package/dist/fetch.js +842 -0
  19. package/dist/fetch.js.map +1 -0
  20. package/dist/ffi.d.ts +346 -0
  21. package/dist/ffi.d.ts.map +1 -0
  22. package/dist/ffi.js +913 -0
  23. package/dist/ffi.js.map +1 -0
  24. package/dist/format.d.ts +104 -0
  25. package/dist/format.d.ts.map +1 -0
  26. package/dist/format.js +108 -0
  27. package/dist/format.js.map +1 -0
  28. package/dist/index.d.ts +28 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +28 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/json.d.ts +127 -0
  33. package/dist/json.d.ts.map +1 -0
  34. package/dist/json.js +577 -0
  35. package/dist/json.js.map +1 -0
  36. package/dist/library.d.ts +225 -0
  37. package/dist/library.d.ts.map +1 -0
  38. package/dist/library.js +234 -0
  39. package/dist/library.js.map +1 -0
  40. package/dist/numeric.d.ts +20 -0
  41. package/dist/numeric.d.ts.map +1 -0
  42. package/dist/numeric.js +107 -0
  43. package/dist/numeric.js.map +1 -0
  44. package/dist/paths.d.ts +47 -0
  45. package/dist/paths.d.ts.map +1 -0
  46. package/dist/paths.js +78 -0
  47. package/dist/paths.js.map +1 -0
  48. package/dist/registry.d.ts +204 -0
  49. package/dist/registry.d.ts.map +1 -0
  50. package/dist/registry.js +409 -0
  51. package/dist/registry.js.map +1 -0
  52. package/dist/results.d.ts +326 -0
  53. package/dist/results.d.ts.map +1 -0
  54. package/dist/results.js +235 -0
  55. package/dist/results.js.map +1 -0
  56. package/dist/schema.d.ts +444 -0
  57. package/dist/schema.d.ts.map +1 -0
  58. package/dist/schema.js +448 -0
  59. package/dist/schema.js.map +1 -0
  60. package/dist/settings.d.ts +43 -0
  61. package/dist/settings.d.ts.map +1 -0
  62. package/dist/settings.js +33 -0
  63. package/dist/settings.js.map +1 -0
  64. package/dist/tar.d.ts +32 -0
  65. package/dist/tar.d.ts.map +1 -0
  66. package/dist/tar.js +310 -0
  67. package/dist/tar.js.map +1 -0
  68. package/dist/transform.d.ts +73 -0
  69. package/dist/transform.d.ts.map +1 -0
  70. package/dist/transform.js +527 -0
  71. package/dist/transform.js.map +1 -0
  72. package/package.json +62 -0
package/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"}