@wavehouse/chtypes 0.1.1 → 0.2.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 (51) hide show
  1. package/CHANGELOG.md +43 -38
  2. package/README.md +45 -414
  3. package/dist/cli.d.ts +1 -1
  4. package/dist/cli.js +1 -1
  5. package/dist/discover.d.ts +2 -2
  6. package/dist/discover.js +2 -2
  7. package/dist/errors.d.ts +60 -13
  8. package/dist/errors.d.ts.map +1 -1
  9. package/dist/errors.js +64 -11
  10. package/dist/errors.js.map +1 -1
  11. package/dist/fetch.d.ts +15 -8
  12. package/dist/fetch.d.ts.map +1 -1
  13. package/dist/fetch.js +15 -8
  14. package/dist/fetch.js.map +1 -1
  15. package/dist/ffi.d.ts +15 -15
  16. package/dist/ffi.js +26 -26
  17. package/dist/ffi.js.map +1 -1
  18. package/dist/format.d.ts +5 -5
  19. package/dist/format.js +5 -5
  20. package/dist/index.d.ts +4 -4
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +4 -4
  23. package/dist/index.js.map +1 -1
  24. package/dist/json.d.ts +3 -3
  25. package/dist/json.js +5 -5
  26. package/dist/json.js.map +1 -1
  27. package/dist/library.d.ts +11 -11
  28. package/dist/library.js +9 -9
  29. package/dist/numeric.js +1 -1
  30. package/dist/paths.d.ts +5 -1
  31. package/dist/paths.d.ts.map +1 -1
  32. package/dist/paths.js +7 -3
  33. package/dist/paths.js.map +1 -1
  34. package/dist/registry.d.ts +6 -6
  35. package/dist/registry.d.ts.map +1 -1
  36. package/dist/registry.js +14 -11
  37. package/dist/registry.js.map +1 -1
  38. package/dist/results.d.ts +65 -28
  39. package/dist/results.d.ts.map +1 -1
  40. package/dist/results.js +95 -10
  41. package/dist/results.js.map +1 -1
  42. package/dist/schema.d.ts +23 -22
  43. package/dist/schema.d.ts.map +1 -1
  44. package/dist/schema.js +15 -15
  45. package/dist/schema.js.map +1 -1
  46. package/dist/settings.d.ts +4 -4
  47. package/dist/settings.js +2 -2
  48. package/dist/settings.js.map +1 -1
  49. package/dist/transform.d.ts +2 -2
  50. package/dist/transform.js +5 -5
  51. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,49 +1,54 @@
1
1
  # Changelog — `@wavehouse/chtypes`
2
2
 
