Skip to content

Go and TypeScript can load the wrong ClickHouse line: a lower-line request is answered by a higher line already cached (measured, 1.0.2) #481

Description

@EricAndrechek

Go and TypeScript can load the wrong ClickHouse line. A request for a LOWER line is answered by a HIGHER line already in the cache. Measured on published 1.0.2.

What happens. Each case below used a fresh cache. "Returned" is the loaded library's own reported ClickHouse version.

binding cache holds request returned
Go 1.0.2 26.8.15.10 For("26.3") 26.8.15.10
Go 1.0.2 26.8.15.10 For("26.3.38.2") (exact) 26.8.15.10
Go 1.0.2 26.9.8.3 For("26.8") 26.9.8.3
TS 1.0.2 the same three the same three the same wrong answers
Go/TS 1.0.2 26.3.38.2 For("26.8") 26.8.15.10 (correct)
Go/TS 1.0.2 empty For("26.3") 26.3.38.2 (correct)
Python 1.0.2 26.8.15.10 26.3 / 26.3.38.2 26.3.38.2 (correct)
  • Rust is correct by its code (inferred; not run here).
  • The writer doesn't matter. A cache written by Go, Python or TypeScript is answered wrongly by every Go and TypeScript reader.
  • The CLI does the same. chtypes fetch 26.8 --cache C, then chtypes fetch 26.3 --cache C, exits 0, prints the 26.8 directory plus a "keeping the existing/newer install" warning, and installs nothing for 26.3. A later chtypes fetch 26.3 --offline (Go) reports CHTYPES_ARTIFACT_MISSING. The online and offline paths disagree, so the cache never repairs itself.

Cause. The "never go backwards" (monotonic) check compares the resolved candidate against EVERY installed entry for the platform, ignoring the request:

  • Go: newerAlreadyInstalled, go/internal/ocifetch/ensure.go;
  • TypeScript: checkMonotonic, ts/src/ocifetch/ensure.ts.

Rust (ensure.rs) considers only installed builds within the version request (the check is skipped only for a literal, non-numeric tag request). Python (_ensure.py) compares only within the same ClickHouse version.

This violates docs/guides/fetch-v1.md §9: a request may be answered only by a build whose signed version is within it.

Who is affected. Anyone using Go or TypeScript whose cache holds more than one ClickHouse line: a shared or mounted cache with autofetch, CI that tests several lines, or a developer machine. The wrong library loads silently, apart from the warning, and every answer it gives is the other line's.

Workaround until the fix ships:

  1. keep one ClickHouse line per cache directory;
  2. after opening, assert the loaded library's version is within the requested line (Go: strings.HasPrefix(lib.Version(), "26.3.")), and refuse otherwise.

Fix (planned as 1.0.4, all four bindings together):

  • Go and TypeScript: the monotonic check considers only installs within the request (same line; an exact request is never overridden), matching Rust and Python.
  • A load-time assertion in all four bindings: the loaded library's reported version must be within the requested line, else a typed error. A wrong-line load then fails loudly, whatever the cache did.
  • Regression: a fetch conformance case (cache holds a higher line, request a lower one and an exact lower build) run by every binding, plus an N×N case across writer binding × reader binding × line in the cache-interop job.

🤖 Generated with Claude Code

Activity

  1. EricAndrechek commented on Oct 6, 2026

    @EricAndrechek
    MemberAuthor

    Correction and precision.

    Correction: the body first said Rust "skips exact requests". It doesn't. Rust considers only installed builds within the request, for every numeric request; the check is skipped only for a literal, non-numeric tag. Rust stays correct (inferred from code). The body is fixed.

    When it triggers:

    • For() / for() first look for an install of the requested line itself, and that path is correct.
    • So the wrong answer needs a request whose line has no install of its own while a higher line is installed.
    • chtypes fetch <line> takes the same path whenever that line is not yet installed.

    Affected releases:

    • Go 1.0.2 and TypeScript 1.0.2: measured.
    • Go 1.0.0 and 1.0.1, and TypeScript 1.0.0: inferred. Their fetch code is byte-identical to 1.0.2 (equal git blob hashes).

    Workarounds on Go/TypeScript 1.0.x until 1.0.4:

    1. Refuse a wrong line after opening. After For(), refuse unless the library's Minor equals the requested line. Minor is read from the loaded library's own build_info.
    2. Pre-fetch lines in ascending order (measured: a cache holding 26.3, then asked for 26.8, is correct).
    3. One cache directory per line.
    4. Pre-fetch with the Python CLI (measured: Python chtypes fetch 26.3 into a cache holding 26.8 installs 26.3.38.2).
  2. EricAndrechek commented on Oct 6, 2026

    @EricAndrechek
    MemberAuthor

    Fixed on main by #484 (merged as a8ac5dd) in all four bindings. It ships in 1.0.4, whose release prep is in progress. Until 1.0.4 is on your registry, the workarounds above still apply.

  3. EricAndrechek commented on Oct 6, 2026

    @EricAndrechek
    MemberAuthor

    Shipped in 1.0.4 on all four registries: crates.io, npm, PyPI and the Go module. Upgrade, and the workarounds above are no longer needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions