chtypes 2.0.0.dev2__tar.gz → 2.0.0.dev3__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.
Files changed (88) hide show
  1. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/CHANGELOG.md +4 -0
  2. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/PKG-INFO +2 -2
  3. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/README.md +1 -1
  4. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/pyproject.toml +1 -1
  5. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/src/chtypes/__init__.py +4 -0
  6. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/src/chtypes/_abi2/_decls.py +2 -2
  7. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/src/chtypes/_abi2/_errmap.py +1 -1
  8. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/src/chtypes/_abi2/_vocab.py +30 -5
  9. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/src/chtypes/_decode.py +27 -1
  10. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/src/chtypes/_ocifetch/_channel.py +72 -0
  11. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/src/chtypes/_ocifetch/_ensure.py +20 -2
  12. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/src/chtypes/_ocifetch/_http.py +18 -0
  13. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/src/chtypes/_ocifetch/_oci.py +30 -2
  14. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/src/chtypes/results.py +35 -1
  15. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/abi2/test_reader_rules.py +13 -0
  16. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/api/test_decode.py +89 -1
  17. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/ocifetch/test_conformance.py +74 -0
  18. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/ocifetch/test_devchannel.py +92 -1
  19. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/uv.lock +1 -1
  20. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/.gitignore +0 -0
  21. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/LICENSE +0 -0
  22. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/src/chtypes/__main__.py +0 -0
  23. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/src/chtypes/_abi2/__init__.py +0 -0
  24. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/src/chtypes/_abi2/_errors.py +0 -0
  25. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/src/chtypes/_abi2/_loader.py +0 -0
  26. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/src/chtypes/_ed25519.py +0 -0
  27. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/src/chtypes/_guard.py +0 -0
  28. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/src/chtypes/_input.py +0 -0
  29. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/src/chtypes/_ocifetch/__init__.py +0 -0
  30. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/src/chtypes/_ocifetch/_constants.py +0 -0
  31. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/src/chtypes/_ocifetch/_dsse.py +0 -0
  32. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/src/chtypes/_ocifetch/_errors.py +0 -0
  33. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/src/chtypes/_ocifetch/_faults.py +0 -0
  34. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/src/chtypes/_ocifetch/_goldens.py +0 -0
  35. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/src/chtypes/_ocifetch/_layout.py +0 -0
  36. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/src/chtypes/_ocifetch/_lock.py +0 -0
  37. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/src/chtypes/_ocifetch/_referrers.py +0 -0
  38. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/src/chtypes/_ocifetch/_unpack.py +0 -0
  39. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/src/chtypes/_setup.py +0 -0
  40. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/src/chtypes/errors.py +0 -0
  41. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/src/chtypes/library.py +0 -0
  42. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/src/chtypes/py.typed +0 -0
  43. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/src/chtypes/registry.py +0 -0
  44. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/abi2/__init__.py +0 -0
  45. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/abi2/_stub_tree.py +0 -0
  46. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/abi2/conftest.py +0 -0
  47. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/abi2/test_conformance.py +0 -0
  48. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/abi2/test_dev_fingerprint.py +0 -0
  49. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/abi2/test_goldens_runner_stub.py +0 -0
  50. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/abi2/test_loader.py +0 -0
  51. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/abi2/test_public_library.py +0 -0
  52. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/abi2/test_public_open.py +0 -0
  53. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/abi2/test_public_registry.py +0 -0
  54. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/abi2/test_public_server.py +0 -0
  55. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/abi2/test_public_setup_cases.py +0 -0
  56. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/api/__init__.py +0 -0
  57. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/api/test_input.py +0 -0
  58. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/api/test_vocab.py +0 -0
  59. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/cli/__init__.py +0 -0
  60. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/cli/conftest.py +0 -0
  61. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/cli/test_devchannel.py +0 -0
  62. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/cli/test_main.py +0 -0
  63. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/conftest.py +0 -0
  64. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/goldens_v1/__init__.py +0 -0
  65. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/goldens_v1/data/stub-goldens.json +0 -0
  66. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/goldens_v1/test_runner.py +0 -0
  67. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/ocifetch/__init__.py +0 -0
  68. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/ocifetch/_bundle_support.py +0 -0
  69. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/ocifetch/_registry_support.py +0 -0
  70. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/ocifetch/_sign_support.py +0 -0
  71. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/ocifetch/conftest.py +0 -0
  72. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/ocifetch/test_cache_roots.py +0 -0
  73. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/ocifetch/test_cache_strict.py +0 -0
  74. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/ocifetch/test_dsse.py +0 -0
  75. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/ocifetch/test_ensure.py +0 -0
  76. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/ocifetch/test_goldens.py +0 -0
  77. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/ocifetch/test_http.py +0 -0
  78. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/ocifetch/test_layout.py +0 -0
  79. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/ocifetch/test_lock.py +0 -0
  80. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/ocifetch/test_oci.py +0 -0
  81. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/ocifetch/test_seam_gate.py +0 -0
  82. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/ocifetch/test_unpack.py +0 -0
  83. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/test_abi_identity.py +0 -0
  84. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/test_cache_roots_api.py +0 -0
  85. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/test_devchannel_registry.py +0 -0
  86. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/test_ed25519.py +0 -0
  87. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/test_examples_tour.py +0 -0
  88. {chtypes-2.0.0.dev2 → chtypes-2.0.0.dev3}/tests/test_fetch_options.py +0 -0