3
- All notable changes to the ts binding. The format is
4
- [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this package follows
5
- [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
3
+ All notable changes to the ts binding. The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this package follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
4
 
7
- The four bindings in this repository are released together and give one answer,
8
- so an entry here has a counterpart in the other three.
5
+ The four bindings in this repository are released together and give one answer, so an entry here has a counterpart in the other three.
6
+
7
+ ## [0.2.0] — 2026-09-15
8
+
9
+ ### Changed — BREAKING
10
+
11
+ - **The TypeScript filter verdicts are the wire characters.** `Verdict.True` / `False` / `Error` / `Decline` are now `'t'` / `'f'` / `'e'` / `'d'`, which Go, Python and Rust have always rendered. TypeScript alone spelled them `'true'` / `'false'` / `'error'` / `'decline'`, so two SDKs could not share a log line, a fixture or a test. The four states and the fail-closed rule are unchanged: `'e'` and `'d'` are NOT answers, and a caller enforcing visibility must hide the row or fail the request on both — collapsing either into `false` inverts fail-closed into fail-open, the measured leak class. Only the rendered characters moved. **The parity suites now ENFORCE the shared value in all four bindings**; until this release two of the four merely declared it.
12
+
13
+ - **TypeScript `FetchError` now extends `ArtifactError` (and so `RegistryError`), where it previously extended `ChtypesError` directly.** A `try`/`catch` chain that tests `RegistryError` before `FetchError` now takes the `RegistryError` arm for the five fetch verdicts, which it did not before. Ordering a catch chain most-specific-first is unaffected.
14
+
15
+ ### Added
16
+
17
+ - **Load-time checksum verification in Go and Rust**, matching what Python and TypeScript have always offered: re-hash the library against `manifest.json`'s `library_sha256` **before `dlopen`**, and refuse the load on a mismatch. Off by default in all four. Whether an artifact was re-hashed before being mapped used to depend on which binding a consumer picked, which is a security posture and not an API spelling. A manifest carrying no `library_sha256` is refused by Go, TypeScript and Rust — verification asked for and not possible is not verification — while Python's returns silently, which is what remains of issue #13's item A2.
18
+ - **One catchable artifact error in TypeScript.** `ArtifactError` is a new exported base: all six artifact conditions now extend it, where `ArtifactMissingError` previously sat under `RegistryError` and the other five under `FetchError`, so a caller had to catch two unrelated types. Every existing `code` value and message is unchanged, and `ArtifactMissingError instanceof RegistryError` still holds.
19
+ - **Eight constants that three bindings exported and the fourth did not**, so a caller no longer retypes a literal the other SDKs hand them: `Registry.SearchPath()` (go), `parse_signature_file` and `FETCH_COMMAND` (python), `RELEASE_KEY_ID` and `DEFAULT_LOCK_FILE` (ts), `fetch::LOCK_SCHEMA` (rust), and `CHTYPES_REGISTRY` / `CHTYPES_AUTOFETCH` as `EnvRegistry` / `EnvAutoFetch` (go) and `ENV_REGISTRY` / `ENV_AUTOFETCH` (ts). Each new name replaces the inline literal in its own binding's code path, so there is one definition rather than two.
20
+
21
+ ### Notes
22
+
23
+ - Speaks **ABI revision 4**, unchanged since 0.1.0, so no artifact needs relinking. The golden set this release was tested against is the one core serves, generated on the ClickHouse lines 24.8, 25.3, 25.8, 25.10, 26.2, 26.3, 26.4, 26.5, 26.6, 26.7 and 26.8.
24
+ - `Library` still has no scope-based release in Python or TypeScript, while `Registry`, `Schema`, `Filter` and `Block` do. That asymmetry is deliberate — a `with library:` or `using library` closes at the end of a block, which is the mid-lifecycle teardown measured to segfault on the next open — and it is now recorded in the parity manifest so no future audit reopens it as a gap.
25
+
26
+ ## [0.1.2] — 2026-09-11
27
+
28
+ ### Changed — BREAKING
29
+
30
+ - **The transform reason for a materialized DEFAULT is now `default_materialized`** — previously the same word spelled with an `s` — and the exported constant naming it is now `Reason.DefaultMaterialized`. ClickHouse's own keyword is `MATERIALIZED`; its parser rejects the `s` spelling outright with a syntax error, so the reason naming that concept now matches the system it describes. Code comparing against the old string or the old constant name must be updated.
31
+ - This is an SDK-only change and **the ABI is untouched: it remains revision 4, and no artifact needs relinking.** The reason is derived in the binding, not received from the artifact — the library emits `default_substituted`, which each binding translates. That wire value is unchanged.
32
+
33
+ ### Changed
34
+
35
+ - **The per-language README is now an orientation page, not a manual.** It carries install, a quickstart that runs, the three outcomes and the error model, and then points at `docs/` for the rest — roughly 450 lines of reference prose moved to `docs/guides/` and `docs/reference/` rather than being repeated four times and drifting four ways.
36
+ - **The published packages no longer cite a repository you cannot open.** 148 source comments — in every binding and in `include/chtypes.h`, all of which ship inside the crate, the sdist and the module — attributed the contract they implement to a private repository's specification. They now cite **the C ABI contract**, with `include/chtypes.h` as the public authority for it. Alongside those, 14 dead paths into that repository, the examples' instruction to build an artifact there, and two descriptions of the signing infrastructure are gone. What remains is every place where the two-repository split is itself the subject — describing it in prose is honest and stays legal; naming something a reader cannot open is not, and `scripts/lint-public.sh` is what enforces the difference. (An earlier draft of this entry enumerated three files. That count was never checked and was wrong by eighteen, which is the same mistake the entry above is about: the rule is checkable, a file list is not.)
37
+ - `scripts/lint-public.sh` is new and blocking in CI, because the sweep this replaces reported itself complete while 148 of its targets survived in a different shape. It refuses both the private repository's name and that exact prose pointer, and its `--selftest` proves each rule fires rather than assuming it.
9
38
 
10
39
  ## [0.1.1] — 2026-09-11
11
40
 
12
- - **The golden set is served, not tracked.** `goldens/cases.json` no longer
13
- exists in this repository. Core publishes `sdk-goldens.json` in the rolling
14
- release as a row in the signed `SHA256SUMS`; `scripts/fetch.sh` installs it at
15
- `<registry>/sdk-goldens.json` and the golden test reads it offline from there
16
- (`CHTYPES_GOLDENS` overrides). A case runs only against the exact ClickHouse
17
- version `generated.exact` names for its line and skips loudly otherwise, so
18
- the set shrinks as lines are added rather than recording an answer twice.
19
- - **Artifact identity gained a wrapper build.** Assets are
20
- `chtypes-<version>-<os>-<arch>-b<N>.tar.gz`, where `<N>` is the core commit's
21
- UNIX timestamp; an absent suffix means build 0 and stays valid. Line
22
- resolution ranks by `(version, build)`, so a rebuild of a line supersedes the
23
- older build.
24
- - Fetching now retries the publish window, so a fetch that races a release no
25
- longer fails on sums naming a file the host has not finished serving.
26
- - README install instructions name the published package rather than a path
27
- dependency.
28
- - Two errors in the published README, both found by running the snippets
29
- rather than reading them: Python's `substituted` is on `RowResult`, not
30
- `BatchResult`, and TypeScript's `using schema = …` is a syntax error on
31
- Node 22, this package's own `engines` floor (`schema.close()` is portable).
32
- - `docs/support.md` is new: which language versions, platforms and ClickHouse
33
- lines are supported, generated from the manifests and the release's own index.
41
+ - **The golden set is served, not tracked.** `goldens/cases.json` no longer exists in this repository. Core publishes `sdk-goldens.json` in the rolling release as a row in the signed `SHA256SUMS`; `scripts/fetch.sh` installs it at `<registry>/sdk-goldens.json` and the golden test reads it offline from there (`CHTYPES_GOLDENS` overrides). A case runs only against the exact ClickHouse version `generated.exact` names for its line and skips loudly otherwise, so the set shrinks as lines are added rather than recording an answer twice.
42
+ - **Artifact identity gained a wrapper build.** Assets are `chtypes-<version>-<os>-<arch>-b<N>.tar.gz`, where `<N>` is the core commit's UNIX timestamp; an absent suffix means build 0 and stays valid. Line resolution ranks by `(version, build)`, so a rebuild of a line supersedes the older build.
43
+ - Fetching now retries the publish window, so a fetch that races a release no longer fails on sums naming a file the host has not finished serving.
44
+ - README install instructions name the published package rather than a path dependency.
45
+ - Two errors in the published README, both found by running the snippets rather than reading them: Python's `substituted` is on `RowResult`, not `BatchResult`, and TypeScript's `using schema = …` is a syntax error on Node 22, this package's own `engines` floor (`schema.close()` is portable).
46
+ - `docs/support.md` is new: which language versions, platforms and ClickHouse lines are supported, generated from the manifests and the release's own index.
34
47
 
35
48
  ## [0.1.0] — 2026-09-10
36
49
 
37
50
  First release, published to npm from the tag `ts/v0.1.0`.
38
51
 
39
- - Speaks **ABI revision 4** of the frozen `chs_*` C ABI
40
- (`include/chtypes.h`, 28 functions). The binding `dlopen`s a per-version
41
- artifact and reimplements no ClickHouse semantics; `Transformed` is the one
42
- derived result.
43
- - Verified against the public golden set (`goldens/cases.json`, schema 1,
44
- 31 cases), whose expectations were generated on ClickHouse
45
- 24.8, 25.3, 25.8, 25.10, 26.5, 26.6 and 26.7 — every case answered
46
- identically on all seven lines, and any case that did not was refused by
47
- the generator.
48
- - Pre-1.0: the artifact name `libchtypes`, the `chs_` prefix and the
49
- `enum chs_format` numbers are frozen. Function signatures freeze at this tag.
52
+ - Speaks **ABI revision 4** of the frozen `chs_*` C ABI (`include/chtypes.h`, 28 functions). The binding `dlopen`s a per-version artifact and reimplements no ClickHouse semantics; `Transformed` is the one derived result.
53
+ - Verified against the public golden set (`goldens/cases.json`, schema 1, 31 cases), whose expectations were generated on ClickHouse 24.8, 25.3, 25.8, 25.10, 26.5, 26.6 and 26.7 — every case answered identically on all seven lines, and any case that did not was refused by the generator.
54
+ - Pre-1.0: the artifact name `libchtypes`, the `chs_` prefix and the `enum chs_format` numbers are frozen. Function signatures freeze at this tag.
package/README.md CHANGED
@@ -1,449 +1,80 @@
1
1
  # chtypes — TypeScript SDK
2
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.
3
+ **If this row were inserted into this table on this ClickHouse version, what would happen?** chtypes answers with ClickHouse's own code: the real C++ type machinery, vendored per release into a native library behind the frozen `chs_*` C ABI and reached here through `ffi-rs`. Nothing semantic is reimplemented, so _"what does ClickHouse do with `256` into a `UInt8`?"_ is answered by ClickHouse rather than by a model of it. One peer binding among `{go, python, ts, rust}` — no language is privileged, and all four give one answer.
13
4
 
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).
5
+ Node ≥ 22, ESM only. `ffi-rs` ships prebuilt for darwin arm64/x64 and linux arm64/x64 (gnu and musl), so there is **no build step**.
16
6
 
17
7
  ## Install
18
8
 
19
- ```sh
20
- pnpm add @wavehouse/chtypes
21
- ```
22
-
23
- To work against a checkout instead, wire it up by path — with pnpm, this
24
- repo's package manager:
25
-
26
- ```jsonc
27
- // your app's package.json
28
- { "dependencies": { "@wavehouse/chtypes": "file:../chtypes/ts" } }
29
- ```
30
-
31
- or `pnpm link <repo>/ts`. Either way, build the package's `dist/`
32
- first — that is what `package.json#exports` serves:
9
+ Two things: this package, and at least one **artifact** — the per-version native library it `dlopen`s at runtime.
33
10
 
34
11
  ```sh
35
- cd <repo>/ts
36
- pnpm install && pnpm build
37
- ```
38
-
39
- (If a linked copy goes stale, note `pnpm install --force` does **not** relink;
40
- remove `node_modules` and reinstall.)
41
-
42
- **Artifact prerequisite.** The SDK loads per-version native artifacts; it does
43
- nothing without them. Fetch a published one — verified, into the per-user cache
44
- every SDK here defaults to — with the package's own command, or build one in
45
- the core repository, which lands it in the same place:
46
-
47
- ```sh
48
- npx @wavehouse/chtypes fetch 25.8 # -> ~/.cache/chtypes/artifacts/<os>-<arch>/25.8/
49
- ```
50
-
51
- (`scripts/fetch.sh` at the repository root is the reference implementation of
52
- the same contract, `docs/fetch.md`; the command, the function and the lazy
53
- fetch are in [Artifacts: fetch, verify, autofetch](#artifacts-fetch-verify-autofetch).)
54
-
55
- A registry holds one subdirectory per version:
56
-
57
- <registry>/25.8/
58
- manifest.json # names the library file — the loader reads this, never guesses
59
- libchtypes.dylib # (.so on Linux; the name comes from manifest.json)
60
- CH_VERSION
61
- unsafe_families.txt # the build's own refuse-list; empty is a valid list
62
-
63
- `new Registry(dir?)` scans such a directory. Where it looks is the search path
64
- of `docs/fetch.md` §1, in order: the explicit `dir`, `$CHTYPES_REGISTRY`, the
65
- per-user cache (`defaultRegistryDir()`), then the reserved system locations
66
- `/usr/local/share/chtypes/artifacts/<os>-<arch>` and `/opt/chtypes/artifacts/<os>-<arch>`.
67
- The first directory holding artifacts is loaded at construction; a line it
68
- lacks is loaded from the first later directory that has it when first asked
69
- for (`registry.searchPath` lists every candidate; a named directory — the
70
- argument or the variable — that does not exist is an error naming it, never a
71
- silent fallback). For each artifact the
72
- loader dlopens the file `manifest.json#library` names, asks the library to name
73
- itself (`chs_clickhouse_version()` — nothing is inferred from paths), and
74
- checks the ABI revision: `chs_abi_revision()` must equal this binding's
75
- `ABI_REVISION` (currently 4). A different nonzero revision is **refused** —
76
- calling through mismatched declarations is undefined; `0` means the artifact
77
- predates the probe and individual missing symbols degrade to
78
- `UnsupportedError` at call time, never to a load failure.
79
-
80
- ## Artifacts: fetch, verify, autofetch
81
-
82
- The fetch/verify contract every SDK implements identically is
83
- [`docs/fetch.md`](../docs/fetch.md); this package carries it as a `bin` and
84
- as a function.
85
-
86
- ```sh
87
- npx @wavehouse/chtypes fetch 25.8 # one line, verified, into the per-user cache
88
- npx @wavehouse/chtypes fetch --all # every line the release publishes for this host
89
- npx @wavehouse/chtypes fetch 25.8 --platform linux-arm64 --dest /opt/chtypes/artifacts/linux-arm64
90
- npx @wavehouse/chtypes fetch 25.8 --lock chtypes.lock # record what was installed (file + sha256)
91
- npx @wavehouse/chtypes fetch 25.8 --frozen # refuse anything chtypes.lock does not pin
92
- npx @wavehouse/chtypes verify # re-hash every installed line against its manifest
93
- npx @wavehouse/chtypes list # what is installed, and what the release offers
94
- npx @wavehouse/chtypes where # the registry directory fetch writes to
95
- ```
96
-
97
- `fetch` prints the installed directory alone on stdout (progress on stderr),
98
- so `dir="$(npx @wavehouse/chtypes fetch 25.8)"` composes. Exit codes: 0 ok ·
99
- 1 verification failed · 2 usage · 3 source unreachable · 4 not published for
100
- this platform/line. A line (`25.8`) resolves to the one patch the release
101
- publishes for it; an exact patch (`25.8.28.1-lts`) is a hard requirement.
102
- Sources: `--tag v1.2.0` picks a frozen release on `CHTYPES_ARTIFACTS_URL`
103
- (default `https://artifacts.wavehouse.dev`; the rolling `artifacts` release
104
- otherwise); `--url <base>` is any other base — a mirror, a `file://` path or
105
- a plain directory. `--offline` never touches the source: installed and
106
- verified is fine, anything else is `CHTYPES_SOURCE_UNREACHABLE`. Fetch writes
107
- to the first of `--dest`, `$CHTYPES_REGISTRY`, the per-user cache; never to a
108
- system location.
109
-
110
- **The verification chain**, in order — nothing else is a verdict, not an exit
111
- code, not a `Content-Length`: (0) `SHA256SUMS.sig`, an ed25519 signature over
112
- the bytes of `SHA256SUMS`, verified with the embedded release key
113
- (`RELEASE_PUBLIC_KEYS`, key id `deb275922dbff76e`) or with the keys in
114
- `CHTYPES_TRUSTED_KEYS=<hex>[,<hex>…]`, which *replace* it — an unsigned or
115
- mis-signed release is `CHTYPES_ARTIFACT_UNTRUSTED` and nothing is downloaded
116
- around it (`CHTYPES_ALLOW_UNSIGNED=1` skips this step with one loud warning
117
- naming the source, never silently); (1) `index.json` names the asset and its
118
- sha256; (2) the now-authentic `SHA256SUMS` must agree; (3) the tarball is
119
- hashed before it is unpacked; (4) `manifest.json` inside must agree with the
120
- index, and the installed library is re-hashed in place after the atomic
121
- rename. Any mismatch is `CHTYPES_ARTIFACT_CORRUPT` and nothing is installed.
122
- Installed-and-verified is a no-op (`--force` re-downloads). Once files are in
123
- a registry directory the loader trusts the directory, as a runtime trusts
124
- `node_modules` — pass `verifyChecksums: true` to re-hash at load.
125
-
126
- The same thing as a function:
127
-
128
- ```ts
129
- import { ensure } from '@wavehouse/chtypes';
130
-
131
- const r = await ensure('25.8'); // idempotent; r.dir is <registry>/25.8, r.installed says whether bytes moved
132
- await ensure('25.8', { lock: 'chtypes.lock', frozen: true }); // CHTYPES_ARTIFACT_PINNED on drift
133
- await ensure('25.8', { url: 'file:///srv/mirror', trustedKeys: [hex] }); // a mirror signed by someone else
134
- ```
135
-
136
- Every flag is a property of `EnsureOptions` (`dest`, `platform`, `tag`, `url`,
137
- `lock`, `frozen`, `force`, `offline`, `trustedKeys`, `allowUnsigned`,
138
- `onProgress`, `signal`); `ensureAll`, `verifyInstalled` and `listArtifacts`
139
- are `--all`, `verify` and `list`. Concurrent `ensure`s of one line in one
140
- process share one fetch.
141
-
142
- **Lazy fetch on first open is opt-in** — `new Registry(dir, { autofetch: true })`
143
- or `CHTYPES_AUTOFETCH=1` — because a production process must not begin a
144
- 250 MB download inside a request. `Registry#for()` is synchronous and never
145
- fetches; `Registry#open()` is its async twin: with autofetch on, a line no
146
- directory on the search path holds is fetched through `ensure` first (into the
147
- directory fetch writes to, once per process per line even under concurrent
148
- opens), then loaded.
149
-
150
- ```ts
151
- const registry = new Registry(undefined, { autofetch: true, fetch: { tag: 'v1.2.0' } });
152
- const lib = await registry.open('25.8'); // fetched on first use, loaded from then on
153
- ```
154
-
155
- **The one error.** A line no directory on the search path holds is an
156
- `ArtifactMissingError` — a `RegistryError` with `code === 'CHTYPES_ARTIFACT_MISSING'`
157
- and this message, verbatim apart from the bracketed parts:
158
-
159
- ```
160
- chtypes: no artifact for ClickHouse <line> (<os>-<arch>). Looked in: <dir1>, <dir2>, ….
161
- Install it: npx @wavehouse/chtypes fetch <line>
162
- or set CHTYPES_AUTOFETCH=1 to fetch on first use.
12
+ pnpm add @wavehouse/chtypes
13
+ npx @wavehouse/chtypes fetch 25.8
163
14
  ```
164
15
 
165
- The fetch verdicts are `FetchError` subclasses carrying the shared codes:
166
- `ArtifactUntrustedError` (`CHTYPES_ARTIFACT_UNTRUSTED`), `ArtifactCorruptError`
167
- (`CHTYPES_ARTIFACT_CORRUPT`), `ArtifactPinnedError` (`CHTYPES_ARTIFACT_PINNED`),
168
- `ArtifactUnpublishedError` (`CHTYPES_ARTIFACT_UNPUBLISHED`) and
169
- `SourceUnreachableError` (`CHTYPES_SOURCE_UNREACHABLE`). Artifacts are Elastic
170
- License 2.0, separate from this package's Apache 2.0; `LICENSE` and `NOTICE`
171
- ship in every release and `fetch` says so once.
16
+ The fetch lands in `~/.cache/chtypes/artifacts/<os>-<arch>/25.8/` — the per-user cache every chtypes binding reads by default — after checking an ed25519 signature over the release and the sha256 of every byte. `$CHTYPES_REGISTRY` overrides it.
172
17
 
173
18
  ## Quickstart
174
19
 
175
20
  ```ts
176
- import { Format, Registry, UnsupportedError } from '@wavehouse/chtypes';
21
+ import { Format, Registry } from '@wavehouse/chtypes';
177
22
 
178
- const registry = new Registry(); // $CHTYPES_REGISTRY, else the per-user cache
179
- const lib = registry.for('25.8'); // minor line or exact patch both resolve
23
+ const registry = new Registry(); // walks the search path
24
+ const lib = registry.for('25.8'); // a line or an exact patch; never a nearest match
25
+ const schema = lib.compileDdl('x UInt8, ts DateTime DEFAULT now()');
180
26
 
181
- // Compile under the deployment's declared settings profile — a type gate
182
- // declared here binds at compile, exactly as that server's CREATE TABLE does.
183
- using schema = lib.compileDdl('id LowCardinality(UInt32), x UInt8, ts DateTime DEFAULT now()', {
184
- settings: { allow_suspicious_low_cardinality_types: '1' },
185
- });
186
- // `using` needs explicit resource management: TypeScript downlevels it, and
187
- // plain JavaScript needs Node >= 24. On Node 22 call `schema.close()` instead —
188
- // same effect, and every disposable here has both.
27
+ const batch = schema.rows(Format.JSONEachRow, Buffer.from('{"x":256}'));
28
+ const row = batch.rows[0];
29
+ console.log(batch.outcome); // accepted
30
+ console.log(row.values[0].text); // 0 — what would actually be stored
31
+ console.log(row.transformed[0]?.reason); // overflow_wrap — which is the product
32
+ console.log(row.substituted[0]?.column); // ts — send it explicitly in the real INSERT
189
33
 
190
- const good = schema.row(Format.JSONEachRow, Buffer.from('{"id": 7, "x": 256}'));
191
- console.log(good.outcome); // accepted
192
- console.log(good.transformed[0]?.reason); // overflow_wrap — 256 stored as 0, silently
193
- console.log(good.substituted[0]?.column); // ts — send it explicitly in the real INSERT
194
-
195
- const bad = schema.row(Format.JSONEachRow, Buffer.from('{"id": 7, "x": '));
196
- console.log(bad.outcome, bad.errCode, bad.errMsg); // rejected <ClickHouse's own code + message>
34
+ schema.close();
197
35
  ```
198
36
 
199
- A bad **row** is never an exception — the verdict is `RowResult#outcome`
200
- (`accepted` | `rejected` | `accepted_poisoned` | `unsupported`). Exceptions are
201
- for schema-level answers (`compileDdl`, `setEngine`, `setTtl`,
202
- `validateType`), and there the class *is* the verdict: `SchemaError` = the
203
- server refused (a real ClickHouse code), `UnsupportedError` = chtypes declines
204
- to guess — fall back to the server. The two are peers; a decline never
205
- satisfies `instanceof SchemaError`.
206
-
207
- ## API reference
208
-
209
- Callables, mapped to the C ABI (`spec/c-abi.md`; `spec/bindings.md` is the
210
- cross-SDK shape):
211
-
212
- | Symbol | C function | Params | Returns | Errors |
213
- |---|---|---|---|---|
214
- | `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) |
215
- | `Registry#versions()` | — derived | — | minor lines, oldest first | never throws |
216
- | `Registry#libraries()` | — derived | — | every loaded `Library`, in release order | never throws |
217
- | `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 |
218
- | `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) |
219
- | `Registry#has(version)` | — derived | version string | `boolean` | never throws |
220
- | `Registry#close()` / `Symbol.dispose` | `chs_shutdown` per library | — | — | see `Library#shutdown` |
221
- | `Library#validateType(expr)` | `chs_validate_type` | type expression | canonical spelling (use verbatim) | `SchemaError` (server refusal, e.g. 50); `UnsupportedError` (unsafe family / missing symbol) |
222
- | `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) |
223
- | `Library#hasCompileSettings()` | probe via `chs_schema_compile` | — | `boolean` (true on every artifact this repo builds) | passes through unexpected errors |
224
- | `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) |
225
- | `Library#referenceType(expr)` | `chs_reference_type` | type expression | widened reference type, `''` if none | `UnsupportedError` (missing symbol) |
226
- | `Library#registeredFamilies()` | `chs_registered_families` | — | family names | `UnsupportedError` (missing symbol) |
227
- | `Library#functionFlags()` | `chs_function_flags` | — | the volatility TSV audit, verbatim | `UnsupportedError` (missing symbol) |
228
- | `Library#shutdown()` | `chs_shutdown` | — | — | `ChtypesError` (reentrancy); missing symbol ignored |
229
- | `Schema#columns` | `chs_schema_column_count/_name/_type/_default_kind/_default_expr/_default_is_literal` (at compile) | — | `ColumnInfo[]`, canonicalised, declaration order | — |
230
- | `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) |
231
- | `Schema#setTtl(ttl)` | `chs_schema_ttl` | rows-TTL expression | — | `UnsupportedError` (any nonzero rc: refused TTL form, guarded exception, missing symbol) |
232
- | `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) |
233
- | `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`) |
234
- | `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) |
235
- | `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) |
236
- | `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) |
237
- | `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) |
238
- | `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 |
239
- | `Schema#close()` / `Symbol.dispose` | `chs_schema_free` | — | — | never throws; idempotent |
240
- | `minorOf(version)` | — derived | version string | minor line | never throws |
241
- | `compareMinor(a, b)` | — derived | two minor lines | numeric order (25.10 > 25.8) | never throws |
242
- | `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) |
243
- | `verifyInstalled(dest?, platform?)` / `listArtifacts(opts?)` | — no C call | registry dir / `EnsureOptions` | `Promise<InstalledArtifact[]>` / `Promise<ListResult>` | `verifyInstalled` never throws; `listArtifacts` the `FetchError` verdicts unless `offline` |
244
- | `registrySearchPath(explicit?, platform?)` / `fetchDestination(explicit?, platform?)` / `hostPlatform()` | — derived (`docs/fetch.md` §1) | path/env probing | `string[]` / `string` / `string` | never throw |
245
- | `resolveRegistryDir(explicit?)` / `defaultRegistryDir()` / `looksLikeRegistry(dir)` | — derived | path/env probing | resolved dir / `string` / `boolean` | never throw |
246
- | `formatName(f)` | — derived | format code | constant name | never throws |
247
- | `encodeSettings(settings?)` | — derived | settings map | `settings_json` text | `ChtypesError` (a JS `number` value) |
248
- | `parseVersionResult` / `parseChangedSettingsResult` / `parseColumnsResult` / `reconstructDdl` | — no C call (discovery kit) | query result bytes / discovered columns | version / settings map / columns / DDL string | `ChtypesError` on malformed input |
249
- | `isLossyReason(reason)` | — derived | reason string | `boolean` (false for exactly 4 reasons) | never throws |
250
- | `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 |
251
- | `nativeStats()` | — diagnostic | — | `{stringsTaken, stringsFreed}` | never throws |
252
-
253
- Types and constants: `Format` (the `chs_format` integers 0–9, frozen),
254
- `CompileMode.Declared` (= C `CHS_COMPILE_DECLARED` = 0), `CODE_UNSUPPORTED`
255
- (−2, the wire sentinel — carried by results, never by an error object),
256
- `ABI_REVISION` (4), `Outcome`, `RowResult`, `BatchResult`, `FilterResult`,
257
- `Verdict`, `Value`, `Transform`, `Substitution`, `Computed`, `ColumnDoc`,
258
- `ColumnInfo`, `EngineOptions`, `CompileOptions`, `CompileFilterOptions`,
259
- `Settings`, `SettingValue`, `Reason`, `ServerProfile`, `DiscoveredColumn`,
260
- `Manifest`, `RegistryOptions`, `EnsureOptions`, `EnsureResult`, `FetchEvent`,
261
- `InstalledArtifact`, `ListResult`, `LockFile`, `ArtifactErrorCode`,
262
- `RELEASE_PUBLIC_KEYS`, `Json`, `JsonKind`, and the error classes
263
- `ChtypesError` (base), `RegistryError`, `ArtifactMissingError` (a
264
- `RegistryError` with `code`), `FetchError` and its five verdict subclasses,
265
- `SchemaError`, `UnsupportedError` (a **peer** of `SchemaError`, deliberately
266
- not a subclass).
267
-
268
- ## Filters, query parameters and the block twin (ABI revisions 3–4)
37
+ The row is **accepted** and `256` is silently stored as `0`. That report — `transformed` — is the one derived answer in the system and the reason it exists.
269
38
 
