chtypes 0.3.2__tar.gz → 0.5.0__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.2 → chtypes-0.5.0}/CHANGELOG.md +38 -0
- {chtypes-0.3.2 → chtypes-0.5.0}/PKG-INFO +2 -2
- {chtypes-0.3.2 → chtypes-0.5.0}/README.md +1 -1
- {chtypes-0.3.2 → chtypes-0.5.0}/pyproject.toml +1 -1
- {chtypes-0.3.2 → chtypes-0.5.0}/src/chtypes/__init__.py +7 -0
- {chtypes-0.3.2 → chtypes-0.5.0}/src/chtypes/__main__.py +10 -12
- {chtypes-0.3.2 → chtypes-0.5.0}/src/chtypes/_document.py +11 -0
- chtypes-0.5.0/src/chtypes/_error_codes.py +153 -0
- {chtypes-0.3.2 → chtypes-0.5.0}/src/chtypes/_native.py +35 -2
- {chtypes-0.3.2 → chtypes-0.5.0}/src/chtypes/errors.py +16 -0
- {chtypes-0.3.2 → chtypes-0.5.0}/src/chtypes/fetch.py +474 -70
- {chtypes-0.3.2 → chtypes-0.5.0}/src/chtypes/registry.py +467 -89
- {chtypes-0.3.2 → chtypes-0.5.0}/src/chtypes/results.py +18 -0
- {chtypes-0.3.2 → chtypes-0.5.0}/tests/test_cli.py +48 -1
- chtypes-0.5.0/tests/test_error_codes.py +250 -0
- chtypes-0.5.0/tests/test_exact_patch_resolution.py +613 -0
- {chtypes-0.3.2 → chtypes-0.5.0}/tests/test_fetch.py +152 -14
- {chtypes-0.3.2 → chtypes-0.5.0}/tests/test_fetch_abi_revision.py +1 -1
- chtypes-0.5.0/tests/test_fetch_sh.py +440 -0
- chtypes-0.5.0/tests/test_partition_key.py +169 -0
- {chtypes-0.3.2 → chtypes-0.5.0}/uv.lock +1 -1
- chtypes-0.3.2/tests/test_fetch_sh.py +0 -139
- {chtypes-0.3.2 → chtypes-0.5.0}/.gitignore +0 -0
- {chtypes-0.3.2 → chtypes-0.5.0}/LICENSE +0 -0
- {chtypes-0.3.2 → chtypes-0.5.0}/src/chtypes/_ed25519.py +0 -0
- {chtypes-0.3.2 → chtypes-0.5.0}/src/chtypes/_manifest.py +0 -0
- {chtypes-0.3.2 → chtypes-0.5.0}/src/chtypes/_rawjson.py +0 -0
- {chtypes-0.3.2 → chtypes-0.5.0}/src/chtypes/discover.py +0 -0
- {chtypes-0.3.2 → chtypes-0.5.0}/src/chtypes/py.typed +0 -0
- {chtypes-0.3.2 → chtypes-0.5.0}/src/chtypes/transform.py +0 -0
- {chtypes-0.3.2 → chtypes-0.5.0}/tests/conftest.py +0 -0
- {chtypes-0.3.2 → chtypes-0.5.0}/tests/test_abi_revision.py +0 -0
- {chtypes-0.3.2 → chtypes-0.5.0}/tests/test_csv_reader.py +0 -0
- {chtypes-0.3.2 → chtypes-0.5.0}/tests/test_discover_reconstruct.py +0 -0
- {chtypes-0.3.2 → chtypes-0.5.0}/tests/test_ed25519.py +0 -0
- {chtypes-0.3.2 → chtypes-0.5.0}/tests/test_errors.py +0 -0
- {chtypes-0.3.2 → chtypes-0.5.0}/tests/test_formats_withnames.py +0 -0
- {chtypes-0.3.2 → chtypes-0.5.0}/tests/test_golden.py +0 -0
- {chtypes-0.3.2 → chtypes-0.5.0}/tests/test_lazy.py +0 -0
- {chtypes-0.3.2 → chtypes-0.5.0}/tests/test_parity.py +0 -0
- {chtypes-0.3.2 → chtypes-0.5.0}/tests/test_quoting_boundaries.py +0 -0
- {chtypes-0.3.2 → chtypes-0.5.0}/tests/test_rawjson.py +0 -0
- {chtypes-0.3.2 → chtypes-0.5.0}/tests/test_registry.py +0 -0
- {chtypes-0.3.2 → chtypes-0.5.0}/tests/test_results_rules.py +0 -0
- {chtypes-0.3.2 → chtypes-0.5.0}/tests/test_rows_export_with.py +0 -0
- {chtypes-0.3.2 → chtypes-0.5.0}/tests/test_transform.py +0 -0
|
@@ -6,6 +6,44 @@ The four bindings in this repository are released together and give one answer,
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.5.0] — 2026-10-01
|
|
10
|
+
|
|
11
|
+
**Upgrade together if you share a lock: 0.4.x and earlier refuse a schema-2 lock, and this release writes one.** Schema-1 locks are still read and are converted the next time the lock is written. A server's exact ClickHouse patch now loads when it is installed, or published and autofetch is on; otherwise the newest installed patch of the same line loads, flagged as not exact and warned once per pair, and no request ever crosses to another line. Several patches of one line coexist: the patch a line request selects installs flat as before, and every other patch installs under `patches/<minor>/<version>/`, where an older flat patch is moved rather than deleted. `--frozen` now installs exactly the pinned file, even while a newer one is served. Same ABI revision as 0.4.0 (6), so the artifacts 0.4.0 loads are the ones this release loads. An older Python SDK's refusal reads `ValueError: chtypes: <lock> is not a chtypes lock file (schema 1)`, before any download.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`fetch --lock` now records the artifact's ABI revision** in each lock entry (`abi_revision`, optional and additive); `--frozen` checks it FIRST, before the file/sha256 pin: a lock that names a different revision raises `ArtifactPinnedError`, naming both numbers and the `python -m chtypes fetch` remedy, instead of surfacing as a bare drifted pin or `ArtifactUnpublishedError`. A lock entry with no recorded revision (written by an SDK before this change) keeps today's behavior, with one sentence appended when it ends in one of those two errors. `offline=True, frozen=True` is unaffected (docs/guides/fetch.md §5, closes #253).
|
|
16
|
+
- **Exact-patch resolution (SDK#284).** `Registry.resolve(v)` returns a `Resolution(library, requested, version, exact)`: a minor-line request (`"25.8"`) is always `exact=True`, pinned to the newest reachable patch for as long as the registry stays open; an exact-patch request (`"25.8.28.1-lts"`) loads that patch when it is installed or — with `autofetch` on — published, and otherwise falls back to the newest patch of the same line with `exact=False` and a `PatchFallbackWarning` (new, exported), raised once per (requested, actual) pair per process. Never crosses to another line — that failure is still `ArtifactMissingError`. `for_version` / `registry[v]` run the same resolution and hand back `.library`. A fallen-back patch is re-checked against the search path at most once every 60 seconds per (destination, requested patch), so a patch installed later — by fetch or by hand — is picked up with no restart and no directory read on every call.
|
|
17
|
+
- **`fetch.installed_patches(registry)`** — every installed PATCH under a registry directory (the flat line slot and every other patch under `patches/`), as `InstalledPatch(minor, version, directory, manifest, flat)`. Supersedes `fetch.installed_lines` as the fetch-level listing, which stays, unlisted, as "the one patch a line request loads."
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- **Several patches of one ClickHouse line now coexist in the cache**, so fetching a second patch no longer overwrites the first. The patch a LINE request selects (`fetch 25.8`) still installs FLAT at `<registry>/<minor>/`, exactly as before; any OTHER exact patch installs at `<registry>/patches/<minor>/<clickhouse_version>/`. When a line fetch changes which patch occupies the flat slot, the outgoing install is DEMOTED — renamed atomically, same filesystem, into `patches/<minor>/<its version>/` — never deleted, before the incoming patch takes the flat slot. Every reader (`Registry`, `list`, `verify`, `installed_patches`) reads both locations; a released (0.4.x and earlier) reader never touches or lists `patches/`, so the two coexist safely in one cache.
|
|
22
|
+
- **Lock schema 2** (`LOCK_SCHEMA = 2`), keyed `<os>-<arch>/<clickhouse_version>` — the exact patch, not the line, so two patches of one line each get their own pin and nothing is silently dropped when a second is fetched. Schema 1 is still read: a `<os-arch>/<minor>` entry converts to its schema-2 key by recovering the exact version from the entry's own pinned `file` name. The first write through an existing schema-1 file rewrites it as schema 2 wholesale — schema is a property of the file, not of one entry. `--frozen` now selects entirely from the lock (never from the release's other rows) and installs exactly the pinned file, even while a newer patch — or a higher build of the same patch — is served: the old "pins X but the release offers Y" refusal is gone. `--all --frozen` installs the newest patch EACH pinned line's lock entries name; a line the release has that the lock does not pin is simply not installed, not a refusal.
|
|
23
|
+
- **fetch.md Decision 7 (a channel-free spelling matches that patch on any channel) now applies to `ensure`'s offline path too** — the request that lands here whenever a live server answers `SELECT version()` with no channel suffix.
|
|
24
|
+
- **Docs: plain `CSV`/`TSV` input also honors `input_format_csv_detect_header` / `input_format_tsv_detect_header`, exactly as a real server does**, because the body is read by ClickHouse's own vendored row readers. The behavior arrived with 0.3.0's switch to those readers; it is now documented (#299).
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **`fetch --lock` without `--frozen` re-pins, as in the other three bindings.** It used to enforce an existing lock entry anyway and refuse a differing one with `CHTYPES_ARTIFACT_PINNED`. That meant the re-lock command the refusal itself names (`--lock` without `--frozen`) was refused too, and the only way out was deleting the lock. Only `--frozen` enforces a pin now (`docs/guides/fetch.md` §5).
|
|
29
|
+
- **An offline exact-patch request without a channel now matches an installed patch that has one** (Decision 7) — `ensure("25.8.28.1", offline=True)` used to raise `CHTYPES_SOURCE_UNREACHABLE` against an installed `25.8.28.1-lts`, because only the REQUESTED spelling had its channel stripped for comparison, never the installed manifest's own version.
|
|
30
|
+
- **An autofetched patch the release does not publish now falls back within its line** instead of raising `ArtifactUnpublishedError` — `Registry`'s lazy fetch used to hand the caller's own patch spelling straight to `ensure`, so it failed hard on an unpublished patch where Go, TypeScript and Rust would quietly serve the line's newest.
|
|
31
|
+
|
|
32
|
+
## [0.4.0] — 2026-09-30
|
|
33
|
+
|
|
34
|
+
Speaks ABI revision 6 and refuses revision-5 artifacts. Revision 6 adds the error-code table and the partition key; revision-6 artifacts are published for every ClickHouse line this SDK supports, beside the revision-5 builds that 0.3.x keeps loading.
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- **`Library.error_codes()`** — THIS build's own ClickHouse error-code table (`chs_error_codes`), as an `ErrorCodeTable`: `name(code) -> str | None`, `code(name) -> int | None`, `all()` (ascending) and iteration, over `ErrorCodeEntry(code, name)`. Per `Library` and never package-level, because the table moves between lines — one number names two different errors on 25.8 and 26.2. An unknown or negative code and an unknown name are `None`, never synthesized; names match exactly. Built on the first call and kept on success only: a `NULL` answer raises `ChtypesError` and is never cached; an artifact that predates the symbol raises `UnsupportedError`.
|
|
39
|
+
- **`Schema.set_partition_by(expr)`** — declare the table's partition key; `""` removes it. It follows `set_engine`'s sign rule, not `set_ttl`'s: `SchemaError` for the server's own CREATE-path refusal (e.g. 36 BAD_ARGUMENTS for a non-deterministic key, 549), `UnsupportedError` for a decline (-2 is a key the server accepts but this build will not evaluate).
|
|
40
|
+
- **`RowResult.partition_id`** and **`BatchResult.partition_count`**, both `None` unless the schema declared a key. A batch over `max_partitions_per_insert_block` is an ordinary `Outcome.REJECTED` with `err_code` 252.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- **Speaks ABI revision 6, and refuses revision-5 artifacts** — and a revision-5 binding refuses revision-6 ones. Revision 6 only ADDS `chs_error_codes` and `chs_schema_partition_by`; no existing declaration changed. It is still a new number, so a fetch after upgrading downloads revision-6 artifacts into their own `abi6` cache directory, and a directory holding only revision-5 artifacts is refused at load, naming both revisions. Revision-6 artifacts are published by the artifact producer; until they are, this binding has nothing to load.
|
|
45
|
+
- **A TTL on a schema with a `CHECK` constraint is now evaluated.** `Schema.set_ttl` on such a schema used to answer `UnsupportedError`; against revision-6 artifacts it answers as on any other schema, accepting the TTL or giving the server's own refusal. That matches a server, whose TTL validation does not read constraints. The change is in the artifacts, so it arrives with them.
|
|
46
|
+
|
|
9
47
|
## [0.3.2] — 2026-09-28
|
|
10
48
|
|
|
11
49
|
Speaks ABI revision 5, unchanged from 0.3.1: every artifact 0.3.1 loads, this release loads.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: chtypes
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
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
|
|
@@ -84,7 +84,7 @@ 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.
|
|
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. The column list needs an artifact of revision 5 or later, and this binding loads only artifacts at its own `chtypes.ABI_REVISION` — any other is refused at load, naming both revisions.
|
|
88
88
|
|
|
89
89
|
## Tests
|
|
90
90
|
|
|
@@ -66,7 +66,7 @@ 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.
|
|
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. The column list needs an artifact of revision 5 or later, and this binding loads only artifacts at its own `chtypes.ABI_REVISION` — any other is refused at load, naming both revisions.
|
|
70
70
|
|
|
71
71
|
## Tests
|
|
72
72
|
|
|
@@ -43,6 +43,7 @@ from __future__ import annotations
|
|
|
43
43
|
from importlib.metadata import PackageNotFoundError as _PackageNotFound
|
|
44
44
|
from importlib.metadata import version as _package_version
|
|
45
45
|
|
|
46
|
+
from ._error_codes import ErrorCodeEntry, ErrorCodeTable
|
|
46
47
|
from ._native import ABI_REVISION
|
|
47
48
|
from ._rawjson import RawNumber, quote_bare_denormals
|
|
48
49
|
from .discover import (
|
|
@@ -71,6 +72,7 @@ from .errors import (
|
|
|
71
72
|
ArtifactUnpublishedError,
|
|
72
73
|
ArtifactUntrustedError,
|
|
73
74
|
ChtypesError,
|
|
75
|
+
PatchFallbackWarning,
|
|
74
76
|
RegistryError,
|
|
75
77
|
SchemaError,
|
|
76
78
|
SourceUnreachableError,
|
|
@@ -93,6 +95,7 @@ from .registry import (
|
|
|
93
95
|
Library,
|
|
94
96
|
Manifest,
|
|
95
97
|
Registry,
|
|
98
|
+
Resolution,
|
|
96
99
|
Schema,
|
|
97
100
|
default_registry_dir,
|
|
98
101
|
host_platform,
|
|
@@ -165,6 +168,8 @@ __all__ = [
|
|
|
165
168
|
"Computed",
|
|
166
169
|
"DefaultKind",
|
|
167
170
|
"DiscoveredColumn",
|
|
171
|
+
"ErrorCodeEntry",
|
|
172
|
+
"ErrorCodeTable",
|
|
168
173
|
"Filter",
|
|
169
174
|
"FilterOutcome",
|
|
170
175
|
"FilterResult",
|
|
@@ -173,10 +178,12 @@ __all__ = [
|
|
|
173
178
|
"Library",
|
|
174
179
|
"Manifest",
|
|
175
180
|
"Outcome",
|
|
181
|
+
"PatchFallbackWarning",
|
|
176
182
|
"RawNumber",
|
|
177
183
|
"Reason",
|
|
178
184
|
"Registry",
|
|
179
185
|
"RegistryError",
|
|
186
|
+
"Resolution",
|
|
180
187
|
"RowResult",
|
|
181
188
|
"Schema",
|
|
182
189
|
"SchemaError",
|
|
@@ -20,7 +20,6 @@ import warnings
|
|
|
20
20
|
from collections.abc import Sequence
|
|
21
21
|
from pathlib import Path
|
|
22
22
|
|
|
23
|
-
from ._manifest import Manifest
|
|
24
23
|
from .errors import (
|
|
25
24
|
CODE_ARTIFACT_UNPUBLISHED,
|
|
26
25
|
CODE_SOURCE_UNREACHABLE,
|
|
@@ -28,7 +27,7 @@ from .errors import (
|
|
|
28
27
|
ChtypesError,
|
|
29
28
|
UnsignedArtifactWarning,
|
|
30
29
|
)
|
|
31
|
-
from .fetch import DEFAULT_ARTIFACTS_URL, DEFAULT_TAG, PLATFORMS
|
|
30
|
+
from .fetch import DEFAULT_ARTIFACTS_URL, DEFAULT_TAG, PLATFORMS
|
|
32
31
|
|
|
33
32
|
__all__ = ["main"]
|
|
34
33
|
|
|
@@ -202,17 +201,20 @@ def _cmd_list(args: argparse.Namespace) -> int:
|
|
|
202
201
|
_not_shown,
|
|
203
202
|
fetch_destination,
|
|
204
203
|
host_platform,
|
|
205
|
-
|
|
204
|
+
installed_patches,
|
|
206
205
|
)
|
|
207
206
|
|
|
208
207
|
platform = args.platform or host_platform()
|
|
209
208
|
registry = fetch_destination(args.dest, platform=platform)
|
|
210
|
-
have =
|
|
209
|
+
have = installed_patches(registry)
|
|
211
210
|
sys.stdout.write(f"installed ({registry}):\n")
|
|
212
211
|
if not have:
|
|
213
212
|
sys.stdout.write(" (nothing)\n")
|
|
214
|
-
for
|
|
215
|
-
|
|
213
|
+
for patch in have:
|
|
214
|
+
# SDK#284: the flat slot (the patch a LINE request selects) and every
|
|
215
|
+
# other patch under patches/<minor>/<version>/ both report.
|
|
216
|
+
tag = "" if patch.flat else " (patches/)"
|
|
217
|
+
sys.stdout.write(f" {patch.minor:<8} {patch.version:<18} {patch.directory}{tag}\n")
|
|
216
218
|
sys.stdout.flush()
|
|
217
219
|
|
|
218
220
|
fetcher = Fetcher(dest=registry, platform=platform, url=args.url, tag=args.tag, progress=_say)
|
|
@@ -222,8 +224,9 @@ def _cmd_list(args: argparse.Namespace) -> int:
|
|
|
222
224
|
sys.stdout.write(f"release ({release.source}, {signed}) offers for {platform}:\n")
|
|
223
225
|
if not offered:
|
|
224
226
|
sys.stdout.write(" (nothing)\n")
|
|
227
|
+
installed_versions = {patch.version for patch in have}
|
|
225
228
|
for entry in offered:
|
|
226
|
-
state = "installed" if
|
|
229
|
+
state = "installed" if entry.clickhouse_version in installed_versions else "not installed"
|
|
227
230
|
sys.stdout.write(
|
|
228
231
|
f" {entry.minor:<8} {entry.clickhouse_version:<18} b{entry.build_number:<3} "
|
|
229
232
|
f"{entry.file} [{state}]\n"
|
|
@@ -237,11 +240,6 @@ def _cmd_list(args: argparse.Namespace) -> int:
|
|
|
237
240
|
return EXIT_OK
|
|
238
241
|
|
|
239
242
|
|
|
240
|
-
def _installed(have: dict[str, tuple[object, Manifest]], entry: ReleaseEntry) -> bool:
|
|
241
|
-
got = have.get(entry.minor)
|
|
242
|
-
return got is not None and got[1].clickhouse_version == entry.clickhouse_version
|
|
243
|
-
|
|
244
|
-
|
|
245
243
|
def _goldens_line(registry: Path) -> None:
|
|
246
244
|
"""The served golden set sits beside the artifacts, so "where is my registry"
|
|
247
245
|
and "is my registry sound" are both moments someone wants to know whether it
|
|
@@ -196,6 +196,10 @@ def _row_result(doc: RawObject) -> RowResult:
|
|
|
196
196
|
verdict_code = _count(doc, "verdict_code")
|
|
197
197
|
verdict_err = _text(doc, "verdict_err")
|
|
198
198
|
|
|
199
|
+
# `partition_id` (revision 6) is present exactly when the schema declared
|
|
200
|
+
# a partition key and this row would be stored; absent is None.
|
|
201
|
+
partition_id = doc.get("partition_id")
|
|
202
|
+
|
|
199
203
|
return RowResult(
|
|
200
204
|
outcome=outcome,
|
|
201
205
|
err_code=_count(doc, "code"),
|
|
@@ -209,6 +213,7 @@ def _row_result(doc: RawObject) -> RowResult:
|
|
|
209
213
|
verdict=verdict,
|
|
210
214
|
verdict_code=verdict_code,
|
|
211
215
|
verdict_err=verdict_err,
|
|
216
|
+
partition_id=partition_id if isinstance(partition_id, str) else None,
|
|
212
217
|
)
|
|
213
218
|
|
|
214
219
|
|
|
@@ -244,6 +249,7 @@ def parse_batch_document(raw: bytes, payload: bytes | None = None) -> BatchResul
|
|
|
244
249
|
verdict=row.verdict,
|
|
245
250
|
verdict_code=row.verdict_code,
|
|
246
251
|
verdict_err=row.verdict_err,
|
|
252
|
+
partition_id=row.partition_id,
|
|
247
253
|
)
|
|
248
254
|
)
|
|
249
255
|
transformed.extend(indexed)
|
|
@@ -297,6 +303,11 @@ def parse_batch_document(raw: bytes, payload: bytes | None = None) -> BatchResul
|
|
|
297
303
|
export_declined=_text(doc, "export_declined"),
|
|
298
304
|
rows_passed=_count(doc, "rows_passed"),
|
|
299
305
|
rows_cut=_count(doc, "rows_cut"),
|
|
306
|
+
# Revision 6: present exactly when the schema declared a partition
|
|
307
|
+
# key; absent is None, never a guessed 0.
|
|
308
|
+
partition_count=_count(doc, "partition_count")
|
|
309
|
+
if isinstance(doc.get("partition_count"), RawNumber)
|
|
310
|
+
else None,
|
|
300
311
|
)
|
|
301
312
|
|
|
302
313
|
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""The error-code table (revision 6): one loaded build's own code -> name map.
|
|
2
|
+
|
|
3
|
+
There is no table in this package, and there must never be one. The table is a
|
|
4
|
+
property of the BUILD: codes join and leave between ClickHouse lines, and one
|
|
5
|
+
number can name two different errors on two lines (903 is LICENSE_EXPIRED on
|
|
6
|
+
25.3 and 25.8, absent on 25.10, and DISTRIBUTED_CACHE_REGISTRY_SHUTDOWN on 26.2
|
|
7
|
+
through 26.9). Every answer
|
|
8
|
+
therefore comes from `Library.error_codes()`, i.e. from `chs_error_codes` of
|
|
9
|
+
the library being asked, and `scripts/check-no-error-code-table.py` fails the
|
|
10
|
+
build if a literal code -> name table appears in any binding.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import threading
|
|
17
|
+
from collections.abc import Callable, Iterator
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
|
|
20
|
+
from .errors import ChtypesError
|
|
21
|
+
|
|
22
|
+
__all__ = ["ErrorCodeEntry", "ErrorCodeTable"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True, slots=True)
|
|
26
|
+
class ErrorCodeEntry:
|
|
27
|
+
"""One row of a build's error-code table: a ClickHouse error code and the
|
|
28
|
+
name THAT BUILD gives it."""
|
|
29
|
+
|
|
30
|
+
code: int
|
|
31
|
+
name: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ErrorCodeTable:
|
|
35
|
+
"""One loaded library's own error-code table, from `chs_error_codes` —
|
|
36
|
+
obtained from `Library.error_codes()` and valid for that library's
|
|
37
|
+
ClickHouse line only. Immutable, so safe to share between threads.
|
|
38
|
+
|
|
39
|
+
Lookups answer only what the build's own table holds: an unknown code, a
|
|
40
|
+
negative code (the ABI's -1 and -2 sentinels included — they are not
|
|
41
|
+
ClickHouse codes) or an unknown name is `None`, never a synthesized
|
|
42
|
+
spelling. Names match exactly and case-sensitively, as the server prints
|
|
43
|
+
them. Iterating yields the entries in ascending code order, as `all()`
|
|
44
|
+
returns them.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
__slots__ = ("_by_code", "_by_name", "_entries")
|
|
48
|
+
|
|
49
|
+
def __init__(self, entries: tuple[ErrorCodeEntry, ...]) -> None:
|
|
50
|
+
by_code: dict[int, str] = {}
|
|
51
|
+
by_name: dict[str, int] = {}
|
|
52
|
+
kept: list[ErrorCodeEntry] = []
|
|
53
|
+
for e in entries:
|
|
54
|
+
# Not a name, not a ClickHouse code, or a repeat: the first entry
|
|
55
|
+
# for a code or a name wins, and nothing else is invented.
|
|
56
|
+
if not e.name or e.code < 0 or e.code in by_code or e.name in by_name:
|
|
57
|
+
continue
|
|
58
|
+
by_code[e.code] = e.name
|
|
59
|
+
by_name[e.name] = e.code
|
|
60
|
+
kept.append(e)
|
|
61
|
+
self._entries = tuple(sorted(kept, key=lambda e: e.code))
|
|
62
|
+
self._by_code = by_code
|
|
63
|
+
self._by_name = by_name
|
|
64
|
+
|
|
65
|
+
def name(self, code: int) -> str | None:
|
|
66
|
+
"""The name this build gives `code`, or None."""
|
|
67
|
+
if code < 0:
|
|
68
|
+
return None
|
|
69
|
+
return self._by_code.get(code)
|
|
70
|
+
|
|
71
|
+
def code(self, name: str) -> int | None:
|
|
72
|
+
"""The code this build gives `name` — an exact, case-sensitive match —
|
|
73
|
+
or None."""
|
|
74
|
+
return self._by_name.get(name)
|
|
75
|
+
|
|
76
|
+
def all(self) -> tuple[ErrorCodeEntry, ...]:
|
|
77
|
+
"""Every entry, in ascending code order."""
|
|
78
|
+
return self._entries
|
|
79
|
+
|
|
80
|
+
def __iter__(self) -> Iterator[ErrorCodeEntry]:
|
|
81
|
+
return iter(self._entries)
|
|
82
|
+
|
|
83
|
+
def __len__(self) -> int:
|
|
84
|
+
return len(self._entries)
|
|
85
|
+
|
|
86
|
+
def __repr__(self) -> str:
|
|
87
|
+
return f"<chtypes.ErrorCodeTable {len(self._entries)} codes>"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def parse_error_codes_document(raw: bytes) -> ErrorCodeTable:
|
|
91
|
+
"""Build a table from a `chs_error_codes` document,
|
|
92
|
+
`{"error_codes":[{"code":N,"name":"…"}, …]}`.
|
|
93
|
+
|
|
94
|
+
Unknown keys are ignored and an absent (or null) key is its default, like
|
|
95
|
+
every document this ABI hands back. A value of the wrong type is a bad
|
|
96
|
+
document, never a guess.
|
|
97
|
+
"""
|
|
98
|
+
try:
|
|
99
|
+
doc = json.loads(raw)
|
|
100
|
+
except ValueError as exc:
|
|
101
|
+
raise ChtypesError(f"chtypes: bad chs_error_codes document: {exc}") from exc
|
|
102
|
+
if not isinstance(doc, dict):
|
|
103
|
+
raise ChtypesError("chtypes: bad chs_error_codes document: not a JSON object")
|
|
104
|
+
rows = doc.get("error_codes")
|
|
105
|
+
if rows is None:
|
|
106
|
+
rows = []
|
|
107
|
+
if not isinstance(rows, list):
|
|
108
|
+
raise ChtypesError("chtypes: bad chs_error_codes document: error_codes is not an array")
|
|
109
|
+
entries: list[ErrorCodeEntry] = []
|
|
110
|
+
for row in rows:
|
|
111
|
+
if not isinstance(row, dict):
|
|
112
|
+
raise ChtypesError("chtypes: bad chs_error_codes document: an entry is not an object")
|
|
113
|
+
code = row.get("code")
|
|
114
|
+
name = row.get("name")
|
|
115
|
+
if code is None:
|
|
116
|
+
code = 0
|
|
117
|
+
if name is None:
|
|
118
|
+
name = ""
|
|
119
|
+
if isinstance(code, bool) or not isinstance(code, int) or not isinstance(name, str):
|
|
120
|
+
raise ChtypesError(f"chtypes: bad chs_error_codes document: entry {row!r}")
|
|
121
|
+
entries.append(ErrorCodeEntry(code=code, name=name))
|
|
122
|
+
return ErrorCodeTable(tuple(entries))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class _ErrorCodeCache:
|
|
126
|
+
"""One library's table once it has been built, and only then.
|
|
127
|
+
|
|
128
|
+
A NULL answer from `chs_error_codes` is a guarded exception inside the
|
|
129
|
+
library — transient by definition — so it raises `ChtypesError` and is NOT
|
|
130
|
+
remembered: the next call asks again. A missing symbol raises the decline
|
|
131
|
+
type and is not remembered either. Only a table that was actually built is
|
|
132
|
+
kept.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
__slots__ = ("_mu", "_table")
|
|
136
|
+
|
|
137
|
+
def __init__(self) -> None:
|
|
138
|
+
self._mu = threading.Lock()
|
|
139
|
+
self._table: ErrorCodeTable | None = None
|
|
140
|
+
|
|
141
|
+
def get(self, fetch: Callable[[], bytes | None]) -> ErrorCodeTable:
|
|
142
|
+
with self._mu:
|
|
143
|
+
if self._table is not None:
|
|
144
|
+
return self._table
|
|
145
|
+
raw = fetch()
|
|
146
|
+
if raw is None:
|
|
147
|
+
raise ChtypesError(
|
|
148
|
+
"chtypes: chs_error_codes returned no document (a guarded exception "
|
|
149
|
+
"inside the library); nothing was cached, so the next call asks again"
|
|
150
|
+
)
|
|
151
|
+
table = parse_error_codes_document(raw)
|
|
152
|
+
self._table = table
|
|
153
|
+
return table
|
|
@@ -78,6 +78,11 @@ _SIGNATURES: Final[dict[str, tuple[object, list[object]]]] = {
|
|
|
78
78
|
),
|
|
79
79
|
"chs_registered_families": (ctypes.c_void_p, []),
|
|
80
80
|
"chs_function_flags": (ctypes.c_void_p, []),
|
|
81
|
+
# Revision 6: the build's own error-code table, an owned JSON document.
|
|
82
|
+
# Optional: an artifact that predates it degrades to UnsupportedError at
|
|
83
|
+
# call time. NULL from a PRESENT symbol is a guarded exception, which is a
|
|
84
|
+
# different answer — see `error_codes`.
|
|
85
|
+
"chs_error_codes": (ctypes.c_void_p, []),
|
|
81
86
|
# settings_json + mode compile a column list under a DECLARED settings
|
|
82
87
|
# profile; NULL/"{}" settings_json and mode 0 answer identically to the
|
|
83
88
|
# pre-consolidation, settings-less compile — structurally, not just by
|
|
@@ -94,6 +99,9 @@ _SIGNATURES: Final[dict[str, tuple[object, list[object]]]] = {
|
|
|
94
99
|
[ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p, _c_owned_p],
|
|
95
100
|
),
|
|
96
101
|
"chs_schema_ttl": (ctypes.c_int, [ctypes.c_void_p, ctypes.c_char_p, _c_owned_p]),
|
|
102
|
+
# Revision 6: the partition key — chs_schema_ttl's exact C shape. The SIGN
|
|
103
|
+
# rule its return follows is chs_schema_engine's (see Schema.set_partition_by).
|
|
104
|
+
"chs_schema_partition_by": (ctypes.c_int, [ctypes.c_void_p, ctypes.c_char_p, _c_owned_p]),
|
|
97
105
|
"chs_schema_column_count": (ctypes.c_int, [ctypes.c_void_p]),
|
|
98
106
|
"chs_schema_column_name": (ctypes.c_char_p, [ctypes.c_void_p, ctypes.c_int]),
|
|
99
107
|
"chs_schema_column_type": (ctypes.c_char_p, [ctypes.c_void_p, ctypes.c_int]),
|
|
@@ -189,7 +197,10 @@ _SIGNATURES: Final[dict[str, tuple[object, list[object]]]] = {
|
|
|
189
197
|
# (chs_block_parse / chs_block_free / chs_filter_eval).
|
|
190
198
|
# 5 = the explicit INSERT column list, 2026-09-15: chs_row, chs_rows and
|
|
191
199
|
# chs_block_parse each gained a trailing columns_json.
|
|
192
|
-
|
|
200
|
+
# 6 = the error-code table and the partition key: chs_error_codes and
|
|
201
|
+
# chs_schema_partition_by joined the surface. Purely additive, and still a
|
|
202
|
+
# new number: a revision-6 binding refuses a revision-5 artifact.
|
|
203
|
+
ABI_REVISION: Final = 6
|
|
193
204
|
|
|
194
205
|
_MANDATORY: Final = (
|
|
195
206
|
"chs_clickhouse_version",
|
|
@@ -217,7 +228,8 @@ class _RWLock:
|
|
|
217
228
|
|
|
218
229
|
**Readers** are every call that reaches `chs_row`, `chs_rows`,
|
|
219
230
|
`chs_schema_compile`, `chs_schema_engine`, `chs_schema_ttl`,
|
|
220
|
-
`chs_validate_type` and the
|
|
231
|
+
`chs_schema_partition_by`, `chs_validate_type`, `chs_error_codes` and the
|
|
232
|
+
column accessors. The C ABI contract
|
|
221
233
|
§Thread-safety declares those safe together **on distinct handles**, so
|
|
222
234
|
per-handle serialization is `Schema`'s own lock and not this one.
|
|
223
235
|
|
|
@@ -507,6 +519,16 @@ class NativeLibrary:
|
|
|
507
519
|
raw = self._take(fn())
|
|
508
520
|
return (raw or b"").decode("utf-8", "surrogateescape")
|
|
509
521
|
|
|
522
|
+
def error_codes(self) -> bytes | None:
|
|
523
|
+
"""The build's own error-code table as the raw JSON document, or None
|
|
524
|
+
when the library could not build it (a guarded exception — transient,
|
|
525
|
+
so the caller must not remember it). A missing symbol raises
|
|
526
|
+
`UnsupportedError`, which is a different answer: the artifact predates
|
|
527
|
+
revision 6."""
|
|
528
|
+
fn = self._need("chs_error_codes", "this artifact predates chs_error_codes (rebuild it)")
|
|
529
|
+
with self._lock.read():
|
|
530
|
+
return self._take(fn())
|
|
531
|
+
|
|
510
532
|
def function_flags(self) -> str:
|
|
511
533
|
"""The function-volatility TSV audit, verbatim. Requires chs_init."""
|
|
512
534
|
fn = self._need(
|
|
@@ -576,6 +598,17 @@ class NativeLibrary:
|
|
|
576
598
|
rc = int(fn(ctypes.c_void_p(handle), ttl_sql.encode(), ctypes.byref(err)))
|
|
577
599
|
return rc, self._take_err(err)
|
|
578
600
|
|
|
601
|
+
def schema_partition_by(self, handle: int, partition_by: str) -> tuple[int, str]:
|
|
602
|
+
"""(rc, err) from chs_schema_partition_by. The caller maps rc by the
|
|
603
|
+
SIGN rule chs_schema_engine uses, not chs_schema_ttl's."""
|
|
604
|
+
fn = self._need(
|
|
605
|
+
"chs_schema_partition_by", "this artifact predates chs_schema_partition_by (rebuild it)"
|
|
606
|
+
)
|
|
607
|
+
err = ctypes.c_void_p()
|
|
608
|
+
with self._lock.read():
|
|
609
|
+
rc = int(fn(ctypes.c_void_p(handle), partition_by.encode(), ctypes.byref(err)))
|
|
610
|
+
return rc, self._take_err(err)
|
|
611
|
+
|
|
579
612
|
def schema_columns(self, handle: int) -> list[tuple[str, str, str, str, bool]]:
|
|
580
613
|
"""(name, type, default_kind, default_expr, default_is_literal) per column."""
|
|
581
614
|
if not self.has_columns:
|
|
@@ -22,6 +22,7 @@ __all__ = [
|
|
|
22
22
|
"ArtifactUnpublishedError",
|
|
23
23
|
"ArtifactUntrustedError",
|
|
24
24
|
"ChtypesError",
|
|
25
|
+
"PatchFallbackWarning",
|
|
25
26
|
"RegistryError",
|
|
26
27
|
"SchemaError",
|
|
27
28
|
"SourceUnreachableError",
|
|
@@ -181,6 +182,21 @@ class UnsignedArtifactWarning(UserWarning):
|
|
|
181
182
|
signature check — the one loud warning docs/guides/fetch.md §4 requires."""
|
|
182
183
|
|
|
183
184
|
|
|
185
|
+
class PatchFallbackWarning(UserWarning):
|
|
186
|
+
"""Emitted once per (requested, actual) pair per process when a request for
|
|
187
|
+
an exact ClickHouse patch falls back to the newest installed or published
|
|
188
|
+
patch of the same line (SDK#284, docs/guides/fetch.md "Version selection").
|
|
189
|
+
|
|
190
|
+
Behavior can differ between patches of one line — the reason this change
|
|
191
|
+
exists at all — so this is a loud, once-per-pair signal, not a debug log
|
|
192
|
+
line. The pair is recorded BEFORE the warning is emitted, so a caller's
|
|
193
|
+
own `warnings.filterwarnings("error", category=PatchFallbackWarning)`
|
|
194
|
+
turning this into an exception does not cause a retry to warn again
|
|
195
|
+
(design risk R-h): the library has already loaded by the time the warning
|
|
196
|
+
fires, and the fallback is not undone.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
|
|
184
200
|
class SchemaError(ChtypesError):
|
|
185
201
|
"""ClickHouse itself REFUSED a type expression, a DDL, an engine or a TTL.
|
|
186
202
|
|