@@ -6,6 +6,10 @@ The four bindings in this repository are released together and give one answer,
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ### Added
10
+
11
+ - A 2.0.0-dev SDK prefers the newest build of its own fingerprint (#511). A version request resolves `<tag>--fp-<fingerprint>` first, the alias the staging dev registry serves beside every dev tag, with the binding's own generated ABI fingerprint, and the tag itself only when every base answers the alias 404; any other failure of the alias request is that failure, with no fallback, and an alias whose index lacks the platform is `CHTYPES_ARTIFACT_UNPUBLISHED`. The dev channel's cache lookups (the installed-build lookup a registry open makes first, the offline answer, and the monotonic rule of a fetch) now see only records whose signed `abi_fingerprint` is the SDK's own, so a newer build of another fingerprint, installed by another dev SDK in the shared cache, is never returned or kept over this one's own, and is never removed or rewritten. So a dev build of a newer fingerprint no longer strands an older dev SDK until its consumers upgrade. Trust is unchanged (the signed predicate and the load checks decide), a listing never shows an alias, nothing records one, `list` and `where` are unchanged, and there is no option: both steps are the dev channel's alone, and the v1 contract and every production channel never take them. The rule is `docs/guides/fetch-v1.md` section 3, and the `dev-alias-*`, `resolve-installed-dev-alias-*` and `list-tags-dev-alias` conformance cases hold all four bindings to it.
12
+
9
13
  ## [1.1.0] — 2026-10-06
10
14
 
11
15
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: chtypes
3
- Version: 2.0.0.dev2
3
+ Version: 2.0.0.dev3
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
@@ -19,7 +19,7 @@ Description-Content-Type: text/markdown
19
19
 
20
20
  # chtypes — Python SDK
21
21
 
22
- > **2.0.0-dev: UNSTABLE, staging only, not for production.** This is the ABI v2 development binding (public issue #511). It speaks ABI v2's unstable description, pins its dev fingerprint and refuses a library with any other ("update your dev SDK"). It fetches only from the staging dev channel (`https://registry-staging.wavehouse.dev/chtypes/v2-dev`) and trusts only the staging key; `CHTYPES_ARTIFACTS_URL`, `CHTYPES_TRUSTED_KEYS` and `CHTYPES_ALLOW_UNSIGNED`, and the `bases`, `trusted_keys` and `allow_unsigned` fetch options, are ignored, each with one warning; `--lock`, `--frozen` and `--update`, and the `frozen`, `lock_path`, `lock_write` and `update` options, are refused, because a dev build is replaceable and a superseded one expires after 14 days. Its cache is `${XDG_CACHE_HOME:-~/.cache}/chtypes/v2-dev`, or `<CHTYPES_CACHE>/v2-dev` under an explicit cache, which no 1.x SDK reads. Every vocabulary keeps a value it does not list as its `unknown(n)` member (`Outcome("x")`, whose `known` is `False`) instead of failing the document. The rules are r1 to r6 of [`docs/reference/abi-v2.md`](../docs/reference/abi-v2.md). For production, use the 1.x package: `pip install "chtypes<2"`.
22
+ > **2.0.0-dev: UNSTABLE, staging only, not for production.** This is the ABI v2 development binding (public issue #511). It speaks ABI v2's unstable description, pins its dev fingerprint and refuses a library with any other ("update your dev SDK"). It fetches only from the staging dev channel (`https://registry-staging.wavehouse.dev/chtypes/v2-dev`) and trusts only the staging key; `CHTYPES_ARTIFACTS_URL`, `CHTYPES_TRUSTED_KEYS` and `CHTYPES_ALLOW_UNSIGNED`, and the `bases`, `trusted_keys` and `allow_unsigned` fetch options, are ignored, each with one warning; `--lock`, `--frozen` and `--update`, and the `frozen`, `lock_path`, `lock_write` and `update` options, are refused, because a dev build is replaceable and a superseded one expires after 14 days. Its cache is `${XDG_CACHE_HOME:-~/.cache}/chtypes/v2-dev`, or `<CHTYPES_CACHE>/v2-dev` under an explicit cache, which no 1.x SDK reads. A dev SDK prefers the newest build of its own fingerprint: it resolves `<tag>--fp-<its fingerprint>` first and the tag only when no such build exists, and its cache lookups ignore another dev SDK's builds in a shared cache, so a newer dev fingerprint never strands it (§3 of [`docs/guides/fetch-v1.md`](../docs/guides/fetch-v1.md)). Every vocabulary keeps a value it does not list as its `unknown(n)` member (`Outcome("x")`, whose `known` is `False`) instead of failing the document. The rules are r1 to r6 of [`docs/reference/abi-v2.md`](../docs/reference/abi-v2.md). For production, use the 1.x package: `pip install "chtypes<2"`.
23
23
  >
24
24
  > An explicit cache directory gets the `v2-dev` subroot whether it comes from `CHTYPES_CACHE` or from `FetchOptions(cache_dir=...)`: the layout is under `<cache_dir>/v2-dev`, never in the directory itself, so code that inspects or pre-populates a cache looks there; `chtypes.cache_root` and `chtypes.search_dirs` return the resolved paths. `CHTYPES_OFFLINE=1` is the environment twin of `FetchOptions(offline=True)` and `--offline`: the cache only, no request.
25
25
 
@@ -1,6 +1,6 @@
1
1
  # chtypes — Python SDK
2
2
 
3
- > **2.0.0-dev: UNSTABLE, staging only, not for production.** This is the ABI v2 development binding (public issue #511). It speaks ABI v2's unstable description, pins its dev fingerprint and refuses a library with any other ("update your dev SDK"). It fetches only from the staging dev channel (`https://registry-staging.wavehouse.dev/chtypes/v2-dev`) and trusts only the staging key; `CHTYPES_ARTIFACTS_URL`, `CHTYPES_TRUSTED_KEYS` and `CHTYPES_ALLOW_UNSIGNED`, and the `bases`, `trusted_keys` and `allow_unsigned` fetch options, are ignored, each with one warning; `--lock`, `--frozen` and `--update`, and the `frozen`, `lock_path`, `lock_write` and `update` options, are refused, because a dev build is replaceable and a superseded one expires after 14 days. Its cache is `${XDG_CACHE_HOME:-~/.cache}/chtypes/v2-dev`, or `<CHTYPES_CACHE>/v2-dev` under an explicit cache, which no 1.x SDK reads. Every vocabulary keeps a value it does not list as its `unknown(n)` member (`Outcome("x")`, whose `known` is `False`) instead of failing the document. The rules are r1 to r6 of [`docs/reference/abi-v2.md`](../docs/reference/abi-v2.md). For production, use the 1.x package: `pip install "chtypes<2"`.
3
+ > **2.0.0-dev: UNSTABLE, staging only, not for production.** This is the ABI v2 development binding (public issue #511). It speaks ABI v2's unstable description, pins its dev fingerprint and refuses a library with any other ("update your dev SDK"). It fetches only from the staging dev channel (`https://registry-staging.wavehouse.dev/chtypes/v2-dev`) and trusts only the staging key; `CHTYPES_ARTIFACTS_URL`, `CHTYPES_TRUSTED_KEYS` and `CHTYPES_ALLOW_UNSIGNED`, and the `bases`, `trusted_keys` and `allow_unsigned` fetch options, are ignored, each with one warning; `--lock`, `--frozen` and `--update`, and the `frozen`, `lock_path`, `lock_write` and `update` options, are refused, because a dev build is replaceable and a superseded one expires after 14 days. Its cache is `${XDG_CACHE_HOME:-~/.cache}/chtypes/v2-dev`, or `<CHTYPES_CACHE>/v2-dev` under an explicit cache, which no 1.x SDK reads. A dev SDK prefers the newest build of its own fingerprint: it resolves `<tag>--fp-<its fingerprint>` first and the tag only when no such build exists, and its cache lookups ignore another dev SDK's builds in a shared cache, so a newer dev fingerprint never strands it (§3 of [`docs/guides/fetch-v1.md`](../docs/guides/fetch-v1.md)). Every vocabulary keeps a value it does not list as its `unknown(n)` member (`Outcome("x")`, whose `known` is `False`) instead of failing the document. The rules are r1 to r6 of [`docs/reference/abi-v2.md`](../docs/reference/abi-v2.md). For production, use the 1.x package: `pip install "chtypes<2"`.
4
4
  >
5
5
  > An explicit cache directory gets the `v2-dev` subroot whether it comes from `CHTYPES_CACHE` or from `FetchOptions(cache_dir=...)`: the layout is under `<cache_dir>/v2-dev`, never in the directory itself, so code that inspects or pre-populates a cache looks there; `chtypes.cache_root` and `chtypes.search_dirs` return the resolved paths. `CHTYPES_OFFLINE=1` is the environment twin of `FetchOptions(offline=True)` and `--offline`: the cache only, no request.
6
6
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "chtypes"
3
- version = "2.0.0.dev2"
3
+ version = "2.0.0.dev3"
4
4
  description = "Python binding for the chtypes artifacts: ClickHouse's own type system, per version, behind the frozen chs_* C ABI"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -53,6 +53,7 @@ from ._abi2._vocab import (
53
53
  DocFlags,
54
54
  FilterOutcome,
55
55
  Format,
56
+ MergeReason,
56
57
  Outcome,
57
58
  Reason,
58
59
  Source,
@@ -95,6 +96,7 @@ from .errors import (
95
96
  from .library import Block, Filter, Library, Schema, Server, open_unverified
96
97
  from .registry import FetchOptions, Registry, Resolved, TrustedKey, cache_root, search_dirs
97
98
  from .results import (
99
+ AtMergeEntry,
98
100
  BatchResult,
99
101
  BuildInfo,
100
102
  Capabilities,
@@ -138,6 +140,7 @@ __all__ = [
138
140
  "ArtifactPinnedError",
139
141
  "ArtifactUnpublishedError",
140
142
  "ArtifactUntrustedError",
143
+ "AtMergeEntry",
141
144
  "BatchResult",
142
145
  "Block",
143
146
  "BuildInfo",
@@ -166,6 +169,7 @@ __all__ = [
166
169
  "Header",
167
170
  "InternalError",
168
171
  "Library",
172
+ "MergeReason",
169
173
  "Outcome",
170
174
  "Reason",
171
175
  "Registry",
@@ -1,4 +1,4 @@
1
- # GENERATED by scripts/abi-v1/gen.py --major 2 from spec/abi-v2/abi.json (CHS_ABI_FINGERPRINT sha256:d87e91be2bbe75b985c38224268c2f7dc5d8236dab0a4b4d61357c6dfdc1a232) — DO NOT EDIT # noqa: E501
1
+ # GENERATED by scripts/abi-v1/gen.py --major 2 from spec/abi-v2/abi.json (CHS_ABI_FINGERPRINT sha256:4129dccc3f4e698dedb0e99cae413f5240085f633c5a1244e2c55bf7a7bb6c3a) — DO NOT EDIT # noqa: E501
2
2
  """The Python binding's generated ABI v2 layer: ctypes signatures, the handle
3
3
  classes, the two-phase resolve functions and invoke_by_name(), the test-only
4
4
  dispatcher the conformance suite drives from tests/fixtures/abi-v2/cases.json.
@@ -19,7 +19,7 @@ from dataclasses import dataclass, field
19
19
  from . import _errmap
20
20
 
21
21
  CHS_ABI_VERSION = 2
22
- CHS_ABI_FINGERPRINT = "sha256:d87e91be2bbe75b985c38224268c2f7dc5d8236dab0a4b4d61357c6dfdc1a232"
22
+ CHS_ABI_FINGERPRINT = "sha256:4129dccc3f4e698dedb0e99cae413f5240085f633c5a1244e2c55bf7a7bb6c3a"
23
23
 
24
24
  # The description's stability: "unstable" while this generation is being
25
25
  # designed, so CHS_ABI_FINGERPRINT moves with every change, and "locked" after
@@ -1,4 +1,4 @@
1
- # GENERATED by scripts/abi-v1/gen.py --major 2 from spec/abi-v2/abi.json (CHS_ABI_FINGERPRINT sha256:d87e91be2bbe75b985c38224268c2f7dc5d8236dab0a4b4d61357c6dfdc1a232) — DO NOT EDIT # noqa: E501
1
+ # GENERATED by scripts/abi-v1/gen.py --major 2 from spec/abi-v2/abi.json (CHS_ABI_FINGERPRINT sha256:4129dccc3f4e698dedb0e99cae413f5240085f633c5a1244e2c55bf7a7bb6c3a) — DO NOT EDIT # noqa: E501
2
2
  """spec/abi-v2/sdk.json's error-mapping tables, as references to the
3
3
  public classes (python/src/chtypes/errors.py, re-exported by
4
4
  python/src/chtypes/_abi2/_errors.py). Only the MAPPING is generated: the
@@ -1,10 +1,10 @@
1
- # GENERATED by scripts/abi-v1/gen.py --major 2 from spec/abi-v2/abi.json (CHS_ABI_FINGERPRINT sha256:d87e91be2bbe75b985c38224268c2f7dc5d8236dab0a4b4d61357c6dfdc1a232) — DO NOT EDIT # noqa: E501
1
+ # GENERATED by scripts/abi-v1/gen.py --major 2 from spec/abi-v2/abi.json (CHS_ABI_FINGERPRINT sha256:4129dccc3f4e698dedb0e99cae413f5240085f633c5a1244e2c55bf7a7bb6c3a) — DO NOT EDIT # noqa: E501
2
2
  """The Python binding's generated vocabularies, from spec/abi-v2/abi.json: the C
3
3
  enums (`Format`, `Status`), the document vocabularies (`Outcome`, `FilterOutcome`,
4
- `Verdict`, `DefaultKind`, `Reason`, `Source`, `DiscoverQueryParam`) with the facts the
5
- description attaches to each value (`Format.ch_name`, `Reason` lossy, `Source`
6
- is_stored, `Verdict.answered`), and the `DocFlags` groups. No other file in this
7
- package keeps a copy of any of it.
4
+ `Verdict`, `DefaultKind`, `MergeReason`, `Reason`, `Source`, `DiscoverQueryParam`) with the
5
+ facts the description attaches to each value (`Format.ch_name`, `Reason` lossy,
6
+ `Source` is_stored, `Verdict.answered`), and the `DocFlags` groups. No other file in
7
+ this package keeps a copy of any of it.
8
8
 
9
9
  Rule r3 (spec/abi-v2/docs.md): every vocabulary has an unknown(n) member, and a
10
10
  value the description does not list reads as it, carrying the raw value, for that
@@ -225,6 +225,30 @@ class DefaultKind(StrEnum):
225
225
  return cls(text)
226
226
 
227
227
 
228
+ class MergeReason(StrEnum):
229
+ """What a merge would do to one row of the part an INSERT writes, as a batch's
230
+ `at_merge` reports it."""
231
+
232
+ TTL_DELETE = "ttl_delete"
233
+ TTL_COLUMN_RESET = "ttl_column_reset"
234
+
235
+ @classmethod
236
+ def _missing_(cls, value: object) -> MergeReason | None:
237
+ return _unknown_str(cls, value)
238
+
239
+ @property
240
+ def known(self) -> bool:
241
+ """Whether the description lists this value: False for exactly the unknown(n)
242
+ member an unlisted value reads as (rule r3)."""
243
+ return type(self)._value2member_map_.get(self._value_) is self
244
+
245
+ @classmethod
246
+ def of(cls, text: str) -> MergeReason:
247
+ """Map a document's spelling. One the description does not list is kept as
248
+ this vocabulary's unknown(n) member, carrying it (rule r3)."""
249
+ return cls(text)
250
+
251
+
228
252
  class DiscoverQueryParam(StrEnum):
229
253
  """A query parameter of the SQL `discover_query` returns, which the caller binds
230
254
  when it runs the query."""
@@ -383,6 +407,7 @@ DESCRIBED_VOCABULARIES: Final[dict[str, type]] = {
383
407
  "chs_status": Status,
384
408
  "chs_format": Format,
385
409
  "transform_reason": Reason,
410
+ "merge_reason": MergeReason,
386
411
  "value_src": Source,
387
412
  "row_outcome": Outcome,
388
413
  "batch_outcome": Outcome,
@@ -28,9 +28,10 @@ import json
28
28
  from collections.abc import Callable, Mapping
29
29
  from typing import Any
30
30
 
31
- from ._abi2._vocab import DefaultKind, FilterOutcome, Outcome, Reason, Source, Verdict
31
+ from ._abi2._vocab import DefaultKind, FilterOutcome, MergeReason, Outcome, Reason, Source, Verdict
32
32
  from .errors import ArtifactCorruptError, internal
33
33
  from .results import (
34
+ AtMergeEntry,
34
35
  BatchResult,
35
36
  BuildInfo,
36
37
  Capabilities,
@@ -325,6 +326,29 @@ def _engine_cell(obj: dict[str, Any], where: str) -> EngineCell:
325
326
  )
326
327
 
327
328
 
329
+ def _int_list(obj: Mapping[str, Any], key: str, where: str) -> tuple[int, ...] | None:
330
+ """A list of non-negative integers: None when absent."""
331
+ if obj.get(key) is None:
332
+ return None
333
+ out = []
334
+ for item in _list(obj, key, where):
335
+ if isinstance(item, bool) or not isinstance(item, int) or item < 0:
336
+ raise _wrong(where, key, "an array of non-negative integers", item)
337
+ out.append(item)
338
+ return tuple(out)
339
+
340
+
341
+ def _at_merge(obj: dict[str, Any], where: str) -> AtMergeEntry:
342
+ # An unlisted reason is its unknown(n): kept, never a failure (rule r3).
343
+ return AtMergeEntry(
344
+ row=_int(obj, "row", where),
345
+ reason=MergeReason.of(_text(obj, "reason", where)),
346
+ column=_bytes_or_none(obj, "column", where),
347
+ stored=_bytes_or_none(obj, "stored", where),
348
+ input_rows=_int_list(obj, "input_rows", where),
349
+ )
350
+
351
+
328
352
  def _engine_rows(obj: Mapping[str, Any], where: str) -> tuple[tuple[EngineCell, ...], ...]:
329
353
  rows = []
330
354
  for i, row in enumerate(_list(obj, "engine_rows", where)):
@@ -411,6 +435,8 @@ def decode_batch(raw: bytes, export: bytes | None) -> BatchResult:
411
435
  framing=None
412
436
  if framing is None
413
437
  else _framing(_obj(framing, where, "framing"), f"{where}.framing"),
438
+ unsupported_settings=_name_list(obj, "unsupported_settings", where),
439
+ at_merge=_objects(obj, "at_merge", where, _at_merge),
414
440
  )
415
441
 
416
442
 
@@ -18,6 +18,19 @@ five ways:
18
18
  used through its subroot `<cache>/v2-dev`, never as a whole layout;
19
19
  - a signed predicate must say abi 2.
20
20
 
21
+ It also resolves one tag the v1 contract never asks for: a version request
22
+ fetches `<tag>--fp-<its own fingerprint>` first, the newest dev build of the ABI
23
+ this binding speaks, and falls back to `<tag>` only when no base has that alias
24
+ (docs/guides/fetch-v1.md §3, "The dev channel's alias step"), so a dev build of a
25
+ newer fingerprint never strands this one. Its cache lookups (`resolve_installed`,
26
+ and `ensure`'s offline answer and its monotonic rule) see only the records whose
27
+ signed `abi_fingerprint` is its own: a build of another fingerprint in a shared
28
+ cache is never returned and never kept over this one's own, and it is never
29
+ removed or rewritten either. It is automatic, with no override; the trust checks
30
+ are unchanged, and a listing never shows an alias. The fingerprint
31
+ is the generated `CHS_ABI_FINGERPRINT` of the ABI layer (`chtypes._abi2._decls`),
32
+ never a hand-written copy.
33
+
21
34
  The v1 contract itself stays in this package, unchanged, because the fetch-v1
22
35
  conformance cases (tests/fixtures/fetch-v1) are its specification and the dev
23
36
  channel shares every other rule with it. Only a test run reaches it:
@@ -29,16 +42,19 @@ override a user can reach.
29
42
  from __future__ import annotations
30
43
 
31
44
  import os
45
+ import re
32
46
  import sys
33
47
  import threading
34
48
  from collections.abc import Callable, Sequence
35
49
  from dataclasses import dataclass, replace
36
50
  from typing import Final, Protocol
37
51
 
52
+ from chtypes._abi2._decls import CHS_ABI_FINGERPRINT
38
53
  from chtypes._ocifetch import _constants as C
39
54
  from chtypes._ocifetch._dsse import TrustedKey
40
55
 
41
56
  __all__ = [
57
+ "ALIAS_SEPARATOR",
42
58
  "DEV_ABI_GENERATION",
43
59
  "DEV_CACHE_DIR",
44
60
  "DEV_CHANNEL_BASE",
@@ -50,12 +66,15 @@ __all__ = [
50
66
  "PinningRefusedError",
51
67
  "abi",
52
68
  "active",
69
+ "alias_tag",
53
70
  "allow_overrides_for_tests",
54
71
  "channel_name",
55
72
  "enforce",
56
73
  "ignored_settings_warned",
57
74
  "pinning_requested",
58
75
  "trusted_keys",
76
+ "use_own_fingerprint_for_tests",
77
+ "visible",
59
78
  "use_dev_channel_for_tests",
60
79
  "use_fetch_v1_for_tests",
61
80
  ]
@@ -79,6 +98,10 @@ DEV_RECORD_SCHEMA: Final = 2
79
98
  # The abi a dev predicate must carry.
80
99
  DEV_ABI_GENERATION: Final = 2
81
100
 
101
+ # Joins a tag and a fingerprint into the dev channel's alias tag:
102
+ # <tag>--fp-<64 lowercase hex> (docs/guides/fetch-v1.md §3).
103
+ ALIAS_SEPARATOR: Final = "--fp-"
104
+
82
105
  # What every lock, frozen or update request gets from a 2.0.0-dev SDK, before
83
106
  # any network call (rule r6).
84
107
  PINNING_REFUSED: Final = (
@@ -102,6 +125,11 @@ class Channel:
102
125
  keys: tuple[dict[str, str], ...] # {"keyid", "ed25519_hex"}: the default trust
103
126
  overridable: bool # the base, trust and unsigned overrides are honored
104
127
  pinnable: bool # lock, frozen and update are honored
128
+ # The fingerprint, 64 lowercase hex, this contract's SDK speaks: a version
129
+ # request resolves its alias tag before the tag itself, and the cache
130
+ # lookups see only records signed with it. "" does neither (the v1 contract
131
+ # and every production channel).
132
+ own_fingerprint: str = ""
105
133
 
106
134
 
107
135
  # What a 2.0.0-dev SDK speaks, and what every process speaks unless a test
@@ -117,6 +145,7 @@ DEV_CHANNEL: Final = Channel(
117
145
  keys=({"keyid": DEV_KEY_ID, "ed25519_hex": DEV_KEY_HEX},),
118
146
  overridable=False,
119
147
  pinnable=False,
148
+ own_fingerprint=CHS_ABI_FINGERPRINT.removeprefix("sha256:"),
120
149
  )
121
150
 
122
151
  # The v1 contract the fetch-v1 conformance cases specify, from the generated
@@ -202,6 +231,49 @@ def allow_overrides_for_tests() -> Callable[[], None]:
202
231
  return _use(replace(DEV_CHANNEL, overridable=True))
203
232
 
204
233
 
234
+ _FINGERPRINT_HEX = re.compile(r"[0-9a-f]{64}")
235
+
236
+
237
+ def use_own_fingerprint_for_tests(fingerprint: str) -> Callable[[], None]:
238
+ """Make this TEST run's active contract speak `fingerprint` (64 lowercase
239
+ hex) as the dev channel speaks its own: a version request resolves that
240
+ fingerprint's alias tag first, and the cache lookups see only records
241
+ signed with it. The fetch-v1 conformance cases that carry
242
+ `request.own_fingerprint` run under it, against fixtures that name a
243
+ fixture fingerprint. Everything else stays the active contract's. Returns
244
+ the restore function; raises outside a test run or on a fingerprint that is
245
+ not 64 lowercase hex."""
246
+ _test_only("use_own_fingerprint_for_tests")
247
+ if not _FINGERPRINT_HEX.fullmatch(fingerprint):
248
+ raise ValueError(f"use_own_fingerprint_for_tests: {fingerprint!r} is not 64 lowercase hex")
249
+ return _use(replace(active(), own_fingerprint=fingerprint))
250
+
251
+
252
+ def alias_tag(tag: str, channel: Channel | None = None) -> str | None:
253
+ """The alias a version request for `tag` resolves first under a contract
254
+ with an own fingerprint, or None when it resolves `tag` alone: under the
255
+ v1 contract and every production channel, and for a request that is not a
256
+ version spelling (an arbitrary tag has no alias)."""
257
+ channel = channel or active()
258
+ if not channel.own_fingerprint or not re.fullmatch(C.SPELLING_REGEX, tag):
259
+ return None
260
+ return f"{tag}{ALIAS_SEPARATOR}{channel.own_fingerprint}"
261
+
262
+
263
+ def visible(predicate: object, channel: Channel | None = None) -> bool:
264
+ """Whether a cache record (or a pre-seeded entry) whose SIGNED predicate is
265
+ `predicate` may answer a request under the active contract: under one with
266
+ an own fingerprint (the dev channel), only when the predicate's
267
+ `abi_fingerprint` is that fingerprint; under every other, always. A record
268
+ it cannot see is never returned and never kept by the monotonic rule, and
269
+ nothing removes or rewrites it: another SDK of another fingerprint owns it."""
270
+ channel = channel or active()
271
+ if not channel.own_fingerprint:
272
+ return True
273
+ fingerprint = predicate.get("abi_fingerprint") if isinstance(predicate, dict) else None
274
+ return fingerprint == f"sha256:{channel.own_fingerprint}"
275
+
276
+
205
277
  def use_dev_channel_for_tests() -> Callable[[], None]:
206
278
  """Make this TEST run speak the dev channel exactly as a user's process does
207
279
  (undoing either function above, for a test of the dev channel itself).
@@ -355,6 +355,10 @@ def _newer_installed(
355
355
  (`26.3.4.1`) keeps only a newer BUILD of that exact version. A request
356
356
  that is not a version spelling (an arbitrary tag) names no range, so
357
357
  nothing is kept for it.
358
+
359
+ Under the dev channel only an install of its own fingerprint counts
360
+ (`_channel.visible`): a newer build of another fingerprint, installed by
361
+ another dev SDK, is never kept over this one's own.
358
362
  """
359
363
  if not re.match(C.SPELLING_REGEX, request_spelling):
360
364
  return None
@@ -368,6 +372,8 @@ def _newer_installed(
368
372
  for dir_, record in list_verified_records(roots):
369
373
  if record.platform != platform_key:
370
374
  continue
375
+ if not _channel.visible(record.predicate):
376
+ continue # another fingerprint's build (_channel.visible): never kept over this one's
371
377
  if not version_within_request(record.version, request_spelling):
372
378
  continue
373
379
  version_key = _version_key(record.version)
@@ -693,7 +699,11 @@ def _ensure_floating(
693
699
  scratch_root = _scratch_root(cache_root_path)
694
700
 
695
701
  index_doc, index_bytes, index_digest = fetch_manifest_by_tag(
696
- bases, request.spelling, policy=policy, retry=retry
702
+ bases,
703
+ request.spelling,
704
+ policy=policy,
705
+ retry=retry,
706
+ alias=_channel.alias_tag(request.spelling),
697
707
  )
698
708
  platform_desc = resolve_platform_manifest(index_doc, platform_key)
699
709
  manifest_hex = parse_digest(platform_desc.digest)
@@ -1050,6 +1060,7 @@ def _verify_and_install_from_local_blobs(
1050
1060
  manifest_digest: str,
1051
1061
  cache_write_root: Path,
1052
1062
  trusted_keys: tuple[TrustedKey, ...],
1063
+ visible_only: bool = False,
1053
1064
  ) -> VerifiedRecord | None:
1054
1065
  """docs/guides/fetch-v1.md §1/§6: "A pre-seeded layout has entries in
1055
1066
  `index.json` with no corresponding `unpacked/` directory yet; the first
@@ -1103,6 +1114,9 @@ def _verify_and_install_from_local_blobs(
1103
1114
  return None
1104
1115
 
1105
1116
  predicate = verified.statement.predicate
1117
+ if visible_only and not _channel.visible(predicate):
1118
+ # A lookup never installs another fingerprint's pre-seeded build.
1119
+ return None
1106
1120
  layer_hex = parse_digest(layer_desc.digest)
1107
1121
  if layer_hex not in verified.statement.subject_sha256:
1108
1122
  return None
@@ -1180,6 +1194,7 @@ def _verify_preseeded_entries(roots: Sequence[Path], trusted_keys: tuple[Trusted
1180
1194
  manifest_digest=digest,
1181
1195
  cache_write_root=cache_write_root,
1182
1196
  trusted_keys=trusted_keys,
1197
+ visible_only=True,
1183
1198
  )
1184
1199
  if record is not None:
1185
1200
  already_verified.add(digest)
@@ -1224,7 +1239,10 @@ def resolve_installed(request: Request, platform: str, options: Options) -> Reso
1224
1239
  candidates = [
1225
1240
  (dir_path, record)
1226
1241
  for dir_path, record in list_verified_records(roots)
1227
- if record.platform == platform and version_within_request(record.version, request.spelling)
1242
+ if record.platform == platform
1243
+ and version_within_request(record.version, request.spelling)
1244
+ # Under the dev channel, only its own fingerprint's builds (_channel.visible).
1245
+ and _channel.visible(record.predicate)
1228
1246
  ]
1229
1247
  if not candidates:
1230
1248
  return None
@@ -88,6 +88,12 @@ class NotFoundHttpError(TransportError):
88
88
  """A 404 (or, for a `file://` base, a missing path)."""
89
89
 
90
90
 
91
+ class AliasNotFoundError(TransportError):
92
+ """`mode="alias"`: every configured base answered the alias tag 404 (or,
93
+ for a `file://` base, has no such path). The one alias outcome the caller
94
+ answers with the tag itself (docs/guides/fetch-v1.md §3)."""
95
+
96
+
91
97
  class UnauthorizedHttpError(TransportError):
92
98
  """A 401 that is not (or is no longer) resolvable by the anonymous token flow."""
93
99
 
@@ -506,6 +512,12 @@ def fetch_from_bases(
506
512
  where it is retried within the shared budget before giving up — a
507
513
  listed digest that 404s is a host fault, never "unpublished", so the
508
514
  final exception for this mode is always `UnreachableHttpError`.
515
+ - `alias`: the dev channel's alias tag (docs/guides/fetch-v1.md §3). A 404
516
+ moves to the next base, as for a tag, but only a 404 on EVERY base is
517
+ "not found" (`AliasNotFoundError`, which the caller answers with the tag
518
+ itself). Any other failure on any base is raised as that failure, as the
519
+ tag would get it, so a transient error can never route a request to the
520
+ tag, which may name a build of another fingerprint.
509
521
 
510
522
  `decided-here` (the spec does not state it): when bases disagree — some
511
523
  404, some unreachable — the outcome follows the LAST base tried, since
@@ -516,6 +528,7 @@ def fetch_from_bases(
516
528
  raise UnreachableHttpError("no base URLs configured")
517
529
  last_index = len(bases) - 1
518
530
  last_error: TransportError | None = None
531
+ alias_failure: TransportError | None = None # mode="alias": a base's non-404 failure
519
532
  for i, base in enumerate(bases):
520
533
  is_last = i == last_index
521
534
  scheme = urlsplit(base).scheme
@@ -538,8 +551,13 @@ def fetch_from_bases(
538
551
  continue
539
552
  except UnreachableHttpError as e:
540
553
  last_error = e
554
+ alias_failure = e
541
555
  continue
542
556
  assert last_error is not None
557
+ if mode == "alias":
558
+ if alias_failure is None:
559
+ raise AliasNotFoundError(f"{path}: not found on any configured base")
560
+ last_error = alias_failure
543
561
  if mode == "tag" and isinstance(last_error, NotFoundHttpError):
544
562
  raise last_error
545
563
  if isinstance(last_error, (UnauthorizedHttpError, ForbiddenHttpError)):
@@ -16,7 +16,13 @@ from chtypes._ocifetch._errors import (
16
16
  ArtifactUnpublishedError,
17
17
  SourceIncompatibleError,
18
18
  )
19
- from chtypes._ocifetch._http import FetchPolicy, RetryPolicy, fetch_from_bases, get_json
19
+ from chtypes._ocifetch._http import (
20
+ AliasNotFoundError,
21
+ FetchPolicy,
22
+ RetryPolicy,
23
+ fetch_from_bases,
24
+ get_json,
25
+ )
20
26
 
21
27
  __all__ = [
22
28
  "Descriptor",
@@ -242,9 +248,31 @@ def fetch_manifest_by_tag(
242
248
  policy: FetchPolicy,
243
249
  retry: RetryPolicy,
244
250
  max_bytes: int = C.MANIFEST_MAX_BYTES,
251
+ alias: str | None = None,
245
252
  ) -> tuple[dict, bytes, str]:
246
- """GET manifests/<tag>; returns (doc, raw_bytes, resolved_digest)."""
253
+ """GET manifests/<tag>; returns (doc, raw_bytes, resolved_digest).
254
+
255
+ With `alias` (the dev channel's `<tag>--fp-<fingerprint>`, from
256
+ `_channel.alias_tag`), GET manifests/<alias> first, and the tag only when
257
+ every base answered the alias 404 (docs/guides/fetch-v1.md §3); any other
258
+ failure of the alias request is raised as it is. What is returned is then
259
+ trusted exactly as the tag's own answer would be."""
247
260
  accept = (C.MEDIA_TYPE_INDEX, C.MEDIA_TYPE_MANIFEST)
261
+ if alias is not None:
262
+ try:
263
+ doc, resp = get_json(
264
+ bases,
265
+ f"/manifests/{alias}",
266
+ mode="alias",
267
+ accept=accept,
268
+ max_bytes=max_bytes,
269
+ policy=policy,
270
+ retry=retry,
271
+ )
272
+ except AliasNotFoundError:
273
+ pass
274
+ else:
275
+ return doc, resp.body, f"sha256:{hashlib.sha256(resp.body).hexdigest()}"
248
276
  doc, resp = get_json(
249
277
  bases,
250
278
  f"/manifests/{tag}",
@@ -14,9 +14,10 @@ from __future__ import annotations
14
14
  from collections.abc import Mapping
15
15
  from dataclasses import dataclass
16
16
 
17
- from ._abi2._vocab import DefaultKind, FilterOutcome, Outcome, Verdict
17
+ from ._abi2._vocab import DefaultKind, FilterOutcome, MergeReason, Outcome, Verdict
18
18
 
19
19
  __all__ = [
20
+ "AtMergeEntry",
20
21
  "BatchResult",
21
22
  "BuildInfo",
22
23
  "Capabilities",
@@ -108,6 +109,31 @@ class EngineCell:
108
109
  value: bytes | None = None
109
110
 
110
111
 
112
+ @dataclass(frozen=True, slots=True)
113
+ class AtMergeEntry:
114
+ """One thing an `OPTIMIZE TABLE ... FINAL` of the part the INSERT writes would do
115
+ to one of its rows, at the call's clock instant: an entry of the batch's
116
+ `at_merge` list (ABI v2). `engine_rows` stays exactly what the INSERT writer
117
+ produces, so the two never contradict each other.
118
+
119
+ `row` indexes `engine_rows`: the part's row, not the input body's. `reason` is
120
+ a `MergeReason`; one the description does not list is its unknown(n) member
121
+ (rule r3). `column` is the column a `ttl_column_reset` resets (from `column`
122
+ or `column_b64`), None when the entry names none. `stored` is the column's
123
+ value after the reset (from `stored` or `stored_b64`), None when the document
124
+ carries none, which it omits when the column's DEFAULT reads the clock or a
125
+ generator and the value is decided at the merge. `input_rows` is the input
126
+ rows, each by its index in the body, that formed the part's row, None when
127
+ the document carries none.
128
+ """
129
+
130
+ row: int
131
+ reason: MergeReason
132
+ column: bytes | None = None
133
+ stored: bytes | None = None
134
+ input_rows: tuple[int, ...] | None = None
135
+
136
+
111
137
  @dataclass(frozen=True, slots=True)
112
138
  class RowResult:
113
139
  """The `row` document. `columns` is every `cols` entry in document order;
@@ -160,6 +186,12 @@ class BatchResult:
160
186
  can be fooled too. A caller that needs every record accounted for declines
161
187
  a body with any skipped row or any `unconsumed` range
162
188
  (docs/guides/batches.md).
189
+
190
+ `unsupported_settings` is the call's own settings the library declined, from
191
+ the batch's top-level list (ABI v2). `at_merge` is what an `OPTIMIZE TABLE ...
192
+ FINAL` of the part would do to its rows, at least: a later background merge
193
+ can remove more as more rows expire, and a row both removed and reset lists
194
+ only `ttl_delete`. Each is empty when the document carries none.
163
195
  """
164
196
 
165
197
  outcome: Outcome
@@ -178,6 +210,8 @@ class BatchResult:
178
210
  partition_count: int | None
179
211
  unconsumed: tuple[Span, ...]
180
212
  framing: Framing | None
213
+ unsupported_settings: tuple[bytes, ...] = ()
214
+ at_merge: tuple[AtMergeEntry, ...] = ()
181
215
 
182
216
 
183
217
  @dataclass(frozen=True, slots=True)
@@ -32,6 +32,7 @@ from chtypes import (
32
32
  FilterOutcome,
33
33
  Format,
34
34
  InternalError,
35
+ MergeReason,
35
36
  Outcome,
36
37
  Reason,
37
38
  Source,
@@ -91,6 +92,12 @@ def test_r2_unknown_members_are_ignored(open_variant) -> None:
91
92
  assert batch.framing is not None and batch.framing.header is not None
92
93
  assert batch.framing.header.names == (b"s",)
93
94
  assert batch.payload == b'{"s":"abc"}\n'
95
+ # ABI v2's batch members (public issue #544): an unknown member inside either is ignored.
96
+ assert batch.unsupported_settings == (b"st",)
97
+ assert len(batch.at_merge) == 1
98
+ entry = batch.at_merge[0]
99
+ assert entry.reason is MergeReason.TTL_COLUMN_RESET and entry.column == b"s"
100
+ assert entry.stored == b"" and entry.input_rows == (0,)
94
101
 
95
102
  with schema.compile_filter("x > 1") as flt:
96
103
  res = flt.rows(JSON, b'{"x":1}')
@@ -163,6 +170,12 @@ def test_r3_unknown_values_are_kept(open_variant) -> None:
163
170
  b = batch("batch.storage_transforms.reason")
164
171
  assert b.outcome is Outcome.ACCEPTED and len(b.rows) == 1
165
172
 
173
+ b = batch("batch.at_merge.reason")
174
+ entry = b.at_merge[0]
175
+ assert entry.reason == "x_future_reason" and not entry.reason.known
176
+ assert isinstance(entry.reason, MergeReason) and entry.column == b"s"
177
+ assert b.outcome is Outcome.ACCEPTED and len(b.rows) == 1
178
+
166
179
  # The schema's enum constrains the writer, never the reader (r3).
167
180
  b = batch("batch.framing.container")
168
181
  assert b.framing is not None and b.framing.container == "x_future_container"