270
- `Schema#compileFilter(expr)` compiles one boolean expression against the
271
- schema's physical columns and `Filter#rows(...)` answers per-row verdicts
272
- with **WHERE-side** semantics (`x = 256` over `UInt8` promotes — false for
273
- every row — it never wraps), computed by ClickHouse's own comparison
274
- functions. Four verdicts: `'true'`/`'false'` are answers; `'error'` (the
275
- predicate threw — a real server fails the whole query) and `'decline'` (this
276
- library declines) are NOT, and an enforcing caller MUST fail closed on both.
277
- **Enforcement gate**: no read-side security may be enforced on this surface
278
- until the WHERE-truth rig gates green — until then it is shadow/replay only
279
- (`spec/c-abi.md` §Filters).
39
+ `ts` was substituted rather than stored: send every substituted column as an explicit value in the real INSERT, or the server re-evaluates `now()` at its own instant and your preview is not what landed.
280
40
 
281
- **Params (revision 4).** The expression may contain `{name:Type}` query
282
- parameters, bound with `compileFilter(expr, { params: { name: 'value' } })`
283
- — values are **strings**, exactly as the server's own parameter channels
284
- carry them. Substitution is the server's own `ReplaceQueryParameterVisitor`:
285
- each value is deserialized by the DECLARED type's own reader and injected as
286
- a typed literal AFTER SQL parsing, so a value is never SQL text and **must
287
- never be hand-escaped into the expression** — injection safety is by
288
- construction, and a hostile value (`' OR 1=1 --`) compares as exactly that
289
- literal. An UNBOUND parameter is the server's own **456** ("Substitution
290
- `name` is not set"), an unparseable value the server's own **457** — both
291
- `SchemaError`, verbatim; a bound name the expression never uses is ignored.
292
- The compiled handle bakes the values in — identity is per
293
- (schema, expr, params) — so **a caller compiling filters from
294
- tenant-influenced values MUST bound its cache and its compile rate**: a
295
- bounded LRU keyed on (schema generation, expr, params-hash) plus a
296
- per-principal compile throttle; an unbounded cache is a memory DoS and an
297
- unmetered compile path is a CPU DoS (`spec/c-abi.md` §Filters, normative).
41
+ ## Three outcomes, and conflating any two is a bug
298
42
 
