My First Contribution to ClickHouse: Two Weeks From "My Laptop Can't Build It" to 154 Green CI Checks

Posted on August 01, 2026 by admin 13 min

two weeks ago my Mac couldn't even build ClickHouse. it failed before the compiler got a chance to be interesting — wrong filesystem, wrong toolchain, not enough disk.

today i have a fix sitting in a PR against ClickHouse/ClickHouse with 154 CI checks green and zero red — PR #112662, fixing issue #111193.

this is the whole messy path, including the parts where i wasted days. if you've ever looked at a giant C++ codebase and thought "that's for other people", this post is for you.

why i wanted this

i wanted to work on C++ in production. not tutorial C++ — the kind that runs on real machines, under real load, where being wrong shows up as a crash and not a red squiggle. the fastest way i know to get that is to go where that code already lives and try to be useful.

ClickHouse is a good place for it. it's a columnar OLAP database, it's enormous, it's fast in ways that are genuinely educational, and the maintainers actually review outside contributions — the CONTRIBUTING.md is short and worth reading before you touch anything. their architecture overview is the other thing i'd read first.

so: pick a project, build it, find a bug, fix it. simple plan. it took about a day for the plan to fall apart.

act 1: i couldn't even build it

the first wall wasn't the code. it was my laptop.

three things went wrong at once:

  • case-insensitive filesystem. macOS by default treats Foo.h and foo.h as the same file. ClickHouse (and its vendored dependencies) do not. things break in confusing ways.
  • disk. a debug build of ClickHouse produces a binary around 6.5 GB. that's one binary. add object files and you're well past what i wanted to spare.
  • toolchain. ClickHouse wants clang 21+. it rejects GCC outright. it also needs a pinned Rust nightly for some vendored components — not "a nightly", a specific one. the developer instructions spell all of this out; i'd skimmed them, which is not the same as reading them.

i spent longer than i should have trying to bend the Mac into shape. eventually i gave up and did the thing i should have done first: spun up a Linux box. an LXC container on my Proxmox server, 24 cores, plenty of disk.

first cold build: 2 hours 13 minutes.

the lesson here is boring but real — for a codebase this big, match its first-class platform instead of fighting your laptop. the maintainers build on Linux with clang. every CI job runs on Linux with clang. going anywhere else means every problem you hit is a problem nobody else has, and nobody can help you with.

also, that build time changes how you work. you can't iterate the way you do on a web app. you think harder before you compile, because a mistake costs you a coffee break.

act 2: two false starts

false start one: i wrote code nobody needed

i found a feature request — BETWEEN SYMMETRIC, standard SQL syntax that swaps the bounds if they're backwards. small, contained, perfect first task.

i implemented it. wrote tests. everything green. opened PR #105955.

then i went looking around and found #106057 — a PR doing the same thing, opened two months earlier.

i closed mine. it stung a bit, but it was entirely my fault.

the lesson: before you write a single line, search the open PRs for the issue number, and then comment on the issue saying you're taking it. both steps. one without the other doesn't work.

useful detail if you're going to try this: ClickHouse doesn't use the good first issue label. their equivalent is easy task. i lost time looking for a label that doesn't exist.

false start two: claiming etiquette

next i picked up SHOW USERS LIKE — another contributor (david) had started it and graciously handed it off when i asked. great. i started working.

then a third person showed up on the issue: "i've already started on this."

that one's still in the pipeline as PR #111692, and honestly the outcome matters less than what it taught me. issues in big repos are a shared resource with no locking. the comment claiming the issue is the lock. it's a social protocol, not a technical one, and it only works if you participate in it loudly and early.

two attempts in, i hadn't landed anything. but i'd learned the process, which turned out to be most of the job.

act 3: the bug that stuck

then i found issue #111193. labelled easy task and bug, triaged by alexey-milovidov — ClickHouse's founder. it was a logical error: an internal assertion failure that crashes a debug build, found by their AST fuzzer (a tool that generates weird-but-valid queries all day looking for exactly this).

first thing i did — before reading any code — was reproduce it. on my debug build the query died with exit code 134, which is SIGABRT. the process aborted itself.

