chtypes 2.0.0.dev11__tar.gz → 2.0.0.dev13__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 (107) hide show
  1. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/CHANGELOG.md +8 -1
  2. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/PKG-INFO +3 -3
  3. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/README.md +2 -2
  4. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/pyproject.toml +1 -1
  5. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/__init__.py +3 -2
  6. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/_ocifetch/_ensure.py +106 -19
  7. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/_ocifetch/_oci.py +41 -0
  8. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/library.py +9 -3
  9. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/abi2/test_public_library.py +18 -1
  10. chtypes-2.0.0.dev13/tests/ocifetch/test_preseed_scan.py +281 -0
  11. chtypes-2.0.0.dev13/tests/ocifetch/test_revocation_statement.py +240 -0
  12. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/uv.lock +1 -1
  13. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/.gitignore +0 -0
  14. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/LICENSE +0 -0
  15. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/__main__.py +0 -0
  16. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/_abi2/__init__.py +0 -0
  17. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/_abi2/_decls.py +0 -0
  18. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/_abi2/_errmap.py +0 -0
  19. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/_abi2/_errors.py +0 -0
  20. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/_abi2/_loader.py +0 -0
  21. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/_abi2/_vocab.py +0 -0
  22. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/_advisory.py +0 -0
  23. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/_codes.py +0 -0
  24. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/_decode.py +0 -0
  25. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/_ed25519.py +0 -0
  26. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/_guard.py +0 -0
  27. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/_input.py +0 -0
  28. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/_ocifetch/__init__.py +0 -0
  29. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/_ocifetch/_channel.py +0 -0
  30. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/_ocifetch/_constants.py +0 -0
  31. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/_ocifetch/_dsse.py +0 -0
  32. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/_ocifetch/_errors.py +0 -0
  33. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/_ocifetch/_faults.py +0 -0
  34. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/_ocifetch/_goldens.py +0 -0
  35. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/_ocifetch/_hold.py +0 -0
  36. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/_ocifetch/_http.py +0 -0
  37. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/_ocifetch/_layout.py +0 -0
  38. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/_ocifetch/_lock.py +0 -0
  39. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/_ocifetch/_prune.py +0 -0
  40. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/_ocifetch/_referrers.py +0 -0
  41. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/_ocifetch/_resolve.py +0 -0
  42. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/_ocifetch/_retired.py +0 -0
  43. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/_ocifetch/_revocation.py +0 -0
  44. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/_ocifetch/_unpack.py +0 -0
  45. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/_setup.py +0 -0
  46. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/errors.py +0 -0
  47. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/py.typed +0 -0
  48. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/registry.py +0 -0
  49. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/src/chtypes/results.py +0 -0
  50. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/abi2/__init__.py +0 -0
  51. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/abi2/_stub_tree.py +0 -0
  52. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/abi2/conftest.py +0 -0
  53. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/abi2/test_conformance.py +0 -0
  54. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/abi2/test_dev_fingerprint.py +0 -0
  55. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/abi2/test_goldens_runner_stub.py +0 -0
  56. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/abi2/test_loader.py +0 -0
  57. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/abi2/test_public_open.py +0 -0
  58. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/abi2/test_public_registry.py +0 -0
  59. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/abi2/test_public_server.py +0 -0
  60. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/abi2/test_public_setup_cases.py +0 -0
  61. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/abi2/test_reader_rules.py +0 -0
  62. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/abi2/test_registry_fetch.py +0 -0
  63. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/abi2/test_registry_flight.py +0 -0
  64. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/api/__init__.py +0 -0
  65. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/api/test_decode.py +0 -0
  66. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/api/test_input.py +0 -0
  67. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/api/test_trusted_keys.py +0 -0
  68. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/api/test_vocab.py +0 -0
  69. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/cli/__init__.py +0 -0
  70. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/cli/conftest.py +0 -0
  71. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/cli/test_devchannel.py +0 -0
  72. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/cli/test_main.py +0 -0
  73. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/cli/test_resolve_prune.py +0 -0
  74. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/cli/test_revocation_cli.py +0 -0
  75. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/conftest.py +0 -0
  76. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/goldens_v1/__init__.py +0 -0
  77. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/goldens_v1/data/stub-goldens.json +0 -0
  78. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/goldens_v1/test_runner.py +0 -0
  79. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/ocifetch/__init__.py +0 -0
  80. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/ocifetch/_bundle_support.py +0 -0
  81. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/ocifetch/_registry_support.py +0 -0
  82. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/ocifetch/_sign_support.py +0 -0
  83. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/ocifetch/conftest.py +0 -0
  84. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/ocifetch/test_cache_roots.py +0 -0
  85. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/ocifetch/test_cache_strict.py +0 -0
  86. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/ocifetch/test_conformance.py +0 -0
  87. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/ocifetch/test_devchannel.py +0 -0
  88. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/ocifetch/test_dsse.py +0 -0
  89. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/ocifetch/test_ensure.py +0 -0
  90. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/ocifetch/test_goldens.py +0 -0
  91. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/ocifetch/test_http.py +0 -0
  92. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/ocifetch/test_layout.py +0 -0
  93. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/ocifetch/test_lock.py +0 -0
  94. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/ocifetch/test_oci.py +0 -0
  95. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/ocifetch/test_prodv2_smoke.py +0 -0
  96. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/ocifetch/test_prodv2channel.py +0 -0
  97. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/ocifetch/test_retired.py +0 -0
  98. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/ocifetch/test_revocation.py +0 -0
  99. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/ocifetch/test_seam_gate.py +0 -0
  100. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/ocifetch/test_unpack.py +0 -0
  101. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/test_abi_identity.py +0 -0
  102. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/test_cache_roots_api.py +0 -0
  103. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/test_devchannel_registry.py +0 -0
  104. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/test_ed25519.py +0 -0
  105. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/test_examples_tour.py +0 -0
  106. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/test_fetch_options.py +0 -0
  107. {chtypes-2.0.0.dev11 → chtypes-2.0.0.dev13}/tests/test_support_v2_gen.py +0 -0