299
- **Choose the brace type for the value's domain.** A bound value is
300
- deserialized by the brace type's OWN reader (`deserializeTextEscaped`),
301
- which **wraps** an out-of-domain integer — `{p:UInt8}` given `"256"` binds
302
- `0` and matches every genuine zero (measured, uniform 24.8–26.7,
303
- server-matched) — while the same constant written as a literal in the
304
- expression PROMOTES (`x = 256` over `UInt8` is simply never true). A
305
- too-narrow parameter type therefore silently matches the wrong rows: size
306
- the type for the tenant-supplied domain (`{p:UInt64}`, `{p:String}`) or
307
- validate the value before binding it. Malformed integer spellings refuse
308
- loudly with the server's own **457** (`"-1"`, `"+7"`, `"007"` as `UInt8`);
309
- an empty string refuses with **32**. If one name is bound twice at the C
310
- boundary, the LAST binding wins — the server's own `insert_or_assign` rule
311
- (unreachable through this SDK's unique-keyed object, stated for
312
- completeness).
43
+ A bad **row** is a verdict, not an exception: `outcome` becomes `'rejected'` with ClickHouse's own code and message. Exceptions are for schema-level answers — `compileDdl`, `setEngine`, `setTtl`, `validateType` — and there the class _is_ the verdict.
313
44
 