confirming the repro is step zero. if you can't make it fail on demand, you can't know you fixed it. everything after this depends on it.

the bug, in plain language

ClickHouse has aggregate function states — partial results you can store and combine later, produced by the -State combinator. two of them matter here:

the catch: they store their state identically in memory. the bytes are the same shape. only the finalization step differs.

now UNION the two together. ClickHouse looks at both branches, sees a compatible state type, and picks one declared result type for the whole thing. but at runtime, each row still finalizes through its own function. so half the rows produce a Float64 and half produce an Array(Float64), while the column has already promised to be one of those.

the produced type disagrees with the promised type. ClickHouse notices, and rather than corrupt memory it does the right thing and aborts with a LOGICAL_ERROR.

the shape of it in C++

this is the part i found genuinely worth learning, because it's a class of bug, not one bug.

every function in ClickHouse has two methods that matter:

  • getReturnTypeImpl — the type this function promises to return, computed from argument types alone, before any data is touched.
  • executeImpl — the column it actually produces, at runtime, from real data.

query engines split these on purpose. planning happens over types, execution happens over data. the planner needs to know the shape of the result without running anything.

but it means there's a contract between the two methods, and the compiler cannot check it. nothing stops executeImpl from returning something getReturnTypeImpl never promised. when they diverge, you get exactly this crash.

once you see it, you start seeing it everywhere.

the fix

before writing anything i asked on the issue which direction the maintainers wanted — reject it up front, or catch it at execution. PedroTadim (a member) said go ahead with the guard.

the fix is small: in executeImpl, compare the column's actual finalization type against the declared one. if they differ, throw a clean ILLEGAL_TYPE_OF_ARGUMENT — a normal, user-facing SQL error explaining what's wrong — instead of an internal assert that kills the process.

that's the whole idea. the user gets an error message instead of a crash.

then a maintainer assigned the issue to me. small thing. felt huge.

a few C++ details i hit along the way, each worth one sentence:

  • incomplete types — a forward-declared class is just a promise the type exists; you have to actually #include its header before calling its methods. i hit this as a build error and it took me a minute to understand why the compiler could see the name but not the methods.
  • typeid_cast — ClickHouse's fast downcast. pointer form returns null on mismatch, reference form throws. picking the right one is your error handling.
  • inline in headers and the One Definition Rule — a program may only have one definition of a function; inline tells the linker "multiple identical copies are fine, merge them". that's why a small shared helper can live entirely in a .h with no .cpp.
  • const& params, move semantics, copy-on-write columns — a column here can be gigabytes, so nothing gets copied casually. you pass by reference, you move instead of copy, and columns are shared until someone actually writes.

act 4: where the real work was