@@ -8,7 +8,9 @@ The four bindings in this repository are released together and give one answer,
8
8
 
9
9
  ### Added
10
10
 
11
+ - Tests: the shared revocation-statement table (`tests/fixtures/revocation-statement/cases.json`) runs against `verify_revocation` and `check_installed`: `superseded_by` must be a string (`""` = no fix), and the first verifying revocation in a listing wins. No behavior changed.
11
12
  - `column_defaults` is covered by unit tests (ABI v2): the exact `insert_options` JSON for a UTF-8 value, a non-UTF-8 value (`value_b64`), an empty value (sent as `""`) and two entries in order, and the decode of a `default_override` cell (`Source.DEFAULT_OVERRIDE`, stored). The batches guide gains a usage section. No behavior changed.
13
+ - Docs: `docs/guides/batches.md` shows how a `column_defaults` value is read, in the body format's own field encoding (CSV quoting, TSV escapes, a JSON string or number), with a value that doesn't read refused at the call before any body byte.
12
14
  - `CODE_DECLINED` (ABI v2, -2): the code a declined row, outcome or verdict carries, generated from the description and exported from `chtypes`, as `bindings-v1.md` section 3 promises.
13
15
  - `CallError.reason` (ABI v2): the error's catalog id, read through `chs_error_reason` and passed through verbatim (ASCII, empty if none), on all four call classes.
14
16
  - Spec batch 2, the options documents (ABI v2; moves the dev fingerprint): keyword-only `insertable_columns`, `column_defaults` (a sequence of `ColumnDefault`), `verdict_basis` (`VerdictBasis`: `INPUT`, `EXPORT`) and `defer_defaults` (`DeferDefaults`: `NONE`, `SERVER`) build `insert_options` for `Schema.row`, `Schema.rows`, `Schema.parse_block` and `Filter.rows`; `param_encoding` (`ParamEncoding`: `ESCAPED`, `RAW`) builds `filter_options` for `Schema.compile_filter`; `posture` (`Posture`: `CREATE`, `LOAD`) builds `schema_options` for `Library.compile_table`. `insert_options_extra`, `filter_options_extra` and `schema_options_extra` pass a member the binding has no name for through as JSON text, so the library, which refuses an unknown key (rule r1), is the only judge. A document with no member set is not sent. `Filter.rows` takes `columns` (`chs_filter_eval_body` takes the column list).
@@ -34,7 +36,8 @@ The four bindings in this repository are released together and give one answer,
34
36
 
35
37
  ### Changed
36
38
 
37
- - **Breaking (ABI v2 dev): `BatchResult.spans` is a tuple of `Span | None`**, one entry per `rows` entry and index-aligned with it: `row_spans` carries `null` for a row the export did not carry (a skipped row, or a row a filter cut), which reads as `None`. It was the exported rows' spans only.
39
+ - **Breaking (vs the 2.0.0-dev releases):** the batch preview's default `doc_flags` is now 0 (the lean result): `Schema.rows` without `doc_flags=` returns no per-value documents. Pass `doc_flags=DocFlags.ALL` explicitly to get every column's per-value documents.
40
+ - **Breaking (ABI v2 dev): `BatchResult.spans` is a tuple of `Span | None`**, one entry per `rows` entry and index-aligned with it: `row_spans` carries `null` for a row the export did not carry (a skipped row, or a row a filter cut), which reads as `None`. It was the exported rows' spans only. Code that reads `span.off` from every entry raises `AttributeError` on a `None` entry: check each entry before reading it.
38
41
  - **Breaking (ABI v2 dev): `AtMergeEntry.row` is `AtMergeEntry.part_row`**, the document's `part_row`.