314
- One naming trap, the transport's rather than this library's: on a real
315
- server's **TCP** channel a parameter *named* `limit` or `offset` fails at
316
- the protocol layer with code 26 even when unused (params ride in a Settings
317
- block there); HTTP is fine, and this library matches the HTTP/substitution
318
- semantics. Avoid those two names for anything that will ever cross TCP.
45
+ - `SchemaError` — the server refused, and `code` is a real ClickHouse code.
46
+ - `UnsupportedError` — chtypes declines to guess, and a real server might well have accepted. **Fall back to the server.**
47
+ - The two are **peers**: a decline never satisfies `instanceof SchemaError`.
319
48
 
320
- **The block twin (revision 4).** `Schema#parseBlock(format, body, settings)`
321
- parses a body ONCE into a `Block` — the parse half of `Filter#rows` — and
322
- `Filter#eval(block)` answers the same `FilterResult` with no re-parse: the
323
- live-SSE hot path is K filters × 1 event, and the re-parse is shed.
324
- Normative equivalence: `eval(parseBlock(body))` ≡ `rows(body)` for every
325
- verdict class (volatile DEFAULTs resolve against the PARSE call's clock
326
- instant — pin `chtypes_now_epoch_nanos` for cross-call identity).
327
- Evaluation is a pure function of (filter, block), takes no settings, and
328
- never consumes or mutates the block. Filter and block MUST come from the
329
- SAME schema: a mismatched same-library pair answers `'rejected'`/1002
330
- loudly, and a cross-library pair throws `ChtypesError` before any C call. A
331
- `Block` follows the filter's lifetime rules exactly (`Schema#close` frees
332
- open blocks first, and `using` nests naturally: block, filter, schema).
333
- Parse-once does not open enforcement earlier: the twin is under the same
334
- enforcement gate.
49
+ ## Documentation
335
50
 
