chtypes 0.2.2__tar.gz → 0.3.1__tar.gz
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.
- chtypes-0.3.1/CHANGELOG.md +129 -0
- {chtypes-0.2.2 → chtypes-0.3.1}/PKG-INFO +4 -2
- {chtypes-0.2.2 → chtypes-0.3.1}/README.md +3 -1
- {chtypes-0.2.2 → chtypes-0.3.1}/pyproject.toml +1 -1
- {chtypes-0.2.2 → chtypes-0.3.1}/src/chtypes/__init__.py +2 -2
- {chtypes-0.2.2 → chtypes-0.3.1}/src/chtypes/_document.py +27 -3
- {chtypes-0.2.2 → chtypes-0.3.1}/src/chtypes/_manifest.py +38 -8
- {chtypes-0.2.2 → chtypes-0.3.1}/src/chtypes/_native.py +108 -8
- {chtypes-0.2.2 → chtypes-0.3.1}/src/chtypes/discover.py +12 -22
- {chtypes-0.2.2 → chtypes-0.3.1}/src/chtypes/fetch.py +131 -54
- {chtypes-0.2.2 → chtypes-0.3.1}/src/chtypes/registry.py +305 -25
- {chtypes-0.2.2 → chtypes-0.3.1}/src/chtypes/results.py +78 -2
- {chtypes-0.2.2 → chtypes-0.3.1}/src/chtypes/transform.py +14 -2
- {chtypes-0.2.2 → chtypes-0.3.1}/tests/conftest.py +8 -2
- chtypes-0.3.1/tests/test_csv_reader.py +445 -0
- chtypes-0.3.1/tests/test_discover_reconstruct.py +91 -0
- {chtypes-0.2.2 → chtypes-0.3.1}/tests/test_errors.py +1 -1
- {chtypes-0.2.2 → chtypes-0.3.1}/tests/test_fetch.py +161 -4
- chtypes-0.3.1/tests/test_formats_withnames.py +458 -0
- chtypes-0.3.1/tests/test_golden.py +262 -0
- chtypes-0.3.1/tests/test_lazy.py +251 -0
- chtypes-0.3.1/tests/test_quoting_boundaries.py +235 -0
- {chtypes-0.2.2 → chtypes-0.3.1}/tests/test_registry.py +67 -19
- chtypes-0.3.1/tests/test_results_rules.py +150 -0
- chtypes-0.3.1/tests/test_rows_export_with.py +133 -0
- chtypes-0.3.1/tests/test_transform.py +231 -0
- chtypes-0.3.1/uv.lock +108 -0
- chtypes-0.2.2/CHANGELOG.md +0 -74
- chtypes-0.2.2/tests/test_golden.py +0 -146
- chtypes-0.2.2/tests/test_transform.py +0 -82
- chtypes-0.2.2/uv.lock +0 -108
- {chtypes-0.2.2 → chtypes-0.3.1}/.gitignore +0 -0
- {chtypes-0.2.2 → chtypes-0.3.1}/LICENSE +0 -0
- {chtypes-0.2.2 → chtypes-0.3.1}/src/chtypes/__main__.py +0 -0
- {chtypes-0.2.2 → chtypes-0.3.1}/src/chtypes/_ed25519.py +0 -0
- {chtypes-0.2.2 → chtypes-0.3.1}/src/chtypes/_rawjson.py +0 -0
- {chtypes-0.2.2 → chtypes-0.3.1}/src/chtypes/errors.py +0 -0
- {chtypes-0.2.2 → chtypes-0.3.1}/src/chtypes/py.typed +0 -0
- {chtypes-0.2.2 → chtypes-0.3.1}/tests/test_abi_revision.py +0 -0
- {chtypes-0.2.2 → chtypes-0.3.1}/tests/test_cli.py +0 -0
- {chtypes-0.2.2 → chtypes-0.3.1}/tests/test_ed25519.py +0 -0
- {chtypes-0.2.2 → chtypes-0.3.1}/tests/test_parity.py +0 -0
- {chtypes-0.2.2 → chtypes-0.3.1}/tests/test_rawjson.py +0 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Changelog — `chtypes`
|
|
2
|
+
|
|
3
|
+
All notable changes to the python 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).
|
|
4
|
+
|
|
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
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.3.1] — 2026-09-26
|
|
10
|
+
|
|
11
|
+
Speaks ABI revision 5, unchanged from 0.3.0: every artifact 0.3.0 loads, this release loads.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- **The publish-window retry (`docs/guides/fetch.md` §3a) now also covers a release-level file whose hash disagrees with its `SHA256SUMS` row** — `sdk-goldens.json`, checked after the requested artifact has already installed, since it is best-effort and never blocks that install — and the retry budget widened from 3 attempts ~10s apart to 5 attempts with delays doubling from 4s (4/8/16/32s, ~60s of sleep, ~70s of wall time with network latency). The artifacts host's edge cache can hold a stale pairing of `SHA256SUMS`, its signature, `index.json` or `sdk-goldens.json` for up to its measured 60-second `Cache-Control: max-age`, wider than the original ~10s budget covered. Every retry now re-reads the WHOLE consistent set fresh — never one freshly re-fetched object checked against another attempt's stale one. A release-level file mismatch that never heals still leaves the golden tests skipping loudly, exactly as before, and never fails the fetch that asked for the artifact (issue #223).
|
|
16
|
+
|
|
17
|
+
## [0.3.0] — 2026-09-22
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **Identifier and literal quoting now come from ClickHouse itself: `QuoteIdentifier`, `QuoteIdentifierIfNeeded` and `QuoteLiteral` on a loaded library** (issue #52; each binding spells the three in its own idiom — the parity contract's `library.quote-*` rows carry the four spellings). Each is a passthrough over `chs_quote_identifier`, `chs_quote_identifier_if_needed` or `chs_quote_literal` — the vendored `backQuote`, `backQuoteIfNeed` and `quoteString` that the server's own formatter runs — and nothing else. **`QuoteLiteral` is new surface**: a string value as a ClickHouse string literal, quotes and escapes included, which is exactly what a caller splicing a value into DDL text needed (the value-injection recipe in `docs/guides/transformations.md` told the caller to do it themselves, because no binding exported it). Inputs are **counted**, not NUL-terminated, so a value carrying a NUL byte is quoted correctly; the answer is escaped and therefore NUL-free. They hang off a loaded library rather than off the package because the answers are the LOADED BUILD's own, and `QuoteIdentifierIfNeeded`'s differ between ClickHouse lines — `all`, `distinct`, `table` and `null` are back-quoted on every line this release was tested against, `select` from 26.3, `from` and `values` from 26.5, and `where` stays bare throughout. That is the server's own list of names it treats as problematic unquoted, not a keyword class: do not read it as one. The three symbols joined ABI revision 5 without a revision bump, so an artifact built from a revision-5 header that predates them still reports revision 5 and does not export them; all four bindings degrade to their own "this artifact predates the symbol" decline rather than failing to load. **The per-line answers are not pinned by any test in this repository yet** — they need a revision-5 artifact, none of which is fetchable, and are deferred (#119).
|
|
22
|
+
- **This binding now speaks ABI revision 5.** `Schema.row`, `Schema.rows` and `Schema.parse_block` gain a keyword-only `columns` argument (a sequence of column names) — the INSERT column list: the data supplies exactly those columns, in list order for the positional formats, and the server computes the rest with the listed values in scope for their DEFAULT expressions. `None` or an empty sequence is the no-list behavior of every earlier revision and is marshaled to NULL, never to `"[]"` (an empty column list renders as `INSERT INTO t () FORMAT X`, which is code 62 `SYNTAX_ERROR` on every served ClickHouse line — measured 2026-09-15). A listed `EPHEMERAL` column's value is read and is in scope for the DEFAULTs that reference it, and is still never stored and never exported. This binding does not validate column names locally: an unknown column, an `ALIAS` column, and a repeated name are all refused by the server, with its own codes (16, 16 and 15 respectively), surfaced exactly as they come back.
|
|
23
|
+
- This release **requires revision-5 artifacts**. A revision-4 artifact (or any artifact reporting a `chs_abi_revision()` other than 5 or 0) is refused at load, naming both the binding's revision and the artifact's.
|
|
24
|
+
- **`Source.EPHEMERAL_INPUT` / `Source.MATERIALIZED_INPUT`** name `columns_json`'s two new `Value.source` provenances (revision 5, issue #53): a listed `EPHEMERAL` column's read value (`"ephemeral_input"`) and a listed `MATERIALIZED` column's supplied value under `insert_allow_materialized_columns=1` (`"materialized_input"` — stored, replacing the expression). `Source` is a plain class of `Final` strings, not an `Enum`, the same shape as `Reason` — for the same reason: an unrecognized `src` from a newer artifact must pass through, not raise. **The two get opposite treatment in `RowResult.values`**: `"ephemeral_input"` is never stored, so it is excluded from `values` exactly as `"skipped"` already is — reported only through the per-column result document, never through `values`. `"materialized_input"` IS stored, so it stays IN `values`. The artifact-backed behavior tests (a listed EPHEMERAL/MATERIALIZED column actually producing these values end to end) need a revision-5 artifact, none of which is fetchable yet, and are deferred; the `values` membership rule itself was decidable without one, since "never stored" is definitional rather than something an artifact reveals.
|
|
25
|
+
- **`Source.SKIPPED`** names the `Value.source` provenance the `"skipped"` string has always carried (`MATERIALIZED`/`ALIAS`/`EPHEMERAL`, never read from an input row). The exclusion it names from `RowResult.values` predates revision 5 and was already correct — `_row_result` has special-cased it from the start — it simply had no named constant, only the bare string literal `"skipped"` at the one call site that checks it. Added to the `Source` class alongside `EPHEMERAL_INPUT` / `MATERIALIZED_INPUT`, in the same idiom, so the parity manifest's `results.skipped-excluded-from-values` rule can be keyed on a real symbol instead of four bindings agreeing on a bare literal by coincidence (issue #90). No behavior change: `_row_result` compares against the same string either way.
|
|
26
|
+
- **`Source.DEFAULT_SUBSTITUTED`** names the `Value.source` provenance the `"default_substituted"` string has always carried: a VOLATILE DEFAULT this library resolved locally, which the caller MUST echo as an explicit column on any later INSERT. `_row_result` has populated `RowResult.substituted` for exactly this src value from the start; like `Source.SKIPPED` before it, it simply had no named constant, only the bare string literal `"default_substituted"` at the one call site that checks it. Added to the `Source` class in the same idiom, so the parity manifest's `results.default-substituted-populates-substituted` rule can be keyed on a real symbol instead of four bindings agreeing on a bare literal by coincidence (issue #122). No behavior change: `_row_result` compares against the same string either way.
|
|
27
|
+
- **`Schema.rows` gains `row_filter=`** (revision 5, second half — issue #54, `docs/guides/filters.md` §Exporting only the rows a filter admits): attach a compiled `Filter` to the export channel and get ONE `chs_rows` parse that answers both a per-row filter verdict (`RowResult.verdict`, the same `Verdict` `Filter.rows` already answers) and, for `Verdict.TRUE` rows, the export bytes. `Verdict.ERROR` (the predicate threw) and `Verdict.DECLINE` (declined — including a row whose own `outcome` was not `Outcome.ACCEPTED`) are NEVER exported and NEVER collapsed into `Verdict.FALSE`: collapsing either inverts fail-closed into fail-open, the leak class this vocabulary exists to prevent. `BatchResult.rows_passed`/`rows_cut` join the result. A filter compiled over a different `Schema` of the same loaded library rejects the whole call, loudly (code 1002); one from a different loaded library raises `ChtypesError` before any C call. The lifetime mechanism is the one `compile_filter` already established — reused, not duplicated. The artifact-backed behavior tests need a revision-5 artifact, none of which is fetchable yet, and are deferred; the decode rule and the cross-library refusal were both decidable without one, and are unit-tested against hand-built documents and fake handles.
|
|
28
|
+
- **`CSVWithNames` = 10 and `TSVWithNames` = 11 join the `chs_format` set** (issue #55; ClickHouse's own format names, which each binding spells in its own idiom). **Both numbers are frozen from the day they ship**, like every other `chs_format` value: they are what crosses the C boundary, and they are never renumbered. Each is CSV / TSV whose first row is a header naming the columns, so the data is addressed by name rather than by position. With an INSERT column list as well, the list decides the block and the header decides the layout: a listed column the header omits takes its `DEFAULT` under `input_format_defaults_for_omitted_fields=1` and the reader's zero under `0`, an unlisted column the header names is an unknown field, and an unlisted column takes its `DEFAULT` whatever that setting says. **Header-name matching differs by ClickHouse line: exact through 26.4, case-insensitive from 26.5**, exactly as those servers do, so the same header can bind differently on either side of that boundary. As an export format both are the existing loud decline. They joined the set inside ABI revision 5 without a revision bump, so an artifact built from a revision-5 header that predates them still reports revision 5 and does not know them: **probe the artifact before declaring either format** (`docs/reference/bindings.md` §Values a binding must accept and reject). The round trip through a real artifact needs a revision-5 artifact that supports them, none of which is fetchable yet, and is deferred (#119).
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- **BREAKING: `QuoteIdentifier` ALWAYS quotes**, and it is a call into the loaded library rather than a function of this binding (issue #52). It used to quote only where it decided quoting was needed, from a hand-written copy of ClickHouse's rule living in the binding; that copy is deleted, not kept as a fallback, because two implementations of one rule is how it came to disagree with the server in the first place. **This changes the bytes a caller gets back** — a name that used to come back bare now comes back quoted — so it can move a cached key, a golden file or a stored test expectation even where the SQL means the same thing. **The one-line migration for a caller who genuinely wanted the old shape is `QuoteIdentifierIfNeeded`**, which is that behavior under a name that says so; read the next two entries before assuming it is byte-for-byte what you had.
|
|
33
|
+
|
|
34
|
+
- **`QuoteIdentifierIfNeeded` returns `` `all` ``, `` `distinct` ``, `` `table` `` and `` `null` `` back-quoted**, where the hand-written copy returned all four bare. The server back-quotes them on every ClickHouse line this release was tested against, so the copy was simply wrong, and a bare `null` is exactly the shape that parses as something else. This is a behavior change **on top of** the breaking one above: a caller who migrates from `QuoteIdentifier` to `QuoteIdentifierIfNeeded` to keep the old shape still gets different bytes for these four names. Which further names it quotes is the loaded build's own rule and moves between lines (`select` from 26.3, `from` and `values` from 26.5, while `where` stays bare), so an answer cached across versions was never safe and is now visibly so.
|
|
35
|
+
|
|
36
|
+
- **`QuoteIdentifier` emits the BACKSLASH spelling for an embedded back-quote** — `` `a\`b` `` — where the deleted copy doubled it (``` `a``b` ```). Both spellings parse, on every line, and both round-trip to the same column name; only one is what the server itself prints. That is how two hand-written quoters can each look correct and still disagree, and it is the same failure that produced the `null` divergence above. A caller comparing quoted text against a stored string, rather than passing it straight into DDL, must expect the new spelling.
|
|
37
|
+
|
|
38
|
+
- **BREAKING: reconstructing DDL from discovered columns is a method on a loaded library.** It was a free function; it is not one any more, because the single thing it spells — the column NAME — is now spelled by the library's own `QuoteIdentifier` instead of by a rule in the binding. Resolve the artifact first (the server's own version string already decides which one) and call reconstruction on it; the arguments, the return and every refusal are unchanged, and the declaration list it produces differs only in how the names are spelled. This was not in the issue's own scope — it is the unavoidable consequence of deleting the hand-written rule, since nothing else in the binding could spell a name afterwards.
|
|
39
|
+
|
|
40
|
+
- **BREAKING: loading is lazy by default, and `preload` is the one eager path.** Constructing a `Registry` already read manifests and `dlopen`ed nothing — but `libraries()` did not: it opened every discovered line, roughly 120 MB of artifact each, to answer a question about what the registry knows. **`libraries()` now lists what is OPEN and opens nothing**, which is what Go, TypeScript and Rust have always answered, and which also fixes `__iter__`, whose every use inherited those loads. The rule, now true of every surface here: nothing in this library opens an artifact except a request for a specific version, or an explicit `preload` — not `versions()`, not `libraries()`, not `in`, not `len()`, not `repr()` (#50).
|
|
41
|
+
|
|
42
|
+
**What a caller who relied on `libraries()` loading must do:** ask for the lines. `for line in registry.versions(): registry.for_version(line)` opens them, or `Registry(dir, preload=["25.8", "26.7"])` opens a named set in order before the constructor returns. `preload` is deliberately a list rather than "everything in the directory", because a registry directory is whatever a fetch left behind. An entry no directory on the search path holds is `ArtifactMissingError` at construction — the same §7 error the first `for_version` would have raised, raised earlier — and it never fetches, even with `autofetch=True`.
|
|
43
|
+
|
|
44
|
+
- **BREAKING: two new construction errors, both decidable from manifests alone.** A search path on which no directory holds a readable `<minor>/manifest.json` is now `RegistryError` at construction, naming every directory looked in; this was the one binding where `Registry()` could succeed against a completely empty machine and report the miss three calls later. And a directory the CALLER NAMED that does not exist is now an error too, as it already was in Go and TypeScript, rather than being skipped in silence. Both are suppressed with `autofetch=True`, where the directory is the destination the first fetch creates, and `for_version` still re-scans, so a line installed after construction keeps working (#50).
|
|
45
|
+
|
|
46
|
+
- **`verify_hashes` is unchanged, and the timing is now stated: a library's checksum is computed immediately before that library is `dlopen`ed, and at no other time** — at construction for the `preload`ed lines, at first use for the rest, never for a line nobody asks for. It is a policy on the registry, not a property of the preload list: a lazily-opened line is hashed too (#50).
|
|
47
|
+
|
|
48
|
+
- **BREAKING: `library_bytes` is now checked on EVERY load, not only under `verify_hashes`** (issue #82, split out of #50 so the behavior change was not hidden inside a larger one). The size check — the file's actual length against the manifest's `library_bytes` — used to run only when verification was on, bundled inside `verify_library`'s hash comparison; Go and TypeScript agreed with Python on that, and Rust did not, checking bytes unconditionally on every load. It is nearly free (one `stat`, never a re-hash of the library's contents) and it catches the commonest shape of a broken artifact directory — a truncated or partially-written library file — so all four bindings now check it unconditionally, and Python stops being three of the four that didn't. The new standalone `check_library_bytes` carries this half; `verify_library` calls it too, so its own behavior is unchanged for a caller who invokes it directly.
|
|
49
|
+
|
|
50
|
+
**What changes for a caller who never asked for verification:** an artifact directory whose manifest carries a `library_bytes` that does not match the file on disk now fails the load (`for_version`, and `preload`, which routes through it) with `chtypes: <path> is <n> bytes, manifest says <m>`, refused before `dlopen` is ever attempted. Before this change the same directory reached `dlopen` regardless — usually failing there too, with a less specific error, though a stale-rather-than-corrupt manifest could pass through unnoticed. A manifest with no `library_bytes` at all (`0`, its default for one that predates the field) is unaffected either way: this was never, and is still not, a reason a load fails for a directory with no size to check against.
|
|
51
|
+
|
|
52
|
+
**Timing, read together with `verify_hashes`'s rule above:** the size check runs at the exact same point the hash check does — immediately before `dlopen`, at construction for `preload`ed lines and at first use for the rest — because `_load` is the one place both checks live, whether or not verification is on.
|
|
53
|
+
|
|
54
|
+
- **CSV and TSV rejection messages are now ClickHouse's own text** (with revision-5 artifacts). The error **codes are unchanged**, and so is every verdict and every stored value; only the human-readable message moved, because CSV and TSV rows are now read by ClickHouse's own readers rather than hand-written splitters. A CSV row the reader rejects now carries an **empty `cols` list**, where the previous path could report a partial one. **Match on the error code, never on the message text** — the message is ClickHouse's to change between releases.
|
|
55
|
+
|
|
56
|
+
- **An empty CSV field under `input_format_defaults_for_omitted_fields=0` is now reported as an input, and its coercion as a transform** (with revision-5 artifacts). Where the previous reader reported no transform, the detector now reports one, because the field's reference value is the empty string rather than absent. The reason follows the column's type — measured as `enum_coerce` for an `Enum`, `fixedstring_pad` for a `FixedString`, and `uuid_mangle` for a `UUID` (the zero UUID). **Stored values are unchanged** — this changes what is reported, not what is stored. A caller that reads "no transform" as "the value was not changed" should expect these rows to report one. Under the default setting (`1`) an empty field still takes the column's `DEFAULT`, as before. **TSV is not affected:** its reader takes an empty field through the typed parse whether the setting is on or off, exactly as the previous splitter did.
|
|
57
|
+
|
|
58
|
+
- **An unlisted column now takes its `DEFAULT` regardless of `input_format_defaults_for_omitted_fields` when a column list is present** (with revision-5 artifacts). With an INSERT column list, a column the list does not name is computed from its `DEFAULT` expression under `input_format_defaults_for_omitted_fields=0` as well as under the default `1`. This is a fix in the artifact that rides revision 5, and it applies to **every format**, not only `CSVWithNames` and `TSVWithNames`.
|
|
59
|
+
|
|
60
|
+
- **The rolling artifacts channel now serves ABI revision 5.** A 0.2.x consumer that fetches from the default channel will download an artifact its bindings **refuse at load** — correct behavior that looks exactly like a broken install. ⚠️ **Pinning an exact ClickHouse version does not avoid this**: a relink republishes the same `clickhouse_version`, and an exact request resolves to the highest-ranked matching row, which is the revision-5 one. The only way to stay on revision 4 is `--url` / `--tag` pointed at a source that still serves revision-4 artifacts, if one is retained. **Upgrading to 0.3.0 is the supported path.**
|
|
61
|
+
|
|
62
|
+
## [0.2.2] — 2026-09-17
|
|
63
|
+
|
|
64
|
+
### Changed
|
|
65
|
+
|
|
66
|
+
- **`Registry(..., verify_hashes=True)` now refuses an artifact whose manifest carries no `library_sha256`, instead of loading it silently.** This is a behavior change in a security posture, not a silent fix: previously, asking for verification against a manifest with the field absent returned as if the bytes had been checked, when they never were — the caller believed the load was verified and it was not. `verify_library` now raises `RegistryError` naming the path and saying the manifest carries no `library_sha256`, the same shape its hash-mismatch path has always raised. `verify_hashes` off is unaffected: the field stays optional for a caller who did not ask. Go, TypeScript and Rust already refused this case; this was the one gap issue #13's item A2 left (#48).
|
|
67
|
+
|
|
68
|
+
### Notes
|
|
69
|
+
|
|
70
|
+
- 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.
|
|
71
|
+
- Seven facts about the compiled library's behavior that a consumer previously had to discover by experiment are now written down: `CHECK`-constraint batch rejection, binding filter parameters as `String`, single-line compact JSON array loss under `allow_errors`, the JSONCompactEachRow export field separator, the value-injection pattern and its compile-time wrap trap, `Columns` as canonical and in declaration order, and the frozen-signatures promise reworded ahead of the next ABI revision (#56). That promise is now stated once, in `docs/support.md`, and linked from everywhere else rather than restated in nine places (#69).
|
|
72
|
+
|
|
73
|
+
## [0.2.1] — 2026-09-15
|
|
74
|
+
|
|
75
|
+
Released in step with the Rust crate, which gains `Registry::open` and `Error::LibraryRead`; this binding's public API is unchanged.
|
|
76
|
+
|
|
77
|
+
### Notes
|
|
78
|
+
|
|
79
|
+
- 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.
|
|
80
|
+
- **The ABI-revision refusal is now run, not assumed.** This binding's suite points a registry at a generated artifact answering the wrong revision and asserts the load is refused naming both numbers, beside a matching-revision control that must load; CI fails the build if either case did not run (#36).
|
|
81
|
+
|
|
82
|
+
## [0.2.0] — 2026-09-15
|
|
83
|
+
|
|
84
|
+
### Changed — BREAKING
|
|
85
|
+
|
|
86
|
+
- **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.
|
|
87
|
+
|
|
88
|
+
- **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.
|
|
89
|
+
|
|
90
|
+
### Added
|
|
91
|
+
|
|
92
|
+
- **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.
|
|
93
|
+
- **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.
|
|
94
|
+
- **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.
|
|
95
|
+
|
|
96
|
+
### Notes
|
|
97
|
+
|
|
98
|
+
- 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.
|
|
99
|
+
- `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.
|
|
100
|
+
|
|
101
|
+
## [0.1.2] — 2026-09-11
|
|
102
|
+
|
|
103
|
+
### Changed — BREAKING
|
|
104
|
+
|
|
105
|
+
- **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.DEFAULT_MATERIALIZED`. 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.
|
|
106
|
+
- 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.
|
|
107
|
+
|
|
108
|
+
### Changed
|
|
109
|
+
|
|
110
|
+
- **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.
|
|
111
|
+
- **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.)
|
|
112
|
+
- `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.
|
|
113
|
+
|
|
114
|
+
## [0.1.1] — 2026-09-11
|
|
115
|
+
|
|
116
|
+
- **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.
|
|
117
|
+
- **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.
|
|
118
|
+
- 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.
|
|
119
|
+
- README install instructions name the published package rather than a path dependency.
|
|
120
|
+
- 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).
|
|
121
|
+
- `docs/support.md` is new: which language versions, platforms and ClickHouse lines are supported, generated from the manifests and the release's own index.
|
|
122
|
+
|
|
123
|
+
## [0.1.0] — 2026-09-10
|
|
124
|
+
|
|
125
|
+
First release, published to PyPI from the tag `python/v0.1.0`.
|
|
126
|
+
|
|
127
|
+
- 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.
|
|
128
|
+
- 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.
|
|
129
|
+
- Pre-1.0: the artifact name `libchtypes`, the `chs_` prefix and the `enum chs_format` numbers are frozen. Function signatures freeze at this tag.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: chtypes
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Python binding for the chtypes artifacts: ClickHouse's own type system, per version, behind the frozen chs_* C ABI
|
|
5
5
|
Project-URL: Homepage, https://github.com/wave-rf/chtypes
|
|
6
6
|
Project-URL: Repository, https://github.com/wave-rf/chtypes
|
|
@@ -76,7 +76,7 @@ A bad **row** is a verdict, not an exception: `outcome` becomes `Outcome.REJECTE
|
|
|
76
76
|
| [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 |
|
|
77
77
|
| [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 |
|
|
78
78
|
|
|
79
|
-
##
|
|
79
|
+
## Four things specific to this binding
|
|
80
80
|
|
|
81
81
|
**A settings value must never be a `float`.** `encode_settings` stringifies an `int` exactly and raises `TypeError` on a `float`: a 19-digit `chtypes_now_epoch_nanos` does not survive an IEEE double, and as a JSON number the setting would be silently ignored.
|
|
82
82
|
|
|
@@ -84,6 +84,8 @@ A bad **row** is a verdict, not an exception: `outcome` becomes `Outcome.REJECTE
|
|
|
84
84
|
|
|
85
85
|
**`ctypes` releases the GIL for the whole duration of a foreign call**, so the GIL is not the exclusion. The package uses a writer-preferring readers-writer lock per loaded image plus one plain lock per `Schema`; `set_default_settings` and `close` take it exclusively, as the ABI requires. Measured under contention: 27,770 batch reads across 8 threads against 566 concurrent settings swaps, every answer byte-identical to the uncontended one.
|
|
86
86
|
|
|
87
|
+
**The INSERT column list (ABI revision 5) is a keyword-only `columns` on the same calls** — `schema.row(fmt, raw, columns=["id", "e"])`, and the same argument on `Schema.rows` and `Schema.parse_block`. `None` or an empty sequence is the no-list behavior of every earlier revision; a list makes the data supply exactly those columns, with a listed `EPHEMERAL` value read and in scope for the DEFAULTs that reference it but never stored. This release requires a revision-5 artifact — a revision-4 one is refused at load, naming both revisions.
|
|
88
|
+
|
|
87
89
|
## Tests
|
|
88
90
|
|
|
89
91
|
`uv run pytest -q`. 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/` through `file://` sources and the test key.
|
|
@@ -58,7 +58,7 @@ A bad **row** is a verdict, not an exception: `outcome` becomes `Outcome.REJECTE
|
|
|
58
58
|
| [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 |
|
|
59
59
|
| [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 |
|
|
60
60
|
|
|
61
|
-
##
|
|
61
|
+
## Four things specific to this binding
|
|
62
62
|
|
|
63
63
|
**A settings value must never be a `float`.** `encode_settings` stringifies an `int` exactly and raises `TypeError` on a `float`: a 19-digit `chtypes_now_epoch_nanos` does not survive an IEEE double, and as a JSON number the setting would be silently ignored.
|
|
64
64
|
|
|
@@ -66,6 +66,8 @@ A bad **row** is a verdict, not an exception: `outcome` becomes `Outcome.REJECTE
|
|
|
66
66
|
|
|
67
67
|
**`ctypes` releases the GIL for the whole duration of a foreign call**, so the GIL is not the exclusion. The package uses a writer-preferring readers-writer lock per loaded image plus one plain lock per `Schema`; `set_default_settings` and `close` take it exclusively, as the ABI requires. Measured under contention: 27,770 batch reads across 8 threads against 566 concurrent settings swaps, every answer byte-identical to the uncontended one.
|
|
68
68
|
|
|
69
|
+
**The INSERT column list (ABI revision 5) is a keyword-only `columns` on the same calls** — `schema.row(fmt, raw, columns=["id", "e"])`, and the same argument on `Schema.rows` and `Schema.parse_block`. `None` or an empty sequence is the no-list behavior of every earlier revision; a list makes the data supply exactly those columns, with a listed `EPHEMERAL` value read and in scope for the DEFAULTs that reference it but never stored. This release requires a revision-5 artifact — a revision-4 one is refused at load, naming both revisions.
|
|
70
|
+
|
|
69
71
|
## Tests
|
|
70
72
|
|
|
71
73
|
`uv run pytest -q`. 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/` through `file://` sources and the test key.
|
|
@@ -54,7 +54,6 @@ from .discover import (
|
|
|
54
54
|
parse_changed_settings_result,
|
|
55
55
|
parse_columns_result,
|
|
56
56
|
parse_version_result,
|
|
57
|
-
reconstruct_ddl,
|
|
58
57
|
)
|
|
59
58
|
from .errors import (
|
|
60
59
|
CODE_ARTIFACT_CORRUPT,
|
|
@@ -120,6 +119,7 @@ from .results import (
|
|
|
120
119
|
Outcome,
|
|
121
120
|
Reason,
|
|
122
121
|
RowResult,
|
|
122
|
+
Source,
|
|
123
123
|
Span,
|
|
124
124
|
Substitution,
|
|
125
125
|
Transform,
|
|
@@ -181,6 +181,7 @@ __all__ = [
|
|
|
181
181
|
"Schema",
|
|
182
182
|
"SchemaError",
|
|
183
183
|
"ServerProfile",
|
|
184
|
+
"Source",
|
|
184
185
|
"SourceUnreachableError",
|
|
185
186
|
"Span",
|
|
186
187
|
"Substitution",
|
|
@@ -199,7 +200,6 @@ __all__ = [
|
|
|
199
200
|
"parse_version_result",
|
|
200
201
|
"quote_bare_denormals",
|
|
201
202
|
"read_manifest",
|
|
202
|
-
"reconstruct_ddl",
|
|
203
203
|
"registry_search_path",
|
|
204
204
|
"verify_library",
|
|
205
205
|
]
|
|
@@ -20,6 +20,7 @@ from .results import (
|
|
|
20
20
|
FilterRowError,
|
|
21
21
|
Outcome,
|
|
22
22
|
RowResult,
|
|
23
|
+
Source,
|
|
23
24
|
Span,
|
|
24
25
|
Substitution,
|
|
25
26
|
Transform,
|
|
@@ -153,8 +154,13 @@ def _row_result(doc: RawObject) -> RowResult:
|
|
|
153
154
|
for entry in cols if isinstance(cols, list) else ():
|
|
154
155
|
col = _col_doc(entry)
|
|
155
156
|
# MATERIALIZED / ALIAS / EPHEMERAL are never read from an input row and
|
|
156
|
-
# are not part of the stored row a subscriber would SELECT.
|
|
157
|
-
|
|
157
|
+
# are not part of the stored row a subscriber would SELECT. A listed
|
|
158
|
+
# EPHEMERAL column's ephemeral_input value IS read but is never
|
|
159
|
+
# stored either, so it is excluded here too — exactly as Source.SKIPPED
|
|
160
|
+
# is — never sitting where a caller reads the stored row (issue #53).
|
|
161
|
+
# materialized_input stays IN: it IS stored, replacing the column's
|
|
162
|
+
# expression.
|
|
163
|
+
if col.src in (Source.SKIPPED, Source.EPHEMERAL_INPUT):
|
|
158
164
|
continue
|
|
159
165
|
values.append(
|
|
160
166
|
Value(
|
|
@@ -164,7 +170,7 @@ def _row_result(doc: RawObject) -> RowResult:
|
|
|
164
170
|
source=col.src,
|
|
165
171
|
)
|
|
166
172
|
)
|
|
167
|
-
if col.src ==
|
|
173
|
+
if col.src == Source.DEFAULT_SUBSTITUTED:
|
|
168
174
|
substituted.append(Substitution(column=col.name, expr=col.input, text=col.stored))
|
|
169
175
|
transformed.extend(classify(col))
|
|
170
176
|
|
|
@@ -180,6 +186,16 @@ def _row_result(doc: RawObject) -> RowResult:
|
|
|
180
186
|
)
|
|
181
187
|
)
|
|
182
188
|
|
|
189
|
+
# `verdict` (RowsExportWith's Python spelling: `rows(..., row_filter=)`)
|
|
190
|
+
# is present exactly when a filter was attached to the call that
|
|
191
|
+
# produced this document — absent from every Row/Rows document and from
|
|
192
|
+
# `rows()` with no `row_filter`. `Verdict.of` degrades an unrecognized
|
|
193
|
+
# character to DECLINE, never to an invented answer.
|
|
194
|
+
verdict_char = _text(doc, "verdict")
|
|
195
|
+
verdict = Verdict.of(verdict_char) if verdict_char else None
|
|
196
|
+
verdict_code = _count(doc, "verdict_code")
|
|
197
|
+
verdict_err = _text(doc, "verdict_err")
|
|
198
|
+
|
|
183
199
|
return RowResult(
|
|
184
200
|
outcome=outcome,
|
|
185
201
|
err_code=_count(doc, "code"),
|
|
@@ -190,6 +206,9 @@ def _row_result(doc: RawObject) -> RowResult:
|
|
|
190
206
|
unsupported_settings=unsupported_settings,
|
|
191
207
|
substituted=tuple(substituted),
|
|
192
208
|
computed=tuple(computed),
|
|
209
|
+
verdict=verdict,
|
|
210
|
+
verdict_code=verdict_code,
|
|
211
|
+
verdict_err=verdict_err,
|
|
193
212
|
)
|
|
194
213
|
|
|
195
214
|
|
|
@@ -222,6 +241,9 @@ def parse_batch_document(raw: bytes, payload: bytes | None = None) -> BatchResul
|
|
|
222
241
|
unsupported_settings=row.unsupported_settings,
|
|
223
242
|
substituted=row.substituted,
|
|
224
243
|
computed=row.computed,
|
|
244
|
+
verdict=row.verdict,
|
|
245
|
+
verdict_code=row.verdict_code,
|
|
246
|
+
verdict_err=row.verdict_err,
|
|
225
247
|
)
|
|
226
248
|
)
|
|
227
249
|
transformed.extend(indexed)
|
|
@@ -273,6 +295,8 @@ def parse_batch_document(raw: bytes, payload: bytes | None = None) -> BatchResul
|
|
|
273
295
|
payload=payload,
|
|
274
296
|
spans=spans,
|
|
275
297
|
export_declined=_text(doc, "export_declined"),
|
|
298
|
+
rows_passed=_count(doc, "rows_passed"),
|
|
299
|
+
rows_cut=_count(doc, "rows_cut"),
|
|
276
300
|
)
|
|
277
301
|
|
|
278
302
|
|
|
@@ -85,26 +85,56 @@ def read_manifest(version_dir: str | os.PathLike[str]) -> Manifest | None:
|
|
|
85
85
|
return None
|
|
86
86
|
|
|
87
87
|
|
|
88
|
+
def check_library_bytes(
|
|
89
|
+
version_dir: str | os.PathLike[str], manifest: Manifest | None = None
|
|
90
|
+
) -> None:
|
|
91
|
+
"""Compare the shared library's on-disk size to the manifest's ``library_bytes``.
|
|
92
|
+
|
|
93
|
+
Unconditional on the load path, unlike ``verify_library``'s hash check
|
|
94
|
+
below (issue #82): nearly free (one ``stat``, never a re-hash of the
|
|
95
|
+
library's contents), and it catches the commonest shape of a broken
|
|
96
|
+
artifact directory -- a truncated or partially-written library file.
|
|
97
|
+
|
|
98
|
+
A manifest with no ``library_bytes`` (0, its default for one that
|
|
99
|
+
predates the field) is not asked, so this is a no-op for one: a
|
|
100
|
+
directory with no size to compare against must never become a new
|
|
101
|
+
reason a load fails.
|
|
102
|
+
"""
|
|
103
|
+
directory = Path(version_dir)
|
|
104
|
+
if manifest is None:
|
|
105
|
+
manifest = read_manifest(directory)
|
|
106
|
+
if manifest is None:
|
|
107
|
+
raise RegistryError(f"chtypes: no usable manifest.json in {directory}")
|
|
108
|
+
if not manifest.library_bytes:
|
|
109
|
+
return
|
|
110
|
+
path = directory / manifest.library
|
|
111
|
+
try:
|
|
112
|
+
size = path.stat().st_size
|
|
113
|
+
except OSError as exc:
|
|
114
|
+
raise RegistryError(f"chtypes: {path}: {exc}") from exc
|
|
115
|
+
if size != manifest.library_bytes:
|
|
116
|
+
raise RegistryError(
|
|
117
|
+
f"chtypes: {path} is {size} bytes, manifest says {manifest.library_bytes}"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
88
121
|
def verify_library(version_dir: str | os.PathLike[str]) -> None:
|
|
89
122
|
"""Re-hash the shared library and compare it against the manifest.
|
|
90
123
|
|
|
91
124
|
The artifact carries its own checksum, so this is neither optional nor
|
|
92
125
|
expensive for anything that arrived over a network: a move that reported
|
|
93
126
|
success and truncated a 232 MB library looks identical to one that worked.
|
|
127
|
+
The size check ahead of the hash, ``check_library_bytes``, ALSO runs
|
|
128
|
+
unconditionally on the load path outside of verification (issue #82);
|
|
129
|
+
calling it here too keeps this function's own behavior unchanged for a
|
|
130
|
+
caller who invokes it directly.
|
|
94
131
|
"""
|
|
95
132
|
directory = Path(version_dir)
|
|
96
133
|
manifest = read_manifest(directory)
|
|
97
134
|
if manifest is None:
|
|
98
135
|
raise RegistryError(f"chtypes: no usable manifest.json in {directory}")
|
|
136
|
+
check_library_bytes(directory, manifest)
|
|
99
137
|
path = directory / manifest.library
|
|
100
|
-
try:
|
|
101
|
-
size = path.stat().st_size
|
|
102
|
-
except OSError as exc:
|
|
103
|
-
raise RegistryError(f"chtypes: {path}: {exc}") from exc
|
|
104
|
-
if manifest.library_bytes and size != manifest.library_bytes:
|
|
105
|
-
raise RegistryError(
|
|
106
|
-
f"chtypes: {path} is {size} bytes, manifest says {manifest.library_bytes}"
|
|
107
|
-
)
|
|
108
138
|
if not manifest.library_sha256:
|
|
109
139
|
raise RegistryError(
|
|
110
140
|
f"chtypes: {path} cannot be verified: manifest carries no library_sha256"
|
|
@@ -60,6 +60,22 @@ _SIGNATURES: Final[dict[str, tuple[object, list[object]]]] = {
|
|
|
60
60
|
"chs_free": (None, [ctypes.c_void_p]),
|
|
61
61
|
"chs_validate_type": (ctypes.c_int, [ctypes.c_char_p, _c_owned_p, _c_int_p, _c_owned_p]),
|
|
62
62
|
"chs_reference_type": (ctypes.c_void_p, [ctypes.c_char_p]),
|
|
63
|
+
# Revision 5, additive: the quoting trio, straight off the vendored
|
|
64
|
+
# backQuote / backQuoteIfNeed / quoteString. The input is COUNTED — a
|
|
65
|
+
# literal may carry a NUL byte, so the length is the contract and
|
|
66
|
+
# `strlen` is not — and the answer arrives in a `char **` out-param.
|
|
67
|
+
"chs_quote_identifier": (
|
|
68
|
+
ctypes.c_int,
|
|
69
|
+
[ctypes.c_char_p, ctypes.c_size_t, _c_owned_p, _c_owned_p],
|
|
70
|
+
),
|
|
71
|
+
"chs_quote_identifier_if_needed": (
|
|
72
|
+
ctypes.c_int,
|
|
73
|
+
[ctypes.c_char_p, ctypes.c_size_t, _c_owned_p, _c_owned_p],
|
|
74
|
+
),
|
|
75
|
+
"chs_quote_literal": (
|
|
76
|
+
ctypes.c_int,
|
|
77
|
+
[ctypes.c_char_p, ctypes.c_size_t, _c_owned_p, _c_owned_p],
|
|
78
|
+
),
|
|
63
79
|
"chs_registered_families": (ctypes.c_void_p, []),
|
|
64
80
|
"chs_function_flags": (ctypes.c_void_p, []),
|
|
65
81
|
# settings_json + mode compile a column list under a DECLARED settings
|
|
@@ -84,14 +100,30 @@ _SIGNATURES: Final[dict[str, tuple[object, list[object]]]] = {
|
|
|
84
100
|
"chs_schema_column_default_kind": (ctypes.c_char_p, [ctypes.c_void_p, ctypes.c_int]),
|
|
85
101
|
"chs_schema_column_default_expr": (ctypes.c_char_p, [ctypes.c_void_p, ctypes.c_int]),
|
|
86
102
|
"chs_schema_column_default_is_literal": (ctypes.c_int, [ctypes.c_void_p, ctypes.c_int]),
|
|
103
|
+
# Revision 5: chs_row gains a trailing columns_json (the INSERT column
|
|
104
|
+
# list). The ABI-revision gate below guarantees this 6-argument
|
|
105
|
+
# declaration describes the loaded artifact before any call is made
|
|
106
|
+
# through it: a rev-4 artifact (5-argument shape) is refused at load.
|
|
87
107
|
"chs_row": (
|
|
88
108
|
ctypes.c_void_p,
|
|
89
|
-
[
|
|
109
|
+
[
|
|
110
|
+
ctypes.c_void_p,
|
|
111
|
+
ctypes.c_int,
|
|
112
|
+
ctypes.c_char_p,
|
|
113
|
+
ctypes.c_size_t,
|
|
114
|
+
ctypes.c_char_p,
|
|
115
|
+
ctypes.c_char_p,
|
|
116
|
+
],
|
|
90
117
|
),
|
|
91
118
|
# Revision 3: chs_rows carries export_format / doc_flags / out_bytes
|
|
92
|
-
# (the C ABI contract §Rows).
|
|
93
|
-
#
|
|
94
|
-
#
|
|
119
|
+
# (the C ABI contract §Rows). Revision 5 appends columns_json after
|
|
120
|
+
# out_bytes and then, in the same open window, the attached row filter
|
|
121
|
+
# LAST. This binding never attaches one and passes NULL — today's
|
|
122
|
+
# behavior byte for byte — but the parameter is DECLARED, because calling
|
|
123
|
+
# a ten-parameter symbol through a nine-parameter declaration leaves the
|
|
124
|
+
# callee reading the filter slot from whatever happened to occupy it. The
|
|
125
|
+
# ABI-revision gate below is what guarantees this 10-argument declaration
|
|
126
|
+
# describes the loaded artifact before any call is made through it.
|
|
95
127
|
"chs_rows": (
|
|
96
128
|
ctypes.c_void_p,
|
|
97
129
|
[
|
|
@@ -103,6 +135,8 @@ _SIGNATURES: Final[dict[str, tuple[object, list[object]]]] = {
|
|
|
103
135
|
ctypes.c_int,
|
|
104
136
|
ctypes.c_uint,
|
|
105
137
|
ctypes.POINTER(_ChsBytes),
|
|
138
|
+
ctypes.c_char_p,
|
|
139
|
+
ctypes.c_void_p,
|
|
106
140
|
],
|
|
107
141
|
),
|
|
108
142
|
# Revision 3: the filter trio (the C ABI contract §Filters). Optional symbols —
|
|
@@ -125,6 +159,7 @@ _SIGNATURES: Final[dict[str, tuple[object, list[object]]]] = {
|
|
|
125
159
|
),
|
|
126
160
|
# Revision 4: the block twin (the C ABI contract §Blocks) — parse a body once,
|
|
127
161
|
# evaluate K filters against the block. Optional, same degradation rule.
|
|
162
|
+
# Revision 5 appends columns_json, LAST, after out_err.
|
|
128
163
|
"chs_block_parse": (
|
|
129
164
|
ctypes.c_void_p,
|
|
130
165
|
[
|
|
@@ -135,6 +170,7 @@ _SIGNATURES: Final[dict[str, tuple[object, list[object]]]] = {
|
|
|
135
170
|
ctypes.c_char_p,
|
|
136
171
|
_c_int_p,
|
|
137
172
|
_c_owned_p,
|
|
173
|
+
ctypes.c_char_p,
|
|
138
174
|
],
|
|
139
175
|
),
|
|
140
176
|
"chs_block_free": (None, [ctypes.c_void_p]),
|
|
@@ -151,7 +187,9 @@ _SIGNATURES: Final[dict[str, tuple[object, list[object]]]] = {
|
|
|
151
187
|
# 4 = the filter phase-2 cycle, 2026-08-31: chs_filter_compile gained
|
|
152
188
|
# params_json ({name:Type} query parameters) and the block twin joined
|
|
153
189
|
# (chs_block_parse / chs_block_free / chs_filter_eval).
|
|
154
|
-
|
|
190
|
+
# 5 = the explicit INSERT column list, 2026-09-15: chs_row, chs_rows and
|
|
191
|
+
# chs_block_parse each gained a trailing columns_json.
|
|
192
|
+
ABI_REVISION: Final = 5
|
|
155
193
|
|
|
156
194
|
_MANDATORY: Final = (
|
|
157
195
|
"chs_clickhouse_version",
|
|
@@ -434,6 +472,31 @@ class NativeLibrary:
|
|
|
434
472
|
raw = self._take(fn(type_expr.encode()))
|
|
435
473
|
return (raw or b"").decode("utf-8", "surrogateescape")
|
|
436
474
|
|
|
475
|
+
# -- quoting -----------------------------------------------------------
|
|
476
|
+
|
|
477
|
+
def quote(self, symbol: str, text: bytes) -> tuple[str, int, str]:
|
|
478
|
+
"""(quoted, code, err) from one of the three `chs_quote_*` symbols.
|
|
479
|
+
|
|
480
|
+
The input is COUNTED, so `text` is bytes and its length is passed
|
|
481
|
+
explicitly: a string literal may legally carry a NUL byte and
|
|
482
|
+
`strlen` would truncate it. The answer is always NUL-free — every
|
|
483
|
+
byte the server escapes comes back escaped — so reading it as a C
|
|
484
|
+
string is exact, not a best effort.
|
|
485
|
+
"""
|
|
486
|
+
fn = self._need(symbol, f"this artifact predates {symbol} (rebuild it)")
|
|
487
|
+
quoted = ctypes.c_void_p()
|
|
488
|
+
err = ctypes.c_void_p()
|
|
489
|
+
with self._lock.read():
|
|
490
|
+
rc = int(fn(text, len(text), ctypes.byref(quoted), ctypes.byref(err)))
|
|
491
|
+
# Both out-params are taken on EVERY path, success or failure:
|
|
492
|
+
# the library owns whatever it wrote, and an answer left behind
|
|
493
|
+
# on an error return is a leak nothing else can reach.
|
|
494
|
+
raw = self._take(quoted.value)
|
|
495
|
+
message = self._take_err(err)
|
|
496
|
+
if rc != 0:
|
|
497
|
+
return "", rc, message
|
|
498
|
+
return (raw or b"").decode("utf-8", "surrogateescape"), 0, ""
|
|
499
|
+
|
|
437
500
|
def registered_families(self) -> str:
|
|
438
501
|
"""The newline-separated family list, verbatim."""
|
|
439
502
|
fn = self._need(
|
|
@@ -538,7 +601,18 @@ class NativeLibrary:
|
|
|
538
601
|
|
|
539
602
|
# -- rows --------------------------------------------------------------
|
|
540
603
|
|
|
541
|
-
def row(
|
|
604
|
+
def row(
|
|
605
|
+
self,
|
|
606
|
+
handle: int,
|
|
607
|
+
fmt: int,
|
|
608
|
+
raw: bytes,
|
|
609
|
+
settings_json: str,
|
|
610
|
+
columns_json: str | None = None,
|
|
611
|
+
) -> bytes:
|
|
612
|
+
"""`columns_json` (revision 5) is the INSERT column list, already
|
|
613
|
+
JSON-encoded, or None for "no list" — passed through to the C side as
|
|
614
|
+
NULL, never as the literal string `"[]"` (the caller-facing rule is
|
|
615
|
+
`Schema.row`'s; this layer only forwards what it is given)."""
|
|
542
616
|
fn = self._fn.get("chs_row")
|
|
543
617
|
if fn is None:
|
|
544
618
|
raise UnsupportedError("this artifact predates chs_row (rebuild it)")
|
|
@@ -546,7 +620,14 @@ class NativeLibrary:
|
|
|
546
620
|
# NUL bytes are legal inside a RowBinary body, so the length is
|
|
547
621
|
# passed explicitly and `strlen` is never involved.
|
|
548
622
|
out = self._take(
|
|
549
|
-
fn(
|
|
623
|
+
fn(
|
|
624
|
+
ctypes.c_void_p(handle),
|
|
625
|
+
fmt,
|
|
626
|
+
raw,
|
|
627
|
+
len(raw),
|
|
628
|
+
settings_json.encode(),
|
|
629
|
+
columns_json.encode() if columns_json is not None else None,
|
|
630
|
+
)
|
|
550
631
|
)
|
|
551
632
|
if out is None:
|
|
552
633
|
raise UnsupportedError("this artifact predates chs_row (rebuild it)")
|
|
@@ -560,11 +641,20 @@ class NativeLibrary:
|
|
|
560
641
|
settings_json: str,
|
|
561
642
|
export_format: int,
|
|
562
643
|
doc_flags: int,
|
|
644
|
+
columns_json: str | None = None,
|
|
645
|
+
filter_handle: int | None = None,
|
|
563
646
|
) -> tuple[bytes, bytes | None]:
|
|
564
647
|
"""(document, payload). `export_format` is -1 (CHS_EXPORT_NONE — no
|
|
565
648
|
export, payload None) or an `enum chs_format` value; `doc_flags` the
|
|
566
649
|
CHS_DOC_* bitmask. Both ride through UNVALIDATED — an unknown value is
|
|
567
650
|
the library's loud refusal to make, never this binding's guess.
|
|
651
|
+
`columns_json` (revision 5) is the INSERT column list, already
|
|
652
|
+
JSON-encoded, or None for "no list" — forwarded as NULL, never `"[]"`.
|
|
653
|
+
|
|
654
|
+
`filter_handle` (revision 5, second half) is the attached row filter
|
|
655
|
+
— None (the default) is "no filter", today's behavior byte for byte.
|
|
656
|
+
`Schema.rows`'s `row_filter=` is the only caller that ever passes one;
|
|
657
|
+
every other call site forwards None exactly as before.
|
|
568
658
|
|
|
569
659
|
Ownership: the export buffer is COPIED into a Python `bytes` and the
|
|
570
660
|
C side's `data` freed with THIS library's `chs_free` before returning
|
|
@@ -583,6 +673,8 @@ class NativeLibrary:
|
|
|
583
673
|
export_format,
|
|
584
674
|
doc_flags,
|
|
585
675
|
ctypes.byref(out_bytes) if out_bytes is not None else None,
|
|
676
|
+
columns_json.encode() if columns_json is not None else None,
|
|
677
|
+
ctypes.c_void_p(filter_handle) if filter_handle is not None else None,
|
|
586
678
|
)
|
|
587
679
|
payload: bytes | None = None
|
|
588
680
|
if out_bytes is not None and out_bytes.data:
|
|
@@ -641,11 +733,18 @@ class NativeLibrary:
|
|
|
641
733
|
# -- blocks --------------------------------------------------------------
|
|
642
734
|
|
|
643
735
|
def block_parse(
|
|
644
|
-
self,
|
|
736
|
+
self,
|
|
737
|
+
handle: int,
|
|
738
|
+
fmt: int,
|
|
739
|
+
body: bytes,
|
|
740
|
+
settings_json: str,
|
|
741
|
+
columns_json: str | None = None,
|
|
645
742
|
) -> tuple[int | None, int, str]:
|
|
646
743
|
"""(block handle, code, err). code/err meaningful only on None — a
|
|
647
744
|
call-level failure (unknown setting 115, framing, a binary decode
|
|
648
745
|
fault) yields no block and no partial answers (the C ABI contract §Blocks).
|
|
746
|
+
`columns_json` (revision 5) is the INSERT column list, already
|
|
747
|
+
JSON-encoded, or None for "no list" — forwarded as NULL, never `"[]"`.
|
|
649
748
|
"""
|
|
650
749
|
fn = self._need("chs_block_parse", "this artifact predates chs_block_parse (rebuild it)")
|
|
651
750
|
code = ctypes.c_int(0)
|
|
@@ -659,6 +758,7 @@ class NativeLibrary:
|
|
|
659
758
|
settings_json.encode(),
|
|
660
759
|
ctypes.byref(code),
|
|
661
760
|
ctypes.byref(err),
|
|
761
|
+
columns_json.encode() if columns_json is not None else None,
|
|
662
762
|
)
|
|
663
763
|
if not bhandle:
|
|
664
764
|
return None, int(code.value), self._take_err(err)
|