39
42
  - A document's `code` can be -2 (`CHS_CODE_DECLINED`) for a row, batch, filter result or verdict this build declined, and every decline names its `reason`.
40
43
  - The refusal of a settings, query-parameter, default or server-settings value that is not valid UTF-8 is gone (it is the `{"b64": ...}` form now); a key that is not valid UTF-8, a macro and a server timezone are still refused, as a `UsageError` naming the input.
@@ -79,6 +82,7 @@ The four bindings in this repository are released together and give one answer,
79
82
  - **Enforcement gate lifted** for `26.8` on `darwin-arm64`.
80
83
  - **Enforcement gate lifted** for `26.9` on `darwin-arm64`.
81
84
  - `docs/limitations.md` is re-checked against artifact build `20261012.142240` (v2-dev.13), the library half measured on that build: functions that need a server, in a column expression, and a body the server may read serially or in parallel under a non-zero error budget are listed under "Declined by design"; the known divergence for a column expression that calls a function needing a server is retired, since that build declines those schemas (on 26.8 and 26.9 the `AI*` and `ai*` functions compile, as the page says); and the `lossy` raw-NUL TSV gap is marked fixed on that build and still present in the 1.0 libraries. `docs/guides/filters.md` gains a section on subcolumn filters and a JSON value typed by its batch.