336
- ## Settings and precedence
51
+ | | |
52
+ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
53
+ | [Quickstart](https://github.com/wave-rf/chtypes/blob/main/docs/quickstart.md) | the same program in all four languages |
54
+ | [TypeScript API reference](https://github.com/wave-rf/chtypes/blob/main/docs/reference/ts.md) | every symbol, the C entry point under it, what it returns and what it throws |
55
+ | [Artifacts](https://github.com/wave-rf/chtypes/blob/main/docs/guides/artifacts.md) | getting one, where it lands, verifying and pinning it |
56
+ | [Batches](https://github.com/wave-rf/chtypes/blob/main/docs/guides/batches.md) | always `rows`, and the two bad-row policies |
57
+ | [Transformations](https://github.com/wave-rf/chtypes/blob/main/docs/guides/transformations.md) | the silent-change report, and the DEFAULTs you must echo back |
58
+ | [Settings](https://github.com/wave-rf/chtypes/blob/main/docs/guides/settings.md) · [Discovery](https://github.com/wave-rf/chtypes/blob/main/docs/guides/discovery.md) | the four channels; asking a real server what profile to validate under |
59
+ | [Filters](https://github.com/wave-rf/chtypes/blob/main/docs/guides/filters.md) · [Multi-version](https://github.com/wave-rf/chtypes/blob/main/docs/guides/multi-version.md) | boolean expressions over rows; several ClickHouse versions in one process |
60
+ | [Support matrix](https://github.com/wave-rf/chtypes/blob/main/docs/support.md) · [Limitations](https://github.com/wave-rf/chtypes/blob/main/docs/limitations.md) | what works where; what chtypes declines to answer |
337
61
 
338
- Settings ride four channels; on the row path the leftmost channel that names a
339
- setting wins:
62
+ ## Four things specific to this binding
340
63
 
341
- per-call map > handle compile profile > library defaults (setDefaultSettings) > ClickHouse defaults
64
+ **`using` is a syntax error on Node 22**, this package's own floor. Explicit resource management needs TypeScript to downlevel it for you, or plain JavaScript on Node ≥ 24. `close()` works everywhere and every disposable here has both, so portable examples use `close()`.
342
65
 
343
- One documented exception: a **type gate** named in the compile profile
344
- (`allow_suspicious_low_cardinality_types`, `allow_experimental_json_type`, …)
345
- binds at compile and then **outranks the per-call map** for that handle's life
346
- — exactly as a real server checks a gate at CREATE and never re-reads it per
347
- INSERT.
66
+ **A settings value may be a `string` or a `bigint`, never a `number`.** The runtime rejects a `number`, because TypeScript is not present at a JS consumer's call site and a 19-digit `chtypes_now_epoch_nanos` does not survive an IEEE double — sent as a JSON number the setting would be silently ignored.
348
67
 
349
- Rules that cost real bugs:
68
+ **`for()` never fetches; `open()` can.** `Registry#for(v)` is synchronous and resolves from the search path alone. `await registry.open(v)` is its async twin, and with `{ autofetch: true }` (or `CHTYPES_AUTOFETCH=1`) it fetches a missing line first. Off by default: a production process must not begin a 250 MB download inside a request.
350
69
 
351
- - **Values are strings (or bigints).** A JS `number` is rejected at runtime:
352
- `chtypes_now_epoch_nanos` is a 19-digit nanosecond epoch, which does not
353
- survive an IEEE double — sent as a JSON number the setting is *silently
354
- ignored*. Use `BigInt(…)` or a string; `bigint` crosses via `toString()`.
355
- - **An unknown setting name rejects the whole call with the server's own
356
- code 115** (with its did-you-mean hint), on every channel.
357
- `setDefaultSettings` refuses **wholesale** — nothing committed.
358
- - The six `chtypes_*` keys are the whole reserved namespace; anything else
359
- spelled `chtypes_*` is an unknown name (115). Per-call:
360
- `chtypes_now_epoch_nanos` (pin the batch instant),
361
- `chtypes_clock_offset_nanos` (measured server−client offset),
362
- `chtypes_max_clock_skew_nanos` (refuse volatile-DEFAULT substitution past
363
- the budget). Process-wide, via `setDefaultSettings` only:
364
- `chtypes_default_eval_memory_bytes` (default 256 MiB),
365
- `chtypes_default_eval_wall_nanos` (default 1 s),
366
- `chtypes_custom_settings_prefixes` (mirror of the server's
367
- `custom_settings_prefixes`; ships as `SQL_`).
368
- - A row whose `unsupportedSettings` list is non-empty is promoted to
369
- `unsupported` (a KNOWN name whose value this build declines) — never scored
370
- as agreement.
371
-
372
- ## Discovery
373
-
374
- **The library never connects to ClickHouse.** It ships three SQL query
375
- constants; your app runs them at connect time with whatever client it already
376
- has, and declares what it learned. Never ask the customer — ask their server.
377
-
378
- | constant | feeds |
379
- |---|---|
380
- | `QUERY_SERVER_VERSION` | `parseVersionResult` → `registry.for(version)` |
381
- | `QUERY_CHANGED_SETTINGS` | `parseChangedSettingsResult` → the compile profile AND per-call settings |
382
- | `QUERY_TABLE_COLUMNS` | `parseColumnsResult` → `reconstructDdl` → `compileDdl` (send `param_db`/`param_table`) |
383
-
384
- ```ts
385
- const profile: ServerProfile = {
386
- version: parseVersionResult(await run(QUERY_SERVER_VERSION)),
387
- settings: parseChangedSettingsResult(await run(QUERY_CHANGED_SETTINGS)),
388
- };
389
- // cache per deployment/tenant, then:
390
- const lib = registry.for(profile.version);
391
- const schema = lib.compileDdl(ddl, { settings: profile.settings }); // CREATE-time
392
- const res = schema.rows(Format.JSONEachRow, body, profile.settings); // per-call
393
- ```
70
+ **`worker_threads` is the boundary.** Two JS threads share one dlopen'd image and one set of C globals, which no per-isolate counter can see. Seed default settings before starting workers, and do not share a `Schema` across them.
394
71
 
395
- The parsers read the JSONEachRow bytes through this package's own byte-exact
396
- reader — `position` survives quoted and bare spellings without a float
397
- round-trip, and `default_kind`/`default_expression` are carried (dropping them
398
- silently loses DEFAULT/MATERIALIZED semantics). A typo'd setting name in the
399
- declared profile fails the **compile** with the server's own 115 — caught at
400
- declare time, not swallowed.
72
+ ## Tests
401
73
 
402
- ## Multi-version use and thread-safety
74
+ `pnpm test`. Tests that need an artifact **skip loudly by name** without a registry on the search path, and a suite that ran nothing fails. The fetch suite runs offline against the miniature releases in `tests/fixtures/fetch/`.
403
75
 
404
- - One `Registry` loads every version side by side. Each artifact is dlopened
405
- `RTLD_LOCAL` (that is the whole mechanism letting two builds that both
406
- define `DB::DataTypeFactory` share a process); cost ≈ 120 MB resident per
407
- loaded version.
408
- - `for()` resolves a minor line (`"25.8"`) or an exact patch
409
- (`"25.8.28.1-lts"`); an unknown patch inside a loaded minor resolves to the
410
- line. Failure is `ArtifactMissingError`, naming every directory looked in
411
- — never a fallback to the nearest version. Version behaviour is
412
- non-monotonic; never infer one version's answer from another's.
413
- - Every call is **synchronous on the JS thread**; ordinary single-threaded
414
- Node code needs no locking. `setDefaultSettings` and `shutdown` carry a
415
- reentrancy tripwire (the `NativeLibrary#inCall` counter) and refuse loudly
416
- if another chtypes call is on the stack — they replace process-global state
417
- the row path reads by reference.
418
- - **`worker_threads` is the boundary.** Two JS threads share one dlopen'd
419
- image and one set of C globals, which no per-isolate counter can see. Seed
420
- default settings BEFORE starting workers, or serialise the seed yourself;
421
- do not share a `Schema` across workers.
422
- - Teardown: `registry.close()` / `library.shutdown()` (or `using` /
423
- `Symbol.dispose`) join the DEFAULT evaluator's background threads —
424
- required before any explicit unload; an ordinary process is covered by
425
- `atexit`.
76
+ Working against a checkout: build `dist/` first (`pnpm install && pnpm build`) — that is what `package.json#exports` serves. If a linked copy goes stale, note that `pnpm install --force` does **not** relink; remove `node_modules` and reinstall.
426
77
 
427
- ## Known limitations
78
+ ## License
428
79
 
429
- - The error model's third arm is deliberate: `unsupported` means "a real
430
- server might well have accepted this; chtypes will not guess" — fall back
431
- to the server (`spec/c-abi.md` §Error model). Unmodelled engines, sorting
432
- keys and TTL forms (WHERE/GROUP BY TTLs, TO DISK/VOLUME, RECOMPRESS,
433
- clock-reading TTLs) decline rather than guess.
434
- - **EPHEMERAL columns:** a format stream carries no column list, so an INSERT
435
- that names an EPHEMERAL column in an explicit column list cannot be
436
- previewed here — detect via `Schema#columns` (`defaultKind ===
437
- 'EPHEMERAL'`) and decline that intersection, never mispreview
438
- (`spec/bindings.md` §EPHEMERAL).
439
- - **macOS artifacts are a dev floor, not an oracle:** `long double` is
440
- 53-bit, so float parses diverge from real servers (Linux matches 395/395 of
441
- the float corpus; macOS 0/395). Float expectations must come from a Linux
442
- artifact or a live server.
443
- - Everything here is **insert-side** coercion. Never fold a `WHERE`-clause
444
- constant through `row`/`rows` — comparison rules differ; refuse an operand
445
- outside the column type's domain instead (`spec/bindings.md` §Constants are
446
- not payloads).
447
- - Binary formats (`RowBinary` family, `Native`, `Buffers`) parse only on
448
- artifacts new enough to carry the reader — probe the artifact; earlier ones
449
- answer 73/117 exactly as their servers do.
80
+ Apache 2.0. The artifacts this package loads are **Elastic License 2.0** — a separate license, shipped inside each artifact release, and `fetch` says so once.
package/dist/cli.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * `chtypes` — the one CLI surface every SDK carries (docs/fetch.md §6),
3
+ * `chtypes` — the one CLI surface every SDK carries (docs/guides/fetch.md §6),
4
4
  * spelled identically in all four:
5
5
  *
6
6
  * chtypes fetch <line>... [--all] [--platform <os-arch>] [--dest <dir>]