opening the PR (#112662) was not the finish line. it was where i learned the most.

first, a bot asks you to sign the CLA on your first PR. two minutes, do it and move on.

then an automated reviewer pointed out that a sibling function, runningAccumulate, had the exact same bug. it was right. rather than patch the same logic twice, i pulled the check into a shared helper and fixed both call sites.

good review makes the PR better. engage with it, don't defend against it. the reviewer found real scope i'd missed.

then CI failed. three times. three different reasons.

these are the best part of this whole story, because each one is a lesson about determinism disguised as a build failure.

trap 1 — old vs new analyzer. ClickHouse runs every test under two query engines: the older analyzer and the newer one (what the analyzer actually does). one of my repro queries only reproduced the bug under the newer analyzer, so the old-analyzer job failed on an unexpected result. fix: keep a repro that behaves identically under both, and pin the analyzer-specific one with SETTINGS enable_analyzer = 1. your test has to be a statement about behaviour, not about your configuration.

trap 2 — the style check. CI forbids the literal phrase "new analyzer" in comments. it's been the default since v24.3, so calling it "new" is misleading. a wording rule failed my build. i laughed, fixed the comment, pushed again. 🙏

trap 3 — random settings flakiness. there's a "flaky check" job that reruns your changed tests many times with randomized settings (their testing docs cover how the whole suite is structured). my positive test asserted the exact output rows of runningAccumulate — which is order-dependent. random optimization settings reordered the rows, the output changed, the test failed intermittently.

the fix was to assert count() instead of exact values — order-independent, still proves the function ran without crashing.

that one's the deepest lesson in the post. relying on an order you don't control is the same root cause as a data race. the code isn't "usually right, occasionally unlucky" — it was wrong the whole time, and randomized CI just made the wrongness visible. nondeterminism isn't flakiness. it's a bug that hasn't picked a side yet.

verifying a review before implementing it

one more review point: the bot suggested hardening against an exotic -StateState combinator case.

before adding the code, i tried to reproduce it. ClickHouse rejected the example itself — Code: 184, nested identical combinator not supported. the case can't happen; the engine blocks it earlier.

so i replied honestly: here's what i tried, here's the error, could you give me a real repro if i'm missing something?

verify the premise of a review before implementing it. don't blindly add defensive code for an impossible case — you're adding complexity someone maintains forever. but don't dismiss it either. asking costs nothing and you might be the one who's wrong. 🤔

act 5: turning the pain into tooling

after the third CI failure i stopped and wrote scripts that run CI's checks locally, before pushing:

  • the style check
  • the test under both analyzers
  • a flaky loop that reruns the test with randomized settings, the way CI does

plus a small poller so i could check PR and CI state without sitting on a browser tab all day.

none of it is clever. all of it removed a class of failure permanently. if a loop hurt you once, automate it before it hurts you twice.

on using AI

i used Claude throughout — navigating an unfamiliar codebase, understanding the aggregate-state machinery, drafting tests. ClickHouse's AI policy (in CONTRIBUTING.md) welcomes it and disclosure is optional, but i'd rather say it.

what it didn't do: understand the bug for me. every one of those CI failures was mine to diagnose, and the review conversation about the -StateState case needed me to actually run the query and read the error. an AI can accelerate reading a codebase enormously. it cannot own a line of code in front of a maintainer. you still have to be able to defend everything you submit — and if you can't, you shouldn't have submitted it. their policy says low-effort PRs get closed, and that's the reason why.

where it stands

the fix — PR #112662 — is green across ClickHouse's full CI matrix — around 171 jobs at last count, 154 passing and 0 failing at the time of writing (the rest gated behind later waves). that matrix is builds × sanitizers (asan, ubsan, tsan, msan, debug) × both analyzers × storage configurations × query plans × shard counts, plus integration tests that spin up real Docker containers.

it's not merged yet — it's awaiting maintainer approval. so i'm not going to claim a merge i don't have. what i have is a correct, fully-green fix in review, which is the part i actually control.

what i actually learned

the C++ / systems part: - type inference and execution are separate concerns in a query engine, and the contract between them is enforced by humans, not compilers - incomplete types, ODR and inline, typeid_cast's two forms, const& and move semantics — all learned by hitting them, which is the only way they stick - determinism is a correctness property, not a testing detail

the process part: - match the project's platform, don't fight it - claim the issue in public before writing code - reproduce before you fix - run CI's checks locally - engage review, and verify review

the honest part:

i spent two weeks and my first two PRs went nowhere. that's not a failure story, that's the actual shape of learning a new codebase. the false starts taught me the process that made the third one work.

the green checks are the receipt. the real thing is knowing that with a reproducible bug, some patience, and a willingness to be visibly wrong in front of strangers, you can walk into a world-class C++ codebase and ship a correct fix. that's not a talent thing. it's a showing-up thing. 🎉

if you want to try this yourself

  1. build it on the platform CI uses — follow the developer instructions properly. a cheap Linux box beats a fancy laptop here.
  2. find an easy task (not good first issue — that label doesn't exist in ClickHouse).
  3. search open PRs for that issue number. then comment on the issue to claim it. both.
  4. reproduce the bug before you touch any code. if you can't, pick another one.
  5. ask the maintainers which fix direction they want before writing it. one comment saves a rewrite.
  6. run CI's checks locally — style, all engine variants, and a randomized rerun loop.
  7. engage the review. if a suggestion seems wrong, go try to reproduce it and report what you found. asking is not weakness.

i am still a little new to this codebase. that turned out to be fine — saying so out loud is what got people to help.

links

everything referenced above, in one place:

if you liked the "build the tool that removes the pain" part of this, that's basically the whole story of datasip too.