85
+ - The ABI v2 spec now states the INSERT-time settings layering the library already uses, since artifact build `20261021.025722` (#632): at the INSERT, the schema's own settings (the compile profile) reach only the parse, minus a type gate, and every expression the INSERT evaluates and its limits (`max_partitions_per_insert_block` among them) run under the call's own settings, the server profile and the setup defaults only. A setting your INSERT session carries for those goes in the call's settings or the server profile (`docs/guides/settings.md`). No API change.
82
86
 
83
87
  ### Removed
84
88
 
@@ -87,6 +91,9 @@ The four bindings in this repository are released together and give one answer,
87
91
 
88
92
  ### Fixed
89
93
 
94
+ - Installing a pre-seeded cache entry from local blobs now checks the layer blob against the manifest's layer descriptor (size and sha256) before it decompresses it, and the manifest blob against its digest and its `index.json` descriptor's size before it parses it, as a downloaded blob already was and as the other three bindings already did. The install had checked only that the signed statement names the layer's digest, then unpacked the file stored under that name without hashing it, so a file replaced under a signed layer's name, holding the same library bytes plus other members, was installed with those members. Such an entry is now not installed, and the lookup is a miss. The installed library itself was always re-hashed against the signed predicate.
95
+ - An offline lookup no longer reads every cached blob once per pre-seeded entry (#690). For each `index.json` entry with no record, the lookup read every blob of the layout, layers included, to find its signature, so entries it could not install (a signature not present, or one no trusted key verifies) cost entries × cache bytes on every lookup: about 4.9 s for 27 such entries over 1.27 GB (`measured`, darwin-arm64). The signature referrers are now read once per lookup, and never a layer, because a blob larger than `limits.manifest_bytes` cannot be a manifest. Installed entries were already skipped. The build that is installed is verified exactly as before: its manifest against its descriptor, its whole layer against its digest, and its signature, platform, request and fingerprint. A unit test counts the blob reads of one lookup that installs nothing and holds them to at most two per blob and five per entry, with no layer read, at 2 and at 20 entries alike.
96
+ - Docs: `docs/guides/settings.md` names the ABI v2 admission time budget `chtypes_default_eval_cpu_nanos`, the evaluating thread's CPU time. Its examples used the 1.x name `chtypes_default_eval_wall_nanos`, which v2 refuses with `CHS_INVALID_ARGUMENT`.
90
97
  - A response body cut off part way (shorter than its Content-Length, a reset, a TLS close) is retried like a 5xx and, once the retry table is spent, is `CHTYPES_SOURCE_UNREACHABLE`; only an oversize body is `CHTYPES_ARTIFACT_CORRUPT`, and it is never retried (#641). Python took a body that ended before its Content-Length as the whole body and failed its digest check as corrupt without a retry; now a short body, or `http.client.IncompleteRead`, is retried and ends as unreachable. The rule is `docs/guides/fetch-v1.md` section 7, and the new `body-cut-off-then-ok` and `body-cut-off-exhausted` conformance cases hold every binding to it.
91
98
  - A chunked response body cut off before its terminating zero-length chunk is held to the same rule (#644): the new `body-cut-off-chunked-then-ok` and `body-cut-off-chunked-exhausted` conformance cases cut one on the layer blob GET, and this binding already retried it and ended as `CHTYPES_SOURCE_UNREACHABLE` once the retry table was spent. No behavior change; the case now holds it.
92
99
  - A settings or query-parameter key or value that is not valid UTF-8 is refused as a usage error; it was silently replaced with U+FFFD, so the library saw a different value. A string holding a lone surrogate (what `surrogateescape` makes of undecodable bytes) was sent as a `\udXXX` escape; the same inputs are refused now, for the per-call zone, the setup defaults and the server profile too. Valid UTF-8 serializes as before.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: chtypes
3
- Version: 2.0.0.dev11
3
+ Version: 2.0.0.dev13
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
@@ -43,7 +43,7 @@ uv add "chtypes>=2.0.0.dev0" # or: pip install "chtypes>=2.0.0.dev0"
43
43
 
44
44
  ```python
45
45
  import chtypes
46
- from chtypes import Format, Registry
46
+ from chtypes import DocFlags, Format, Registry
47
47
 
48
48
  chtypes.setup(timezone="UTC") # once, before the first open (optional)
49
49
  registry = Registry(autofetch=True)
@@ -51,7 +51,7 @@ library = registry.for_version("26.8") # a release line or an exact version
51
51
 
52
52
  ddl = b"CREATE TABLE t (x UInt8, ts DateTime DEFAULT now()) ENGINE = Memory"
53
53
  with library.compile_table(ddl) as schema:
54
- batch = schema.rows(Format.JSON_EACH_ROW, b'{"x":256}\n')
54
+ batch = schema.rows(Format.JSON_EACH_ROW, b'{"x":256}\n', doc_flags=DocFlags.ALL)
55
55
 
56
56
  row = batch.rows[0]
57
57
  print(batch.outcome) # accepted
@@ -24,7 +24,7 @@ uv add "chtypes>=2.0.0.dev0" # or: pip install "chtypes>=2.0.0.dev0"
24
24
 
25
25
  ```python
26
26
  import chtypes
27
- from chtypes import Format, Registry
27
+ from chtypes import DocFlags, Format, Registry
28
28
 
29
29
  chtypes.setup(timezone="UTC") # once, before the first open (optional)
30
30
  registry = Registry(autofetch=True)
@@ -32,7 +32,7 @@ library = registry.for_version("26.8") # a release line or an exact version
32
32
 
33
33
  ddl = b"CREATE TABLE t (x UInt8, ts DateTime DEFAULT now()) ENGINE = Memory"
34
34
  with library.compile_table(ddl) as schema:
35
- batch = schema.rows(Format.JSON_EACH_ROW, b'{"x":256}\n')
35
+ batch = schema.rows(Format.JSON_EACH_ROW, b'{"x":256}\n', doc_flags=DocFlags.ALL)
36
36
 
37
37
  row = batch.rows[0]
38
38
  print(batch.outcome) # accepted
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "chtypes"
3
- version = "2.0.0.dev11"
3
+ version = "2.0.0.dev13"
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"
@@ -13,14 +13,15 @@ C ABI. Nothing here reimplements a coercion rule, which is why the answers are
13
13
  exact by construction rather than approximately right.
14
14
 
15
15
  import chtypes
16
- from chtypes import Format, Registry
16
+ from chtypes import DocFlags, Format, Registry
17
17
 
18
18
  chtypes.setup(timezone="UTC") # once, before the first open
19
19
  registry = Registry(autofetch=True)
20
20
  library = registry.for_version("26.8") # a release line or an exact version
21
21
  ddl = b"CREATE TABLE t (ts DateTime, seq UInt8) ENGINE = Memory"
22
22
  with library.compile_table(ddl) as schema:
23
- batch = schema.rows(Format.JSON_EACH_ROW, b'{"ts":"2026-01-15 10:30:00","seq":256}\\n')
23
+ body = b'{"ts":"2026-01-15 10:30:00","seq":256}\\n'
24
+ batch = schema.rows(Format.JSON_EACH_ROW, body, doc_flags=DocFlags.ALL)
24
25
 
25
26
  batch.outcome # Outcome.ACCEPTED: the insert would succeed
26
27
  batch.transformed # the changes ClickHouse makes and reports as success
@@ -75,6 +75,7 @@ from chtypes._ocifetch._layout import (
75
75
  from chtypes._ocifetch._lock import Lock, LockPin, load_lock, new_lock, save_lock
76
76
  from chtypes._ocifetch._oci import (
77
77
  Descriptor,
78
+ copy_blob_verified,
78
79
  fetch_blob_bytes,
79
80
  fetch_blob_to_path,
80
81
  fetch_manifest_by_digest,
@@ -86,6 +87,8 @@ from chtypes._ocifetch._oci import (
86
87
  resolve_platform_manifest,
87
88
  spelling_components,
88
89
  validate_spelling,
90
+ verify_body_matches_digest,
91
+ verify_json_descriptor,
89
92
  version_within_request,
90
93
  )
91
94
  from chtypes._ocifetch._referrers import ReferrerListing, discover_referrers, read_listing
@@ -1296,6 +1299,48 @@ def _ensure(
1296
1299
  raise _translate_transport_error(e) from e
1297
1300
 
1298
1301
 
1302
+ def _local_bundle_referrers(
1303
+ blobs_dir: Path, skip: Collection[str], known: dict[str, bytes] | None = None
1304
+ ) -> dict[str, list[tuple[str, dict]]]:
1305
+ """Each subject manifest digest's signature referrer manifests among
1306
+ `blobs_dir`'s blobs, as (blob name, parsed manifest), in listing order.
1307
+ A referrer need not be listed in `index.json`, so every blob that could
1308
+ be one is read, once per lookup rather than once per entry. A blob named
1309
+ in `skip` (the caller's candidate layers) and a blob larger than
1310
+ `limits.manifest_bytes`, which cannot be a manifest, are never read, so
1311
+ no layer is read here, and a blob in `known` (bytes the caller already
1312
+ read, by name) is not read again (public issue #690)."""
1313
+ out: dict[str, list[tuple[str, dict]]] = {}
1314
+ try:
1315
+ blob_paths = list(blobs_dir.iterdir())
1316
+ except OSError:
1317
+ return out
1318
+ for blob_path in blob_paths:
1319
+ if blob_path.name in skip:
1320
+ continue
1321
+ try:
1322
+ body = known.get(blob_path.name) if known is not None else None
1323
+ if body is None:
1324
+ if blob_path.stat().st_size > C.MANIFEST_MAX_BYTES:
1325
+ continue
1326
+ body = blob_path.read_bytes()
1327
+ doc = json.loads(body)
1328
+ except (ValueError, OSError, UnicodeDecodeError):
1329
+ continue
1330
+ if not isinstance(doc, dict) or doc.get("mediaType") != C.MEDIA_TYPE_MANIFEST:
1331
+ continue
1332
+ subject = doc.get("subject") or {}
1333
+ subject_digest = subject.get("digest") if isinstance(subject, dict) else None
1334
+ if not isinstance(subject_digest, str):
1335
+ continue
1336
+ # A signature referrer only: a revocation of the manifest carries the same
1337
+ # bundle media type, and is not the signature of the artifact.
1338
+ if doc.get("artifactType") != C.MEDIA_TYPE_BUNDLE:
1339
+ continue
1340
+ out.setdefault(subject_digest, []).append((blob_path.name, doc))
1341
+ return out
1342
+
1343
+
1299
1344
  def _verify_and_install_from_local_blobs(
1300
1345
  *,
1301
1346
  source_root: Path,
@@ -1303,6 +1348,8 @@ def _verify_and_install_from_local_blobs(
1303
1348
  cache_write_root: Path,
1304
1349
  trusted_keys: tuple[TrustedKey, ...],
1305
1350
  visible_only: bool = False,
1351
+ referrers: dict[str, list[tuple[str, dict]]] | None = None,
1352
+ manifest_size: int | None = None,
1306
1353
  ) -> VerifiedRecord | None:
1307
1354
  """docs/guides/fetch-v1.md §1/§6: "A pre-seeded layout has entries in
1308
1355
  `index.json` with no corresponding `unpacked/` directory yet; the first
@@ -1314,7 +1361,14 @@ def _verify_and_install_from_local_blobs(
1314
1361
  `source_root/blobs/sha256/*`, never the network. Returns `None` — never
1315
1362
  raises — for a missing, unreadable or untrusted entry, the same
1316
1363
  tolerant shape `_find_verified_signature` uses for a network referrer:
1317
- a bad pre-seed is "nothing usable here," not a crash."""
1364
+ a bad pre-seed is "nothing usable here," not a crash.
1365
+
1366
+ The manifest blob is checked against `manifest_digest` (and against
1367
+ `manifest_size`, the index descriptor's size, when the caller has one)
1368
+ before it is parsed, and the layer blob against the manifest's layer
1369
+ descriptor (size and sha256) before it is decompressed: the same
1370
+ checks a download gets (§7.3), so no byte the signature does not cover
1371
+ reaches the install."""
1318
1372
  blobs_dir = source_root / "blobs" / "sha256"
1319
1373
  if not blobs_dir.is_dir():
1320
1374
  return None
@@ -1323,28 +1377,25 @@ def _verify_and_install_from_local_blobs(
1323
1377
  if not manifest_path.is_file():
1324
1378
  return None
1325
1379
  try:
1326
- manifest_doc = json.loads(manifest_path.read_bytes())
1380
+ manifest_bytes = manifest_path.read_bytes()
1381
+ verify_body_matches_digest(manifest_bytes, manifest_digest, what="pre-seeded manifest")
1382
+ if manifest_size is not None:
1383
+ verify_json_descriptor(
1384
+ manifest_bytes,
1385
+ Descriptor(media_type="", digest=manifest_digest, size=manifest_size),
1386
+ what="pre-seeded manifest",
1387
+ )
1388
+ manifest_doc = json.loads(manifest_bytes)
1327
1389
  layer_desc = manifest_layer_descriptor(manifest_doc)
1328
1390
  except (ValueError, OSError, FetchError):
1329
1391
  return None
1330
1392
 
1393
+ if referrers is None:
1394
+ referrers = _local_bundle_referrers(blobs_dir, {parse_digest(layer_desc.digest)})
1331
1395
  verified = None
1332
1396
  bundle_digest = None
1333
1397
  bundle_manifest_digest = None
1334
- for blob_path in blobs_dir.iterdir():
1335
- try:
1336
- doc = json.loads(blob_path.read_bytes())
1337
- except (ValueError, OSError, UnicodeDecodeError):
1338
- continue
1339
- if not isinstance(doc, dict) or doc.get("mediaType") != C.MEDIA_TYPE_MANIFEST:
1340
- continue
1341
- subject = doc.get("subject") or {}
1342
- if subject.get("digest") != manifest_digest:
1343
- continue
1344
- # A signature referrer only: a revocation of the manifest carries the same
1345
- # bundle media type, and is not the signature of the artifact.
1346
- if doc.get("artifactType") != C.MEDIA_TYPE_BUNDLE:
1347
- continue
1398
+ for referrer_name, doc in referrers.get(manifest_digest, ()):
1348
1399
  try:
1349
1400
  referrer_layer = manifest_single_layer(doc, expected_media_type=C.MEDIA_TYPE_BUNDLE)
1350
1401
  bundle_bytes = (blobs_dir / parse_digest(referrer_layer.digest)).read_bytes()
@@ -1354,7 +1405,7 @@ def _verify_and_install_from_local_blobs(
1354
1405
  if candidate is not None:
1355
1406
  verified = candidate
1356
1407
  bundle_digest = referrer_layer.digest
1357
- bundle_manifest_digest = "sha256:" + blob_path.name
1408
+ bundle_manifest_digest = "sha256:" + referrer_name
1358
1409
  break
1359
1410
  if verified is None:
1360
1411
  return None
@@ -1383,9 +1434,18 @@ def _verify_and_install_from_local_blobs(
1383
1434
  scratch = _scratch_root(cache_write_root)
1384
1435
  tmp_dir = tempfile.mkdtemp(dir=str(scratch))
1385
1436
  try:
1437
+ # Size and sha256 against the layer descriptor BEFORE decompressing
1438
+ # (§7.3), on a copy in scratch, which is then the file unpacked.
1439
+ verified_layer = os.path.join(tmp_dir, "layer")
1440
+ copy_blob_verified(
1441
+ str(layer_path),
1442
+ layer_desc,
1443
+ verified_layer,
1444
+ what=f"pre-seeded layer {layer_desc.digest}",
1445
+ )
1386
1446
  unpacked_dir = os.path.join(tmp_dir, "unpacked")
1387
1447
  unpack_tar_zst(
1388
- str(layer_path),
1448
+ verified_layer,
1389
1449
  unpacked_dir,
1390
1450
  library_name=predicate["library"],
1391
1451
  library_sha256=predicate["library_sha256"],
@@ -1422,7 +1482,9 @@ def _verify_preseeded_entries(roots: Sequence[Path], trusted_keys: tuple[Trusted
1422
1482
  anywhere yet, and verifies+unpacks it from that root's OWN local blobs,
1423
1483
  writing the result into `roots[0]` (the writable cache) regardless of
1424
1484
  which root — including a read-only system directory — the blobs came
1425
- from."""
1485
+ from. An entry that already has a record is passed over without reading
1486
+ a blob, and each root's signature referrers are read once, never by
1487
+ reading a candidate's layer (public issue #690)."""
1426
1488
  cache_write_root = roots[0]
1427
1489
  already_verified = {record.manifest for _dir, record in list_verified_records(roots)}
1428
1490
  for root in roots:
@@ -1431,16 +1493,41 @@ def _verify_preseeded_entries(roots: Sequence[Path], trusted_keys: tuple[Trusted
1431
1493
  index_doc = json.loads(index_path.read_text(encoding="utf-8"))
1432
1494
  except (OSError, ValueError):
1433
1495
  continue
1496
+ blobs_dir = root / "blobs" / "sha256"
1497
+ candidates: list[str] = []
1498
+ candidate_layers: set[str] = set()
1499
+ manifests_read: dict[str, bytes] = {}
1500
+ sizes: dict[str, int] = {}
1434
1501
  for m in index_doc.get("manifests") or []:
1435
1502
  digest = m.get("digest")
1436
1503
  if not digest or digest in already_verified:
1437
1504
  continue
1505
+ candidates.append(digest)
1506
+ size = m.get("size")
1507
+ if isinstance(size, int) and not isinstance(size, bool):
1508
+ sizes[digest] = size
1509
+ try:
1510
+ manifest_hex = parse_digest(digest)
1511
+ manifest_bytes = (blobs_dir / manifest_hex).read_bytes()
1512
+ manifests_read[manifest_hex] = manifest_bytes
1513
+ layer = manifest_layer_descriptor(json.loads(manifest_bytes))
1514
+ candidate_layers.add(parse_digest(layer.digest))
1515
+ except (ValueError, OSError, FetchError):
1516
+ continue
1517
+ if not candidates:
1518
+ continue
1519
+ referrers = _local_bundle_referrers(blobs_dir, candidate_layers, manifests_read)
1520
+ for digest in candidates:
1521
+ if digest in already_verified:
1522
+ continue
1438
1523
  record = _verify_and_install_from_local_blobs(
1439
1524
  source_root=root,
1440
1525
  manifest_digest=digest,
1441
1526
  cache_write_root=cache_write_root,
1442
1527
  trusted_keys=trusted_keys,
1443
1528
  visible_only=True,
1529
+ referrers=referrers,
1530
+ manifest_size=sizes.get(digest),
1444
1531
  )
1445
1532
  if record is not None:
1446
1533
  already_verified.add(digest)
@@ -26,6 +26,7 @@ from chtypes._ocifetch._http import (
26
26
 
27
27
  __all__ = [
28
28
  "Descriptor",
29
+ "copy_blob_verified",
29
30
  "fetch_blob_bytes",
30
31
  "fetch_blob_to_path",
31
32
  "fetch_manifest_by_digest",
@@ -143,6 +144,46 @@ def verify_body_matches_digest(body: bytes, digest: str, *, what: str) -> None:
143
144
  )
144
145
 
145
146
 
147
+ def copy_blob_verified(src_path: str, descriptor: Descriptor, dest_path: str, *, what: str) -> None:
148
+ """Copy a LOCAL blob file (a pre-seeded layout's `blobs/sha256/<hex>`)
149
+ to `dest_path`, streaming, and check the copy's size and sha256 against
150
+ `descriptor` BEFORE the caller does anything else with it — the local
151
+ counterpart of `fetch_blob_to_path`, with the same §7.3 rule ("check
152
+ size and sha256 before decompressing"). The caller unpacks the copy it
153
+ verified, never the source file, so the bytes checked are the bytes
154
+ used. Reads at most `descriptor.size + 1` bytes; removes `dest_path` on
155
+ any failure."""
156
+ expected = parse_digest(descriptor.digest)
157
+ hasher = hashlib.sha256()
158
+ size = 0
159
+ try:
160
+ with open(src_path, "rb") as src, open(dest_path, "wb") as dst:
161
+ while size <= descriptor.size:
162
+ chunk = src.read(min(1 << 20, descriptor.size + 1 - size))
163
+ if not chunk:
164
+ break
165
+ hasher.update(chunk)
166
+ dst.write(chunk)
167
+ size += len(chunk)
168
+ if size > descriptor.size:
169
+ raise ArtifactCorruptError(
170
+ f"{what}: larger than the descriptor's size {descriptor.size}"
171
+ )
172
+ if size != descriptor.size:
173
+ raise ArtifactCorruptError(
174
+ f"{what}: size {size} disagreed with the descriptor's {descriptor.size}"
175
+ )
176
+ actual = hasher.hexdigest()
177
+ if actual != expected:
178
+ raise ArtifactCorruptError(f"{what}: sha256 {actual} disagreed with {expected}")
179
+ except BaseException:
180
+ try:
181
+ os.unlink(dest_path)
182
+ except OSError:
183
+ pass
184
+ raise
185
+
186
+
146
187
  def resolve_platform_manifest(index_doc: dict, platform_key: str) -> Descriptor:
147
188
  """Pick the one manifest descriptor in an OCI image index matching
148
189
  `platform_key` (e.g. "linux-arm64"), refusing a duplicate or absent
@@ -54,6 +54,8 @@ __all__ = ["Block", "Filter", "Library", "Schema", "Server", "open_unverified"]
54
54
  # One image per file, process-wide: keyed on the realpath's device and inode, so
55
55
  # two registries, two spellings or a hardlink of one artifact share one image
56
56
  # and one Library. Guarded by the setup lock, which an open holds throughout.
57
+ # The batch preview's default `doc_flags`: 0, the lean result (no per-value documents).
58
+ _LEAN_DOC_FLAGS = DocFlags(0)
57
59
  _IMAGES: dict[tuple[int, int], Library] = {}
58
60
 
59
61
 
@@ -212,7 +214,8 @@ class Filter(_Handle):
212
214
  insert_options_extra: Mapping[str, str] | None = None,
213
215
  ) -> FilterResult:
214
216
  """Evaluate over a body. `settings` and `session_timezone` are the body's
215
- PARSE settings only: the filter's own zone was fixed when it compiled.
217
+ INSERT settings: they govern its parse and the expressions the INSERT
218
+ evaluates, never the WHERE, whose zone was fixed when the filter compiled.
216
219
  `columns` is the INSERT column list and the keywords after it the
217
220
  `insert_options` members, as for `Schema.rows`."""
218
221
  payload = body_bytes(body)
@@ -304,11 +307,14 @@ class Schema(_Handle):
304
307
  insert_options_extra: Mapping[str, str] | None = None,
305
308
  row_filter: Filter | None = None,
306
309
  export: Format | int | None = None,
307
- doc_flags: DocFlags = DocFlags.ALL,
310
+ doc_flags: DocFlags = _LEAN_DOC_FLAGS,
308
311
  ) -> BatchResult:
309
312
  """A whole body. `export` asks for the accepted rows serialized in that
310
313
  format (`payload`, and `spans`); to insert a body whose rows carry a
311
- `default_generated` column, insert `payload`, never the original input."""
314
+ `default_generated` column, insert `payload`, never the original input.
315
+
316
+ `doc_flags` chooses the document groups; absent means 0, the lean result
317
+ (no per-value documents). Pass `DocFlags.ALL` for every group."""
312
318
  payload = body_bytes(body)
313
319
  settings_bytes = settings_json(settings, session_timezone)
314
320
  columns_bytes = columns_json(columns)
@@ -158,10 +158,27 @@ def test_rows_marshals_columns_filter_export_and_doc_flags(lib, monkeypatch) ->
158
158
  assert fmt == 0 and body == b'{"a":1}\n' and settings is None and options is None
159
159
  assert json.loads(columns) == [{"name": "a"}, {"name_b64": "/w=="}]
160
160
  assert handle is not None and export == int(Format.CSV) and flags == 3
161
- assert plain[5] is None and plain[6] == -1 and plain[7] == 7 # no export, all groups
161
+ assert plain[5] is None and plain[6] == -1 and plain[7] == 0 # no export, lean (0)
162
162
  assert plain[2] == b"1\n" # a bytes-like body is copied to bytes
163
163
 
164
164
 
165
+ def test_rows_doc_flags_default_is_lean_and_explicit_all_reaches_the_call(lib, monkeypatch) -> None:
166
+ calls: list[tuple] = []
167
+ real = lib._api.preview_batch
168
+
169
+ def spy(*args):
170
+ calls.append(args)
171
+ return real(*args)
172
+
173
+ monkeypatch.setattr(lib._api, "preview_batch", spy)
174
+ with lib.compile_table(CREATE) as schema:
175
+ schema.rows(Format.CSV, b"1\n")
176
+ schema.rows(Format.CSV, b"1\n", export=Format.CSV)
177
+ schema.rows(Format.CSV, b"1\n", doc_flags=DocFlags.ALL)
178
+ # Argument 7 of chs_preview_batch is doc_flags, as the real stub call received it.
179
+ assert [c[7] for c in calls] == [0, 0, int(DocFlags.ALL)]
180
+
181
+
165
182
  def test_no_export_means_no_payload(lib) -> None:
166
183
  with lib.compile_table(CREATE) as schema:
167
184
  assert schema.rows(Format.CSV, b"1\n").payload is None