The forge reads .github/workflows natively and github.com reads nothing else,
so one directory serves both planes instead of two conventions that disagree
about which one is live. The reusable in hanzoai/ci already lives there, so a
caller naming that path now resolves to a file that exists.
Co-authored-by: Hanzo Dev <[email protected]>
hanzo-build-linux-amd64, lux-build-linux-amd64, zoo-build-linux-amd64 and
every other org-prefixed spelling all meant the same machine. Which org a
job belongs to does not change which host can build it, so the org bought
one pool name per org per arch and each could go stale on its own -- the
evo host alone answered to fourteen names for one label.
One vocabulary now: linux-amd64, linux-arm64, macos-arm64, win-amd64. The
generic labels every plain workflow uses (ubuntu-latest, self-hosted) are
untouched, so naive CI keeps working.
Co-authored-by: Hanzo Dev <[email protected]>
hanzoai/ci keeps the reusable at .github/workflows/build.yml — the one path
both planes resolve, since github.com reads a reusable only from there and the
forge reads it too. The v1 tag moved with it, so a caller still naming
.hanzo/workflows/build.yml asks for a blob that is not in the tree:
InsertRun: read hanzoai/ci@v1:.hanzo/workflows/build.yml: object does not exist
The caller itself stays at .hanzo/workflows/cicd.yml on purpose: github.com has
no runner for these labels, so a caller placed there could never be scheduled.
Co-authored-by: Hanzo Dev <[email protected]>
One family. Font stacks name Zen and fall back to the native faces, tokens
name the role rather than a face, and the packages that used to supply a
face now point at @hanzo/font.
Self-hosted @font-face blocks are removed rather than renamed: rewriting the
family in one would leave a src pointing at a file this repo no longer ships.
@hanzo/font carries the binaries and declares the faces, so the local copies
go with the declaration that used them.
Comments naming a face we no longer use are deleted, not reworded.
Co-authored-by: Hanzo Dev <[email protected]>
runs-on was a bare scalar. Jobs written that way queue and are never
picked up — link and quest have waited since 10 August, painter since the
5th, none of them with a single success — while repos writing the same
label as a sequence drain within seconds of a push on the same fleet.
Same label, same runners, different shape. It fails silently: nothing errors,
the job simply never starts, so the lane looks configured and does nothing.
The fork already renamed its internals - the built binary is datastore, the
packages are datastore-server and friends, and the shipped config.xml already
says Datastore - but the surfaces a human reads still said ClickHouse. Issue
templates asked which ClickHouse version was affected, the PR template told
contributors to close upstream's issues, and CI job names, log lines, error
messages and argparse descriptions named a product this repo does not ship.
Only prose moved: display names, log and error messages, comments, docstrings
and form labels. Everything a machine resolves stayed exactly as it was,
because renaming those breaks a build rather than a sentence - binary and path
tokens such as clickhouse-client and /etc/clickhouse-server, python and shell
identifiers, CLICKHOUSE_* environment variables, the /ClickHouse container
mount, docker images in a namespace we do not own, S3 buckets, maven and pypi
coordinates, and every URL pointing at upstream. The attribution in NOTICE and
README is untouched.
Two links were wrong rather than merely stale, so they are corrected here. The
PR template's Closes and Related placeholders sent contributors to upstream's
tracker instead of this one, and an earlier pass had rewritten a source
reference to github.com/Datastore/Datastore, which does not resolve.
Co-authored-by: Hanzo Dev <[email protected]>
docs/ was 502M and 23,841 files: upstream's entire documentation site, in ten
languages, with their branding throughout. Nothing in our pipeline touches it —
the image builds from the root Dockerfile and cmake compiles no documentation,
and our CI is one image build rather than upstream's Praktika jobs, so the doc
jobs and the style check that reads this tree never run for us. docs.hanzo.ai
serves our documentation and 404s /datastore, so none of it reached a reader
either.
The licence is the part worth knowing. docs/LICENSE is CC BY-NC-SA 4.0 —
NONCOMMERCIAL — which is not this repository's Apache-2.0. So that tree was
never ours to ship in a commercial product, independent of whose name is on it.
That exposure did not stop at the tree: programs/server/docs.html inlined four
of those snippets (a JSON-type guide, two settings tables, a data-type mapping)
and that page IS served by the binary. Deleting the tree would have left the
copies behind, so DOC_SNIPPETS is emptied — the resolver still iterates it, so
an entry importing one renders without it rather than failing, and writing our
own is a matter of adding entries back.
The root LICENSE and NOTICE are untouched and still carry the upstream Apache
copyright. The eleven legal files that went with the tree were docs/LICENSE
itself and ten translations of one system-table reference page.
Co-authored-by: Hanzo Dev <[email protected]>
play.html's SQL highlighting was dead. The C API had been renamed to
datastore_lexer_* in Lexer.cpp and play.html's calls renamed with it, but the
base64 payload embedded in the page was never rebuilt — it still exported
clickhouse_lexer_*. So play.html asked WebAssembly for six functions the module
does not have. docs.html kept the old call names, which is the only reason its
highlighting still worked.
This is the failure mode worth naming: rename either side of a binary contract
alone and nothing errors loudly — the highlighter just stops highlighting.
Rebuilt from src/Parsers/Lexer.cpp with the flags src/Parsers/CMakeLists.txt
uses (-Os -fno-exceptions -fno-rtti -DLEXER_STANDALONE_BUILD --target=wasm32
-flto -nostdlib -Wl,--no-entry -Wl,--export-all, plus -std=c++20 for the fold
expressions). 6720 bytes, exporting datastore_lexer_{size,create,next_token,
token_is_error,token_is_end,token_is_significant}. Embedded in BOTH pages and
both now call those names — verified by instantiating the payload and checking
every symbol each page uses is present, which is the check that would have
caught this when the rename happened.
Co-authored-by: Hanzo Dev <[email protected]>
PrometheusMetricsWriter emits DatastoreProfileEvents_, DatastoreMetrics_,
DatastoreAsyncMetrics_, DatastoreErrorMetric_ and Datastore_Info, while four
consumers still asked for the ClickHouse spelling, so each was reading a string
its own server had stopped writing. The three integration tests failed outright,
the prometheus endpoint one twice over, since its allowed-prefix assertion
rejects every metric line before the query counter is ever looked up. The keeper
stress harness was quieter and worse: an allowlist matching nothing reads as a
run that produced no metrics rather than as an error.
The keeper-stress-analysis skill keeps the ClickHouse spelling on purpose. It
queries keeper_stress_tests.keeper_metrics_ts on play.clickhouse.com, where
those names are stored data written by upstream nightlies rather than anything
this server emits, and that table holds no Datastore rows at all. Renaming there
would turn a query that matches into one that matches nothing.
Co-authored-by: Hanzo Dev <[email protected]>
Renames the remaining button id (btnQueryClickhouse → btnQueryDatastore, one
definition and ten references across the two visualizers) and the test harness's
prose about which server it drives.
What stays, and it is not branding: `clickhouse/mysql-js-client` is an upstream
image the integration tests still PULL (ci/defs/defs.py, and the compose file
that runs it). A pull coordinate names someone else's published artifact, so
changing it is publishing our own test image rather than editing a string — the
harness comment now says that where a reader will be standing.
Also still standing, for the reason recorded at its embed site: the
`clickhouse_lexer_*` exports baked into docs.html's WebAssembly payload.
Co-authored-by: Hanzo Dev <[email protected]>
Apache-2.0 grants no trademark licence, so the upstream name and its links do
not belong on a surface a user of this fork sees. The four links that pointed
at the upstream repository — the source-code link, the two marks beside it,
and the About link in the map's attribution strip — point here.
What stays is not branding: the dashboard parses metric names off the wire,
and that prefix is what this server actually emits. The regex, the comment
describing the format and the example in the placeholder are one identifier,
not a name. Changing them means changing the metric the server publishes,
which is a decision about a wire format and every scrape config already
reading it.
The copyright and the attribution are untouched in NOTICE, which is where a
redistributor is asked to keep them.
Co-authored-by: Hanzo Dev <[email protected]>
These 99 lines were written while this repo's two remotes had diverged, on a
commit that only one of them carried, so reconciling them left the content off
main. It stands on its own and is worth having back: the half-renamed -Wundef
macro behind a two-hour build failure and the one-pass check that finds the
whole class of it, what the live 26.2.3.2 image actually is (upstream tarballs
plus a mv), and that the forge API lives at /v1.
Co-authored-by: Hanzo Dev <[email protected]>
The 31 workflows under .github/workflows were upstream ClickHouse's own Praktika
pipeline. Every one targeted a runner pool that exists only in ClickHouse's fleet
-- style-checker-aarch64, arm-large, amd-medium, release-maker and the pr-*
variants -- so none could ever be scheduled here. On git.hanzo.ai they were
already unreachable, because WORKFLOW_DIRS takes the first directory that exists
and .hanzo/workflows does. GitHub kept executing them anyway: revert_broken_prs
ran on a schedule and failed every attempt. Deleting the directory is what stops
it.
What goes with them, stated plainly: the upstream integration, functional,
stress, fuzzer, jepsen and sqlancer matrices; nightly coverage, statistics,
changelog and docs autogeneration; distroless rebuilds; and ClickHouse's release
automation -- auto_releases, create_release, backport_branches, cherry_pick,
merge_queue. None of it ran here. What we actually consume is gated by
.hanzo/workflows/cicd.yml, which builds this tree from source and makes the
binary answer a query before the image is pushed.
retry_infra_failures is gone for good. It reran failed runs of pull_request.yml
through the GitHub Actions API and drove GitHub auto-merge; both its subject and
its mechanism are GitHub's, and neither has a counterpart here.
upstream-sync returns, at .hanzo/workflows. It already ran on our own label and
was only collateral to an earlier sweep of .github/workflows, and LLM.md has
described it as live throughout. It now opens its pull request through the forge
API rather than the GitHub CLI, and names the branch after the upstream commit
instead of the day it ran, so a re-run on an unchanged upstream head does nothing
rather than proposing the same merge twice.
This pipeline builds a 135-submodule C++ tree from source -- hours per run,
against minutes for the rest of the estate -- and the file already explains that
the runner pool is the scarce thing, which is why there is no pull_request
trigger and why concurrency cancels in progress.
The same argument applies to paths and was missing: a push touching only
markdown queued a full build. Measured just now, run 64026, from a SECURITY.md
change. paths-ignore covers the files that cannot change the artifact; anything
that can is deliberately not in the list.
This repo is a fork, and its SECURITY.md still carried the upstream's contact —
so a researcher who found a hole in what WE ship was told to mail a vendor who
cannot fix it, and who would then know about a live vulnerability in our
production before we did.
Fourteen repos were in that state: datastore -> [email protected], git and
its two worktrees -> [email protected], auto -> [email protected],
calendar -> [email protected], dns -> [email protected], python-sdk ->
[email protected], plus bot, gui-gates, gui-geist, playground and
playground-oauthcb pointing at brand domains with no mailbox behind them.
All of them now carry one policy and one address, [email protected], matching
what cloud and gateway already used. The policy says out loud to report here
rather than upstream, because the next fork will inherit the same file and the
same instinct.
The two-hour failure is a half-renamed `-Wundef` macro, not memory and not the
job ceiling, and it is worth writing down with the one-pass check that finds the
whole class — every miss costs another two-hour round trip.
Also records what the live 26.2.3.2 image actually is (upstream tarballs plus a
`mv`, with nginx baked in from the start), which is what makes "rebase the rename
onto 26.2" a new from-source image rather than a cherry-pick; that config
committed to universe does not reach a cluster whose Application is
non-enforcing, and that act_runner reads its ceiling only at boot; and how to
reach the forge API, which lives at /v1/ and no longer mirrors.
Co-authored-by: Hanzo Dev <[email protected]>
CMakeLists.txt sets DATASTORE_CLOUD and src/Common/config.h.in emits it as
`#cmakedefine01 DATASTORE_CLOUD`, so DATASTORE_CLOUD is the only spelling that
ever reaches the preprocessor. Eighteen `#if` sites still read CLICKHOUSE_CLOUD,
which is defined nowhere — and this tree builds with -Werror,-Wundef, so each one
is a hard error rather than a silently-false branch.
That is what has been failing the from-source build, roughly two hours in, once
ninja reaches Storages/: MergeTreeData.h and the two Iceberg Compaction files
report `'CLICKHOUSE_CLOUD' is not defined, evaluates to 0`. It reads like the
build died of exhaustion because of when it lands, which is how it was twice
answered as memory and once as a timeout.
The definition side of the rename was already complete — the CMake variable, the
config template and the contrib guards all say DATASTORE_CLOUD. Only the reads
were missed, so this renames the reads and nothing else.
Co-authored-by: Hanzo Dev <[email protected]>
Written down because it was rediscovered from scratch: there is no client binary, the logs are not
on the PVC so a crashing container takes them with it, the x-datastore-* headers come from an
nginx proxy rather than the engine, and two config faults brick startup -- a `--` inside an XML
comment, and a merge pool below the mutation free-entries threshold.
Also the parts death spiral (tiny parts are the symptom, not the cause), the subPath-mount
landmine, the two bugs the rename left, the contract-line renames that are already throwing, and
an honest cost for ZAP -- which is present as a port-opener serving a null capability, not a
transport.
The broad pass renamed prose and strings but stopped short of a handful of
places where the old name was load-bearing, and in two of them it moved a
declaration without its use. What is left here is the correction, not more
renaming.
Two are bugs, not cosmetics:
BuzzHouse `MySQLIntegration::truncateSuffix` still reads `is_clickhouse`,
a name that exists nowhere in the tree — the member is `is_datastore`
three lines above and three lines below. It compiles today only because
`ENABLE_BUZZHOUSE` is off; turning it on does not build.
`FileChecker` writes `{"datastore":...}` into sizes.json but reads only
`datastore` or `yandex`. A directory written by a build that said
`clickhouse` reads back as an empty file list — no error, no missing
file, just a checker that silently believes the table has no data.
`ConfigProcessor` already accepts all three names for the config root;
this makes the on-disk reader agree with it.
Two gtests assert `ClickHouseHistogramMetrics_` / `ClickHouseDimensionalMetrics_`
against a writer that emits `Datastore*`, so they cannot pass.
`SOURCE(CLICKHOUSE(...))` is no longer valid SQL — the dictionary source
registers as `datastore` — but 48 examples in `FunctionDocumentation` still
teach it, and `system.functions` serves those to users. They now say
`DATASTORE`, matching the 12 that already did.
The rest identify the product at runtime: the `DATASTORE_RHT_KERNEL` env
var, the `datastore_disk_checker_` probe file, the Arrow type-mapping
error, the two window-function docs, the Kafka key set, and a fatal-error
log line that sent our operators to upstream's issue tracker.
Deliberately NOT renamed, because renaming them breaks something real:
the 162 `github.com/ClickHouse/ClickHouse` citations (attribution), the
contributor handles in the generated contributors table (people's names),
and `ClickhouseClientOptions` / `GetClickhouseOptions` in the Azure
blob-storage layer (vendored SDK API — ours to call, not to rename).
Co-authored-by: Hanzo Dev <[email protected]>
Run #8 falsified the previous commit. It ran with 3 compile jobs and died the
same silent way as run #7 — log ending mid-line, no error, no FAILED:, no
"ninja: build stopped" — but EARLIER, at [9688/15284] instead of [11637/15284],
and in rocksdb translation units, which are not memory-hungry. Less parallelism
producing an earlier death is not what a memory ceiling does.
What the two runs actually share is a clock. Job durations were 190.5 and 193.5
minutes. act_runner's job timeout is 3h (git-runner-config says so in as many
words: "runner.timeout allows 3h"), and 3h plus teardown is what both runs spent.
A timeout tears down the container and truncates the log identically to an OOM,
which is why the two are indistinguishable from a single run's evidence — the
distinguishing signal only appears when you line up wall time across runs with
different settings.
So the memory story was wrong. There is no evidence any of this ever OOMed: the
runner pod showed restarts=0 and an empty lastState both times, and no OOM or
eviction event was recorded. Six jobs never exhausted anything; it simply got
further before the same clock ran out. Restored to 6, the pod's CPU limit.
The build does not fit the budget, and that is the finding rather than a tuning
problem. At 6 jobs ninja reached 76% of 15284 targets in 173.6 minutes, so a
complete build needs ~228 minutes of compiling plus ~17 minutes of preamble:
~245 against a 180 minute cap, 1.36x over, short by about an hour, on a pod
limited to 6 CPU. Nothing on the cmake line closes that. Resuming across runs via
a BuildKit cache mount does not either — runs #7 and #8 landed on git-runner-6
and git-runner-18, and each runner has its own dockerd, so a warm /build is a
1-in-20 accident rather than a mechanism.
Closing this needs a longer job timeout for this workload or a larger dedicated
runner. Both are platform decisions, so they are recorded in the Dockerfile and
here instead of being hidden under progressively smaller job counts.
Run #7 compiled for 174 minutes, reached 11637 of 15284 targets, and stopped. Not
failed: stopped. The log's last line is a Building CXX for
Functions/FunctionsConversion_impl27.cpp.o and then nothing — no error, no
FAILED:, no "ninja: build stopped", no signal reported anywhere in 2 MB of log.
git-runner-6 showed restarts=0 and lastState={}, and the namespace recorded no
OOM or eviction. That combination is the signature this Dockerfile already
described: the kill lands on a process inside the pod's own dockerd, so the
kubelet never sees it and the only symptom is a log that ends mid-sentence.
It was our own budget, as far as the evidence reaches. No sibling build finished
anywhere near 00:31Z, and the node now idles at 10 GB of 62.8, so a neighbour
peaking at the same moment is possible but unevidenced. What is evidenced is
where it died. limit_jobs.cmake budgets MAX_COMPILER_MEMORY=2500, which put 6
jobs near 15 GB and looked comfortable inside 26 GiB — but 2500 is an average
over ~15k targets, and FunctionsConversion is the tail that average was never
describing. Upstream split it into impl00..impl27 because a single TU would not
build. Six of them at once exhausted the cgroup, which puts each above ~3.9 GB.
So: 3 compile jobs, holding even if the real peak is 7 GB apiece, rather than 4
which is only safe if it is under ~5.8. Half the cores sit idle through that
stretch and the build gets slower; a run that dies at 75% is slower still,
having spent three hours to produce nothing. Link stays at 1.
Raising this pod's 26Gi limit would be the wrong repair. git-runner runs 20
replicas packed 10 to a node, each requesting 4Gi against a 26Gi limit — 260Gi of
limit on a 62.8 GiB box. A bigger limit just relocates the same invisible kill to
the node. Right-sizing that fleet is a platform decision and is noted here rather
than worked around.
Configure now COMPLETES ("Configuring done (19.3s)") and the build died in the
generate step instead, on "No SOURCES given to target" for _zxc, _zxc_*_avx2/512,
_simdutf and libdeflate.
The targets are not empty; the paths were. CMake defines <PROJECT>_SOURCE_DIR,
and CMakeLists.txt:18 declares project(Datastore), so ClickHouse_SOURCE_DIR is
simply an undefined variable that expands to the empty string. Every contrib
wrapper still spelling it that way built its source path as "/contrib/zxc" or
"/contrib/simdutf/src" — rooted at /, not at the source tree — so the globs
matched nothing and add_library was handed an empty list. An undefined variable
is not an error in CMake, which is why this surfaced as a missing-sources
complaint about the target rather than a bad path.
This is the same unfinished rename as the previous commit wearing different
clothes. Swept both spellings across every in-repo cmake file: exactly two tokens,
ClickHouse_SOURCE_DIR and ClickHouse_BINARY_DIR, 23 occurrences in 13 files, all
of them contrib/*-cmake wrappers plus src/CMakeLists.txt. The submodules
themselves are untouched — the wrappers are ours, contrib/<name>-cmake is not a
.gitmodules path.
Renamed rather than aliased. A one-line set(ClickHouse_SOURCE_DIR
${Datastore_SOURCE_DIR}) in the root would also have worked and would cost less
on the next upstream merge, but the tree already carries hundreds of
Datastore_SOURCE_DIR uses in these same directories: the choice to rename was
made when the project was renamed, and these 13 files are the remainder of that
job, not a new decision. Two spellings for one path is the thing that produced
this bug in the first place.
configure died at src/CMakeLists.txt:127, "Cannot specify link libraries for
target clickhouse_compression which is not built by this project". Two lines
above, add_library creates datastore_compression. The rename reached the
definition and not the reference, and the miss survived because line 127 sits
behind `if (TARGET ch_contrib::sz3)` — it is only evaluated once SZ3 configures,
which nothing in this pipeline had ever got far enough to do.
Fixing that one line would only have surfaced the next, so this is the whole
class rather than the instance. Two sweeps, because one of them is not enough:
diffing every name referenced as the first argument of target_link_libraries /
target_include_directories / target_compile_* / add_dependencies /
set_target_properties against every name created by add_library / add_executable
/ add_custom_target finds 15 undefined, of which 13 are legitimate — corrosion
builds _cargo, _ch_rust_*, delta_kernel_ffi and chdig via corrosion_import_crate,
and google_cloud_cpp_* comes from functions in its own contrib cmake. That leaves
clickhouse_compression and clickhouse_common_io. But a first-argument diff cannot
see a stale name in a dependency LIST or a call to a command that no longer
exists, so the occurrence-level pass matters too and finds both.
Ten occurrences, all of them the same missed rename:
- undefined target, first arg, fatal at configure: src/CMakeLists.txt 127, 570, 578
- undefined names inside unit_tests_dbms's link list: 939, 940, 941. These fail
at LINK, not configure, and only when ENABLE_TESTS=1, so this pipeline would
never have reported them.
- calls to clickhouse_add_executable, which does not exist — the macro is
datastore_add_executable (CMakeLists.txt:668): benchmarks:26, fuzzers:22.
- inert: src/CMakeLists.txt:498 passes a name to add_object_library, whose body
ignores it entirely and only calls add_headers_and_sources(dbms ...); and a
comment at CMakeLists.txt:379 naming the old macro. Neither can break a build;
both are renamed so the next reader cannot mistake them for the live kind.
Renamed on evidence, not on the prefix: ch_contrib::* aliases and upstream contrib
targets keep their names, and every name changed here was confirmed undefined in
the tree with a defined datastore_* counterpart.
Two configure-time defects, both found only because the submodule and imgver
fixes let cmake run at all for the first time.
COMPILER_CACHE=disabled. A missing ccache is fatal here rather than advisory:
FAIL_ON_UNSUPPORTED_OPTIONS_COMBINATION defaults ON, which sets
RECONFIGURE_MESSAGE_LEVEL to FATAL_ERROR, and cmake/ccache.cmake takes that path
when it finds neither ccache nor sccache. The builder image has neither, so
configure stopped at ccache.cmake:48. `disabled` rather than apt-installing one,
because nothing in this Dockerfile or in hanzoai/ci mounts a cache: a compiler
cache here would start cold on every run and leave nothing behind for the next,
which is the same result as disabled plus an apt round trip. Installing one is
worth it only alongside a cache mount that outlives the layer.
PARALLEL_COMPILE_JOBS / PARALLEL_LINK_JOBS. cmake/limit_jobs.cmake derives both
from cmake_host_system_information, which reports the machine and not this
container's cgroup. On the runner fleet that reads 64311 MB / 8 cores while the
pod is capped at 26 GiB / 6 CPU, so it computes 64311/MAX_LINKER_MEMORY(5000) =
12 link jobs, clamps that to the host's 8 cores, and schedules 8 concurrent links
budgeted at 40 GB into a 26 GiB pod. The failure mode is the reason to fix it
before it happens rather than after: the kill lands on a process inside the pod's
own dockerd, so Kubernetes records no OOM event and the build log just stops,
which reads like a compile error and is not one. 6 compiles (the CPU limit; past
that only earns CFS throttling) and 1 link is 6*2500 + 5000 = 20 GB, with room to
spare. Both land as ninja JOB_POOLS.
The build never reached the C++ compile: imgver runs before docker buildx, and
it exits non-zero rather than let a build fall back to sha-<short>. datastore
declared a version nowhere imgver looks — no package.json, no Cargo.toml, no
VERSION, and the root pyproject.toml is pylint configuration with no `version =`
key — so it died with "no version for ghcr.io/hanzoai/datastore" about a minute
in. Run #2 did not show this because it ran an older build.yml that never called
imgver; the ci v1 that main resolves to now does.
26.7.1 is read off the tree, not chosen: cmake/autogenerated_versions.txt carries
VERSION_MAJOR 26, VERSION_MINOR 7, VERSION_PATCH 1. Upstream's VERSION_STRING is
the four-component 26.7.1.1 and imgver's semver is three, hence 26.7.1.
Declaring it is safe against an unknown registry floor, which matters because the
floor could not be read from here (the available credential lacks read:packages).
imgver publishes max(declared, published): verified against the real script at
three floors — none -> 26.7.1, 26.7.1 -> 26.7.2, 26.7.9 -> 26.7.10. The series
cannot be walked backwards by naming a lower number, and no tag can ever cover
two digests.
Same shape and same reason as hanzoai/postgres, which declares version: 18.0.0.
.gitmodules had 135 [submodule] stanzas and the tree had 134 gitlinks. The odd
one out was contrib/zap: config committed, gitlink never was. update-submodules.sh
feeds every .gitmodules path to `git submodule update`, so the path git had never
heard of died with `pathspec 'contrib/zap' did not match any file(s) known to git`,
xargs turned that into exit 123, and the image stopped at Dockerfile:95 ~90s in.
The other 134 clones succeeded around it, which is why the line is easy to miss.
It is load-bearing, not orphaned. contrib/CMakeLists.txt does
add_contrib (zap-cmake zap); zap-cmake reads 67 sources out of contrib/zap/c++/src;
src/CMakeLists.txt links ch_contrib::zap into dbms for src/Server/ZapServer.cpp.
Dropping the stanza instead would have traded a clone error for add_contrib's
FATAL_ERROR on the missing base folder.
Pinned to zap-proto/cpp-core main tip b444363 — the tip specifically, because
update-submodules.sh fetches --depth=1 --single-branch against .gitmodules'
branch = main, and any commit behind the tip costs a second, deeper fetch. All 67
paths zap-cmake names exist there, and the generated .zap.h/.zap.c++ are checked
in, so no bootstrap compiler is needed.
Also pins the caller to hanzoai/ci@v1, the tag the rest of the fleet uses. v1 and
v2 resolve to the same forge commit today, so behaviour is unchanged; this just
stops the one repo on an unmaintained alias from drifting when v1 moves. And the
header comment claiming this file cannot run until the repo is un-mirrored is
struck: runs #1 and #2 both fired from mirror-sync push events.
The S3 engine and table-function docs handed readers `'minioadmin',
'minioadminpassword'` five times as the way to authenticate, plus the
integration fixture's own `ClickHouse_Minio_P@ssw0rd` in the s3Cluster
examples. Copy-pasteable weak credentials in the reference docs are a worse
problem than the branding: they get pasted. Those are placeholders now, and the
endpoints point at Hanzo S3 (`hanzoai/s3` — SeaweedFS-derived, Apache-2.0, and
never a MinIO fork).
`registerStorageObjectStorage.cpp` also linked a docker-compose recipe at
github.com/Datastore/examples, a URL the ClickHouse->Datastore rebrand invented
and that has never resolved. The sentence no longer depends on it.
Scope is deliberately docs only, both copies of them: `docs/en/**` — the one
doc tree `docs/AGENTS.md` says to edit — and the `src/**` `R"DOCS_MD(...)"`
blocks that mirror it into the binary. Every changed line in a .cpp is inside a
documentation raw-string (verified by delimiter-span check, 21/21); the only
header change is a `///` comment. No executable code, no test, no fixture, no
build config.
The integration-test harness keeps MinIO on purpose, and LLM.md now records
why so this is not re-litigated or sed'd: `minio==7.2.20` is a real dependency
behind `from minio import Minio` in helpers/cluster.py, 60 test files call 16
distinct client methods, 136 tests pass with_minio=True, ~120 storage XMLs
hardcode the fixture credential, and the compose file leans on MinIO-only
surface (--certs-dir TLS, prometheus auth mode, console address, virtual-host
bucket aliases) fronted by a registry-pulled clickhouse/s3-proxy image. That
port is a real project whose acceptance test is the S3 suite going green, and
it cannot be run here: ghcr.io/hanzoai/s3 is linux/amd64 only and this is
arm64.
Co-authored-by: Hanzo Dev <[email protected]>
I moved this to .github/workflows on the reasoning that the forge copy is a
read-only mirror and GitHub only resolves reusables out of .github/workflows.
The first half is true; the conclusion was wrong. The GitHub org has ZERO
self-hosted runners registered — /orgs/hanzoai/actions/runners returns
total_count 0, because the estate moved to native CI — so a GitHub-side job
queues against a pool that does not exist. Verified on run 30409963733: accepted,
queued, never scheduled, cancelled. Left in place it would have done that on
every push to main forever, which is worse than not having CI at all.
So the caller belongs at the canonical native path after all, and the real
blocker is the repo, not the file: git.hanzo.ai/hanzoai/datastore is
mirror=true, and a mirror runs no CI. Converting it to a normal repo — the shape
hanzoai/iam and hanzoai/universe already have in that same namespace — is the
one change that makes this build. That precondition is documented in the
workflow rather than left for the next person to rediscover from a silent
no-run.
Also fix a configure-time failure in the Dockerfile: cmake/tools.cmake resolves
llvm-ar/ranlib/objcopy/nm/strip by versioned name and raises FATAL_ERROR when
one is missing. Those ship in llvm-N, not llvm-N-dev, which was the only one
installed — so the build would have died at cmake time.
Co-authored-by: Hanzo Dev <[email protected]>
The forge copy of this repo is a PULL MIRROR — mirror=true, 8h interval,
original_url github.com/hanzoai/datastore — so it is read-only, it syncs
GitHub -> forge rather than the other way, and a mirror runs no CI. GitHub is
the only forge that can build this repo, and GitHub resolves reusable workflows
only out of .github/workflows. The caller I put in .hanzo/workflows would have
been valid YAML that never ran: no error, no run, just an image that never
appears.
@v1 is not a downgrade. hanzoai/ci v1 and v2 differ by a file move and nothing
else — v1 holds .github/workflows/build.yml, v2 holds .hanzo/workflows/build.yml,
byte-identical — and the path is part of the version. Same pipeline, addressed
where GitHub can see it.
If the forge repo is ever de-mirrored to canonical, this moves back to
.hanzo/workflows/cicd.yml @v2 and nothing else changes.
Co-authored-by: Hanzo Dev <[email protected]>
This repo had no native CI, so nothing built it. Add the estate-standard pair —
a ~7-line caller into hanzoai/ci plus a root hanzo.yml holding the real config —
and the from-source Dockerfile they drive, publishing ghcr.io/hanzoai/datastore.
From source because the existing docker/server/Dockerfile installs .deb packages
from packages.hanzo.ai, which does not exist: the global rebrand rewrote the
upstream package host into that name, so the file reads as if we run an apt repo
and only fails at apt-get install. Source is the only path that yields OUR 26.7
rather than somebody else's binary. docker/server/* is left untouched — it is
machinery for a distribution channel we do not operate, and editing it would
conflict on the next upstream merge.
Two things the shared reusable does not do, handled in the Dockerfile: it runs a
bare actions/checkout with no submodule support, so the builder runs upstream's
own contrib/update-submodules.sh (which also strips vendored CMake files); and it
offers no post-build hook, so the artifact gate is the last layer instead —
SELECT version() and system.build_options through the binary just linked, which
fails the build rather than shipping an image that cannot answer a query.
Toolchain is pinned to what ci/docker/fasttest provisions, because that is what
this tree is known to compile under: cmake/tools.cmake hard-fails below clang 21
and the Rust contribs want the pinned nightly. ENABLE_TESTS=0 is the one
deviation from upstream defaults; ENABLE_DATASTORE_ALL stays on so the image
keeps keeper rather than losing it silently.
No deploy block. The live StatefulSet resolves its data directory to
/var/lib/hanzo-datastore while this tree compiles in /var/lib/datastore, so an
image-only swap would miss the PVC and every config mount, initialize an empty
database on the ephemeral layer, and still pass its /ping probes. That migration
needs a snapshot and a mount-path change in the same commit, not a rollout.
Co-authored-by: Hanzo Dev <[email protected]>
This workflow ran on a self-hosted actions-runner-controller pool. ARC is
decommissioned: CI executes inside git.hanzo.ai (Gitea Actions, .hanzo/workflows)
and delivery is cd.hanzo.ai reconciling the reviewed image pin in
hanzoai/universe. GitHub is a mirror and runs nothing.
Removed rather than repointed — there is no GitHub-side pool to move to, and
leaving it would keep ARC load-bearing.
This workflow ran on a self-hosted actions-runner-controller pool. ARC is
decommissioned: CI executes inside git.hanzo.ai (Gitea Actions, .hanzo/workflows)
and delivery is cd.hanzo.ai reconciling the reviewed image pin in
hanzoai/universe. GitHub is a mirror and runs nothing.
Removed rather than repointed — there is no GitHub-side pool to move to, and
leaving it would keep ARC load-bearing.
The config processor treats yandex, clickhouse and datastore as equivalent
root elements so a server started against an existing config file keeps
resolving it. A rename had left the clickhouse branch reading "datastore"
twice, so the third alias tested nothing and a clickhouse-rooted file was
rejected with "Root element doesn't have the corresponding root element".
Deployed config files under o11y and universe use that root, so this is the
difference between a server that starts and one that does not.
Co-authored-by: Hanzo Dev <[email protected]>
The README was upstream's community page with a find-replace applied. It
advertised ClickHouse Inc's meetups, release calls and careers page as ours,
pointed at a `datastore.cloud` we do not run, and carried four dead links plus
an install line that piped an HTML page into `sh`.
Replaced with what this fork actually is: separated storage and compute over
Hanzo S3, one binary with embedded coordination, OLAP-only engine set, no gRPC,
and the Raft to Quasar migration. Benchmark numbers keep their scale caveat.
Ports table and auth headers now match what the server reads.
Co-authored-by: Hanzo Dev <[email protected]>
A prior ClickHouse to Datastore rename rewrote `github.com/ClickHouse/ClickHouse`
to `github.com/Datastore/Datastore`, a repository that does not exist. That
turned 162 references across 131 files into dead links, and turned captured
stack traces and historical changelogs into false statements.
A citation of upstream is a fact, not branding. Product surfaces keep the Hanzo
Datastore name; references to the upstream project point at the upstream project.
Also untracks `.claude/`, agent scratch that was never meant to be committed.
Co-authored-by: Hanzo Dev <[email protected]>
13,838 upstream commits, forward from the 26.6.1.1 fork point. 129 conflicts:
37 resolved mechanically (take upstream's file, re-apply the brand sweep) and
92 by hand where our fork and upstream had both changed the same code.
The rule throughout: upstream owns the engine, we own the naming and a small set
of deliberate deviations. Never resolve by discarding an upstream change — a
dropped rename or a missed new parameter becomes a compile error a thousand
files away.
Deviations verified intact after the merge:
ENABLE_XRAY still defaults OFF (CMakeLists.txt:105)
X-Datastore-User read by the server (authenticateUserByHTTP.cpp)
X-ClickHouse-* in src/ 0
hanzo/research present
system.*_log retention bound present
ArrowFlight rip held against upstream re-adding it
Two things worth naming, because neither shows up as a conflict:
src/CMakeLists.txt gained an UNGUARDED
`add_object_library(datastore_storages_arrowflight Storages/ArrowFlight)` from
upstream, for a directory our gRPC rip deleted. It sat outside every conflict
hunk, so a resolver reading only the markers would sail straight past it into
a CMake configure error. Removed; the Server/ArrowFlight sibling at line 474
stays because it is inside `if (TARGET ch_contrib::arrow_flight)`.
The brand sweep had rewritten github.com/ClickHouse/ClickHouse issue links to
github.com/Datastore/Datastore — a dead host. Those point at upstream's own
tracker and are restored, alongside the contributors list, sql.clickhouse.com
and the ClickHouse Inc copyright: attribution is theirs, not ours to rename.
base/glibc-compatibility/memcpy/memcpy.h is deleted, not kept: upstream dropped
it for LLVM-libc's, and our only change to it was a comment rebrand.
- json::Parser bounds nesting at 256 levels: past the cap value() returns Null
(a parse-fail) instead of recursing, so a deeply-nested response degrades
rather than overflowing the stack. Every server response is parsed here; a
200k-nested body previously SIGSEGV'd the producer.
- Curl transport caps the response body at 8 MiB (a short write aborts the
transfer), so a hostile server cannot OOM the client. Redirects were already
disabled (FOLLOWLOCATION=0). Parity with the Go/Rust ports.
- writeReal emits CPython repr(float) notation: fixed for decimal exponent in
[-4, 16), scientific otherwise, so 1e15/0.0001 match json.dumps. The realistic
value domain is unchanged (ingest byte-identity to Python preserved).
- Docs state the accurate contract: cross-language identity is semantic (server
keys on project + id); this serializer additionally matches json.dumps bytes.
Verified: 200k-nest parse returns without crashing; SHA-256 self-test PASS;
ingest body still 1375 bytes / sha256 1914bc1e… (byte-identical to Python).
Mirror the Python hanzo-research SDK verb-for-verb so native producers (kernels,
benchmarks, the datastore/luxcpp stack) record R&D evidence on /v1/research.
Wire output is byte-identical to Python json.dumps (key order, ensure_ascii,
float repr). Verdict{Proven,Refuted,Inconclusive} is type-safe; kind is an open
string. Zero-config provenance (git/commits/host/libs) auto-captured; the
since-narrative sha is hex-validated before it reaches the shell. Dependency-free
core (ordered JSON + SHA-256 + base64, stdlib only); libcurl is the one swappable
transport behind -DHANZO_RESEARCH_CURL. Self-checking example + CTest.
2026-07 incident: `system.text_log` grew to ~115 GB / 2.5B rows with no
`TTL` and filled the datastore data disk, taking down api.hanzo.ai.
Bake a `config.d` overlay (`datastore-log-retention.xml`) into the server
image giving every internal `system.*_log` table a 3-day `TTL` and capping
`text_log` to `warning` level. Root element is `<clickhouse>` to match the
shipped main `config.xml`; `opentelemetry_span_log` (the one table the main
config gives an explicit `<engine>`, keyed on `finish_date`) carries its
`TTL` inside `<engine>` because a separate `<ttl>` beside an `<engine>` is
rejected at startup (Code 36 BAD_ARGUMENTS).
Validated by booting ghcr.io/hanzoai/datastore:26.2.3.2 with the overlay:
server starts clean and all 12 tables report the `TTL`.
Per CTO, kind spans visor's compute spectrum: machine, bot, cluster,
nodepool, container, function. Switch kind Enum8('machine'=1,'bot'=2) ->
kind LowCardinality(String) so a new kind never needs a schema migration;
only machine and bot emit today. ORDER BY (ts, org, app, project) and the
rest of the table unchanged.
Per CTO terminology, compute usage now carries a kind dimension
Enum8('machine'=1,'bot'=2): a BOT is a machine running the @hanzo/bot agent
(gw.hanzo.bot); a MACHINE is raw compute with no agent. Every bot is a
machine; not every machine is a bot. admin.hanzo.ai renders two lenses
(Bots / Machines) over the one table.
Renames hanzo.compute_events -> hanzo.compute_usage, adds the kind column,
and orders by (ts, org, app, project). Columns: org, app, project, kind,
event, machine_id, size, price_cents, ts. Emitted by hanzoai/visor over the
ClickHouse HTTP interface (INSERT ... FORMAT JSONEachRow).
Per-tenant compute fleet events (launched | running | destroyed) rolled
one-way out of hanzoai/visor alongside its commerce metering. Keys are IAM
slugs in the org > app > project hierarchy, so admin.hanzo.ai rolls fleet
count and spend up by org/app/project across every tenant.
MergeTree, monthly partitions, ORDER BY (org, app, project, event, ts);
ingested via the ClickHouse HTTP interface (INSERT ... FORMAT JSONEachRow).
Part of the tenant-data-hierarchy analytical rollup.
The coordination benchmark found tryCommit re-walked the entire never-pruned
pending map every call (O(N^2)) and the gate retained every finalized block's
votes (unbounded RSS). Now finalized entries are erased from pending and their
votes dropped from the gate (consensus2 v0.1.2 drop()) on promotion — scan walks
in-flight only, memory bounded. keeper_quasar_engine_test still green.
Co-authored-by: Hanzo Dev <[email protected]>
The earlier 'half memory' figure was real but measured at 10M rows (fixed-overhead
dominated); at 100M rows the data dominates and it's ~4%. State the scale — both
numbers are true at their scale. Corrects the imprecise 'different metric/config'
wording.
Co-authored-by: Hanzo Dev <[email protected]>
Records the measured comparison (ClickBench 43q/100M hits, spark, 4 runs): the
PGO+BOLT datastore.bolt 26.6.1.1 buys ~3.8% less peak RSS (13.96 vs 14.52 GB) and
is statistically TIED on total query time (16.82 vs 16.83s) vs the official
ClickHouse binary. This REFUTES the circulating 'half memory / half binary /
much faster' claims under this workload — they don't hold and shouldn't be quoted.
Caveats stated (4 runs, per-query noise +/-25%, one workload/machine). The fork's
value is architectural (S-Chain, consensus2, gRPC removed), not raw OLAP speed.
Co-authored-by: Hanzo Dev <[email protected]>
Drop itemOf; QuasarKeeperConsensus passes the full 32-byte block id to wave,
matching consensus2's hardened API (red M4 — no grindable 64-bit prefix that
could cross-contaminate one block's confidence with another's polls). The gate
was always immune (keys on the full id + re-verifies sigs); this hardens the
liveness layer too. keeper_quasar_engine_test still build-verified green on a
26.6.1.1 tree (5-validator BLS quorum cert → real KeeperStateMachine).
Co-authored-by: Hanzo Dev <[email protected]>
The NuRaft-replacement engine now drives genuine leaderless Lux consensus
(consensus2: photon/wave liveness + a >2/3-stake BLS quorum certificate) instead
of the forgeable toy libluxconsensus counter. submit -> record_poll/record_vote
-> tryCommit applies entries to the real KeeperStateMachine in consensus order,
only on a quorum cert (alpha distinct validators AND strictly >2/3 stake).
keeper_quasar_engine_test rewritten to a 5-validator set with REAL BLS12-381
signed votes against the real KeeperStateMachine<KeeperMemoryStorage>:
build-verified on a 26.6.1.1 tree (aarch64, clang-21) — 9 ZK coordination
entries reach a quorum cert and commit in order; a sub-quorum (3<alpha) request
does NOT commit. The old toy keeper_quasar_poc is retained, gated, as the
documented baseline only.
Opt-in (CONSENSUS2_DIR + BLST_LIB + LUXCPP_ROOT); reused C++ sources compiled in
for libc++ ABI match with SYSTEM includes; never part of a normal build. This is
the engine swap that lets NuRaft be deleted; the KeeperServer dispatcher cutover
+ contrib/NuRaft removal are the next stage.
Co-authored-by: Hanzo Dev <[email protected]>
* docs: cross-link Datastore and Hanzo S3 (S-Chain) in LLM.md
Datastore separates storage from compute: MergeTree parts are content-addressed
objects on Hanzo S3 / S-Chain (hanzoai/s3), demonstrated by a working PoC; the
transport is ZAP (gRPC and Arrow Flight removed). Datastore is the OLAP half of a
two-engine platform whose OLTP half is the SQLite/Base decentralized SQL engine;
both ride S-Chain storage and target Quasar coordination. Links the design paper
(hanzo-datastore in hanzoai/papers). Docs-only; no code or behavior change.
* docs: correct overclaims in S-Chain section (red review)
- ZAP framed as intended replacement (server stub landed), not the live
transport; live query path is native TCP (port 9000), matching the ports table.
- OLTP decentralized SQL engine marked designed/not-built, not existing.
- drop 'content-addressed' (SeaweedFS uses sequencer file-IDs, not CAS).
- fix garbled PoC sentence (system.parts reports disk seaweed).
---------
Co-authored-by: Hanzo Dev <[email protected]>
XRay defaulting ON put ~45MB of sled sections + per-function sleds in EVERY
binary, not just coverage builds, and broke aarch64 BOLT (composed PREL64
relocations). XRay is opt-in tracing instrumentation; a binary should not carry
it unless asked. WITH_COVERAGE_XRAY now enables ENABLE_XRAY itself (it requires
it), so coverage builds are unaffected. Verified: default configure now reports
"Not using LLVM XRay".
XRay (-fxray-instrument) defaulted ON, adding ~45MB of sled sections to every
binary plus per-function sleds, and its composed R_AARCH64_PREL64 relocations
made llvm-bolt fail on aarch64 ("expected relocatable expression"). Disabling
it for the optimized build: binary 945MB->648MB after BOLT, RSS ~50MB vs the
official PGO+LTO+BOLT ClickHouse's ~116MB, faster on filters/aggregations, and
BOLT now runs clean on aarch64 (needs ulimit -s 2097152 for veneers).
Set optimize_const_name_size explicitly so the over-threshold constant ALIAS
columns are guaranteed to be scalarized into __getScalar regardless of any future
change to the setting's default value, keeping the regression meaningful.
Co-authored-by: Yakov Olkhovskiy <[email protected]>
`buildQueryTreeForShard` runs `ReplaceLongConstWithScalarVisitor` after inlining
ALIAS columns, turning over-threshold constants into `__getScalar('<hash>')`
calls (controlled by `optimize_const_name_size`, default 256). The shard header
therefore names such a collapsed column after the scalar, not after the literal.
`buildShardCollapseFanOut` previously computed the inlined action name without
that rewrite, so the lookup missed for duplicate long-literal `ALIAS` columns and
the caller fell back to the positional conversion, again raising
`NUMBER_OF_COLUMNS_DOESNT_MATCH`.
Mirror the same constant-to-scalar rewrite (with the same `>= 0` guard) in
`actionNameAfterAliasInlining` so the computed name matches the shard header, and
add a focused regression with duplicate over-threshold constant `ALIAS` columns
to `04357_distributed_alias_same_expression`.
Co-authored-by: Yakov Olkhovskiy <[email protected]>
Extends the distributed ALIAS-column coverage with cases where the alias is
referenced in clauses other than the SELECT projection:
- WHERE + GROUP BY on alias columns sharing a sub-expression;
- JOIN ON referencing an alias column (GLOBAL JOIN);
- an aggregate over an alias column appearing only in ORDER BY (sort-key
aggregate state that travels in the merge but not in the output).
Each is checked against the equivalent local query, including over the
query-plan-serialization transport (serialize_query_plan = 1).
Co-authored-by: Yakov Olkhovskiy <[email protected]>
Broaden the duplicate-ALIAS column-count regression coverage to match the
distributed scenarios exercised by PR #106402:
- `Distributed`-over-`Distributed` (two transport hops), including over the
query-plan-serialization transport (`serialize_query_plan = 1`);
- `remote` over a `Distributed` table with parallel replicas (the
`remote` -> `Distributed` -> parallel-replicas second hop);
- single-hop `serialize_query_plan = 1` variant.
These all reproduce `NUMBER_OF_COLUMNS_DOESNT_MATCH` on master and are fixed by
the shard-collapse fan-out in this PR. Column-order scenarios remain covered by
the separate fix in PR #107675; the `GLOBAL IN`/`JOIN` alias-name collision is
tracked separately as a distinct issue.
Co-authored-by: Yakov Olkhovskiy <[email protected]>
When a Distributed query is executed up to `WithMergeableState`, the shard's
query tree has its `ALIAS` columns inlined into their defining expressions. If
several projection/sort/group items expand to the same expression, the shard's
`ActionsDAG` deduplicates them into a single output column, so the shard header
has fewer columns than the header the initiator expects (the initiator keeps the
columns distinct, because there the `ALIAS` columns are not inlined).
The reconciliation in `buildQueryPlanForTableExpression` only renamed columns by
position via `ActionsDAG::makeConvertingActions` with `MatchColumnsMode::Position`,
which requires an equal number of columns and therefore threw
`NUMBER_OF_COLUMNS_DOESNT_MATCH`, e.g.:
SELECT a1, a2 FROM dist_same_expr ORDER BY dt DESC;
where `a1` and `a2` are both `ALIAS toString(x)`.
This adds `buildShardCollapseFanOut`, which reconstructs the initiator's expected
header from the deduplicated shard header by fanning out each collapsed shard
column back to every expected column that maps onto it. The mapping is computed
exactly: for every expression node in the query tree we compute both its
identifier-based action name (matching the initiator header) and its action name
after inlining `ALIAS` columns (matching the shard header), and use this
translation to resolve which shard column feeds each expected column. The helper
returns `std::nullopt` (and the caller falls back to the previous behaviour) when
the situation is not a recognized projection collapse.
This replaces the reverted #106404 approach (which disambiguated expressions on
the shard with a wrapper function) with a boundary-only reconciliation that does
not change shard-side naming.
Closes: https://github.com/ClickHouse/ClickHouse/issues/106403
Co-authored-by: Yakov Olkhovskiy <[email protected]>
Address review feedback: the embedded `StorageFactory` documentation for
`StorageArrowFlight` and the `FunctionDocumentation` for `TableFunctionArrowFlight`
were still describing the engine/function as read-only. These registrations feed
`system.table_engines.description` and `system.table_functions.description` (and
`SHOW ENGINES`), so they now also document INSERT/write support, named collections,
schema inference, the default port, and the `arrow_flight_request_descriptor_type`
setting, matching the updated documentation pages.
Co-authored-by: Yakov Olkhovskiy <[email protected]>
The interface documentation belongs in the ClickHouse/clickhouse-docs
repository (tracked separately), not in the main repo. The file was
committed under tmp/ which is not gitignored here (only /ci/tmp is).
Also align the arrowFlight table function frontmatter description with
the updated body (reading and writing) and the table engine doc.
Co-authored-by: Yakov Olkhovskiy <[email protected]>
Address review comment
https://github.com/ClickHouse/ClickHouse/pull/106404#discussion_r3405148644
The previous detection in `buildQueryTreeDistributed` used
'`proj_node->getAlias()` matches an ALIAS column name in
`ColumnsDescription`' as a proxy for 'this projection item was inlined by
`ReplaseAliasColumnsVisitor`'. The proxy is unreliable:
* False negative: `SELECT a1, toString(x) FROM dist` (where `a1 ALIAS
toString(x)`). The directly written `toString(x)` has no alias, so
it is not recognized as the duplicate that needs disambiguation, and
`NUMBER_OF_COLUMNS_DOESNT_MATCH` still fires.
* False positive: `SELECT a1 AS a2, a2` (where `a2` is an ALIAS
column). The user-renamed `a1` carries alias "a2", which matches
the ALIAS column name, and the heuristic could wrap the wrong
projection item.
Replace the heuristic with a structural test that mirrors what the outer
planner does. Snapshot each projection item's tree hash BEFORE inlining;
after inlining, group items by their post-inlining hash and within each
group partition by original hash. The first partition keeps the natural
DAG name, each subsequent partition gets a fresh `__actionName` wrap, and
items in the same partition share the same wrap. This guarantees the
shard emits exactly as many distinct DAG output names as the outer
planner's `NameSet` deduplication collapses to, regardless of whether
the collision was introduced by inlining, user-written duplicates, or
any mix of the two.
Cover the previously untested scenarios in
`04305_03400_distributed_alias_same_expression.sql`: `SELECT a1,
toString(x)` (mixed), interleaved `SELECT a1, a2, a1`, paired `SELECT
a1, a1, a2, a2`, and pure user duplication `SELECT toString(x),
toString(x)`.
Related: https://github.com/ClickHouse/ClickHouse/issues/105689
Related: https://github.com/ClickHouse/ClickHouse/issues/81631
Co-authored-by: Yakov Olkhovskiy <[email protected]>
Make the previously hardcoded FormatSettings limits configurable via
fuzzer arguments for future tuning. Defaults remain at 100 (no behavior
change).
Co-authored-by: Yakov Olkhovskiy <[email protected]>
The NightlyFuzzers job has been failing every night for the past 2+ weeks
due to two fuzzers hitting libFuzzer's timeout and RSS limits with inputs
that are not genuine bugs.
`create_parser_fuzzer`: timeout (20-29s vs 20s limit) in 10/14 nights.
Caused by expensive `ParserCreateQuery` 5-way disjunction and large ASTs
being formatted via `formatWithSecretsOneLine`. Add `max_parser_depth=50`,
`max_ast_depth=200`, `max_ast_elements=10000`, and reduce
`max_parser_backtracks` from 10000 to 7000. These cause the parser/AST
checks to throw exceptions (caught by the fuzzer) instead of running for
20+ seconds.
`data_type_deserialization_fuzzer`: OOM (4100-4500 MB vs 4096 MB limit) in
14/14 nights. Root causes: nested `Array` types with `max_parser_depth=100`
allow multiplicative element explosion during deserialization; Object/JSON
`max_object_size` was left at default 100000; MemoryTracker at 1 GiB maps
to ~3-4 GiB RSS under ASan. Reduce `max_parser_depth` from 100 to 20, add
configurable `max_object_size` (set to 100) and `memory_limit` (set to
512 MiB) as new fuzzer arguments wired through C++ code.
Co-authored-by: Yakov Olkhovskiy <[email protected]>
The previous approach of modifying `calculateActionNodeNameWithCastIfNeeded`
was fundamentally flawed — it couldn't distinguish between _CAST nodes
created by the optimization pass vs those added during AST serialization,
causing regressions in multiple tests.
New approach: in `RewriteAggregateFunctionWithIfPass`, when the kept branch
is a ConstantNode and needs type promotion to Nullable, use
`convertToNullable` directly instead of `createCastFunction`. This avoids
the naming mismatch entirely because:
- On initiator: the constant is already Nullable, named via getResultType
- On remote: after AST round-trip and folding, same getResultType applies
- No FunctionNode vs folded-ConstantNode asymmetry exists
For non-constant arguments (column references, expressions), the original
`createCastFunction` path is preserved since those don't have the naming
divergence issue.
Co-authored-by: Yakov Olkhovskiy <[email protected]>
Replace `FieldToDataType` + try/catch with a simpler check: call
`requiresCastCall(type, type)` on the first arg's result type directly.
For literals parsed from AST, the natural field type always equals the
result type, so this is equivalent. The check reduces to testing if the
type is Nullable/Array/Tuple (which triggers cast wrapping).
Co-authored-by: Yakov Olkhovskiy <[email protected]>
- Add comment to empty catch block to satisfy style checker
- Force `optimize_rewrite_aggregate_function_with_if = 1` in test to
prevent the stateless runner from randomizing it to 0
Co-authored-by: Yakov Olkhovskiy <[email protected]>
The previous fix was too aggressive — it used the source _CAST's first
argument type for ALL `receivedFromInitiatorServer` constants, which broke
cases where the `_CAST` was added during AST serialization of a folded
constant (e.g., NULL with type Nullable(String)).
Now we only use the first argument's type when it's a "simple" constant
whose natural field type matches its stored type (i.e., it wouldn't need
its own _CAST wrapping). This correctly distinguishes:
- Case A: `_CAST` was a live FunctionNode on the initiator (optimization
pass) — first arg `0` has type UInt8, field type matches → use UInt8
- Case B: `_CAST` was added during serialization — first arg NULL has
type Nullable(Nothing), field type is Nullable → needs cast → fall back
to `getResultType`
Co-authored-by: Yakov Olkhovskiy <[email protected]>
The bugfix validation requires the test to fail on master. Using
`remote('127.0.0.1', ...)` (single host) doesn't trigger the distributed
execution path — the query runs locally without header comparison between
initiator and remote. Change to `remote('127.0.0.{1,2}', ...)` which
forces actual distributed execution and reproduces the column name mismatch.
Co-authored-by: Yakov Olkhovskiy <[email protected]>
When `RewriteAggregateFunctionWithIfPass` rewrites `sum(if(cond, val, null))`
to `sumIf(_CAST(val, 'Nullable(T)'), cond)`, the `_CAST` stays as a
FunctionNode on the initiator (no resolve pass runs after optimization).
On the remote (secondary) server, the same `_CAST` gets constant-folded
during re-resolution, producing a ConstantNode with result type
`Nullable(T)`.
The column naming function `calculateActionNodeNameWithCastIfNeeded` used
`getResultType()` for the inner type annotation, which gives `T` on the
initiator (from the pre-cast constant) but `Nullable(T)` on the remote
(from the folded result). This caused a column name mismatch:
- Initiator: `_CAST(0_UInt8, 'Nullable(UInt8)'_String)`
- Remote: `_CAST(0_Nullable(UInt8), 'Nullable(UInt8)'_String)`
Fix: when the constant was folded from `_CAST` (`receivedFromInitiatorServer`
is true), use the source `_CAST` function's first argument type as the
inner type annotation. This mirrors how the initiator names it.
Closes: https://github.com/ClickHouse/ClickHouse/issues/82941
Co-authored-by: Yakov Olkhovskiy <[email protected]>
Two fixes:
1. Repeated same-alias references (`SELECT a1, a2, a2`): only wrap the
first occurrence of each distinct ALIAS column name. Repeated
references share one identifier in the original tree — wrapping both
produces more unique columns than the initiator expects.
2. Name collision safety: use `__distributed_alias_` prefix for generated
action names to avoid collisions with real columns (e.g. a table
having a column literally named `a2_1`).
Co-authored-by: Yakov Olkhovskiy <[email protected]>
Each duplicate ALIAS projection position needs its own unique action
name — even repeated references to the same column. The position-based
suffix (`a2_1`, `a2_2`) guarantees each wrapped item is distinct.
The `getSampleBlock` (modified tree) and `expected_header` (original
tree) both produce the same column count because the original tree
also preserves all projection positions (with Position-based matching,
column count must agree — names are irrelevant).
`SELECT a1, a2, a2` is a valid query that exercises this: positions 1
and 2 both collide with position 0 and both get wrapped uniquely.
Co-authored-by: Yakov Olkhovskiy <[email protected]>
The repeated-same-ALIAS-column case (SELECT a1, a2, a2) requires deeper
planner changes to handle correctly — the initiator and shard disagree
on how many distinct columns to produce because the planner merges
identical logical columns. This is a separate issue from the original
bug (two DIFFERENT ALIAS columns with the same expression body).
The bot's concern about nested ALIAS usage like
`SELECT concat(a1, ''), concat(a2, '')` is also noted as a known
limitation — it would require tracking inlined positions through the
visitor, which is out of scope for this fix.
Co-authored-by: Yakov Olkhovskiy <[email protected]>
Fix two gaps:
1. Renamed projections (`SELECT a1 AS first, a2 AS second`): use the
node alias (set by `ReplaseAliasColumnsVisitor` to the original ALIAS
column name) instead of projection display name for the `getDefault`
lookup. This correctly identifies ALIAS columns even after user renames.
2. Repeated projections (`SELECT a1, a2, a2`): generate the action name
as `column_name + "_" + position` (e.g. `a2_1`, `a2_2`) to
guarantee uniqueness per projection position, preventing collisions
when the same ALIAS column appears multiple times.
Add regression tests for both cases.
Co-authored-by: Yakov Olkhovskiy <[email protected]>
Fix three issues:
1. Empty alias: use `projection_columns[i].name` from `QueryNode::getProjectionColumns()`
instead of `getAlias()` which can be empty after analyzer resolution.
2. MULTIPLE_EXPRESSIONS_FOR_ALIAS on shard: call `removeAlias()` on the
inner expression before wrapping in `__actionName`, so only the wrapper
carries the alias in the serialized AST.
3. False positives: only wrap projection items that correspond to ALIAS
columns in the storage metadata (`ColumnDefaultKind::Alias`). This
prevents wrapping unrelated duplicate expressions like `SELECT 1 x, x y`
or `SELECT toString(x), toString(x)` that happen to share a tree hash.
Update tests for relaxed `__actionName` first-argument type:
- 03035: `__actionName(materialize('aaa'), 'aaa')` now succeeds
- 04201: rewritten to test current semantics (any first arg type,
String-only second arg)
Co-authored-by: Yakov Olkhovskiy <[email protected]>
Relax the type restriction on __actionName's first argument: the
original all-arguments-must-be-String guard (added in #62287 as a
fuzzer crash workaround) was overly broad — the actual bug was empty
action names from the second argument, not a type mismatch on the
first. Now only the second argument (the desired name) is required to
be String; the first argument (the wrapped expression) can be any type.
In buildQueryTreeDistributed, after ReplaseAliasColumnsVisitor inlines
ALIAS columns, scan the projection for structurally identical
expressions (same tree hash ignoring aliases). Wrap subsequent
duplicates in __actionName(expr, 'column_alias') which forces the
planner to use the column alias as the action-node name — guaranteed
unique since column aliases within a table are unique by definition.
No nesting, no collisions, works on both initiator and shard.
Also fix typo: "nad" → "and" in error messages.
Closes: https://github.com/ClickHouse/ClickHouse/issues/106403
Co-authored-by: Yakov Olkhovskiy <[email protected]>
Revert the `identity()` wrapper approach in `StorageDistributed.cpp`
(it cannot handle 3+ duplicates or existing `identity()` projections).
Instead, fix in `PlannerActionsVisitorImpl::visit` with a precise
condition: add a DAG ALIAS node only when a later list item resolves to
the same DAG node as an earlier item AND the two items carry different
non-empty query-tree aliases. This targets the distributed ALIAS column
case (where `ReplaseAliasColumnsVisitor` inlines identical expressions
under different aliases like "a1" and "a2") without affecting:
- `arrayJoin` duplicates (second item has no alias → skipped)
- `SELECT x, x` (column refs have no alias on the tree node → skipped)
- `SELECT * REPLACE(a AS b)` producing same-alias duplicates (same
alias → condition false → skipped)
- JOIN projections with duplicate columns (same alias → skipped)
The generated alias name incorporates the query-tree alias for
readability (e.g. `toString(__table1.x)_a2`) and loops with a suffix
until fresh in the scope to avoid collisions.
Co-authored-by: Yakov Olkhovskiy <[email protected]>
Use a while loop: keep wrapping in identity() until the resulting tree
hash is unique among all previously-seen projection hashes. This
handles:
- 3+ ALIAS columns expanding to the same expression (each gets an
additional layer of identity() nesting)
- A projection that already contains identity(expr) alongside ALIAS
columns expanding to the same expr
Co-authored-by: Yakov Olkhovskiy <[email protected]>
The previous approach (adding alias nodes in `PlannerActionsVisitorImpl::visit`
for all expression lists) was too broad and broke unrelated tests:
- `02369_analyzer_array_join_function`: interfered with arrayJoin semantics
- `03631_select_replace_comprehensive`: broke PREWHERE with duplicate columns
- `04145_multiple_expressions_for_alias_parallel_replicas`: changed projection
handling for parallel replicas
- `03777_join_runtime_filter_duplicate_columns`: changed EXPLAIN output
New approach: apply the fix only in `buildQueryTreeDistributed` after
`ReplaseAliasColumnsVisitor` inlines ALIAS columns. Scan the projection
for structurally identical expressions (same tree hash ignoring aliases)
and wrap subsequent duplicates in `identity()` — a no-op function that
creates a distinct action-node name without affecting query results.
This is targeted to the distributed query path and does not affect local
queries or other expression lists like ORDER BY, GROUP BY, etc.
Co-authored-by: Yakov Olkhovskiy <[email protected]>
Fix the generated unique name to be truly unique in the current scope:
loop with incrementing suffix until `containsNode` confirms the name
is fresh. This prevents silently resolving to an unrelated column if
the naive suffix name already exists (e.g. a column named `x_1`).
Also fix test references:
- 04305: correct expected output (4 lines, not 6)
- 03777: update EXPLAIN output to reflect the additional alias nodes
now visible in intermediate plan headers
Co-authored-by: Yakov Olkhovskiy <[email protected]>
When a Distributed table has two ALIAS columns expanding to the same
expression (e.g. both are `toString(x)`), querying them with ORDER BY
fails because the planner's ActionsDAG deduplicates the projection
items by their action node name, shrinking the output column count.
The initiator then throws:
Code: 20. Number of columns doesn't match (source: 2 and result: 3)
Root cause: `ReplaseAliasColumnsVisitor` inlines both alias
expressions into the shard query tree. Since the expressions are
structurally identical, `calculateActionNodeName` produces the same
DAG node name for both. `addFunctionIfNecessary` returns the same
node, and downstream `removeUnusedActions` / `finalizeInputAndOutputColumns`
deduplicates outputs by name -- producing fewer columns than expected.
Fix: In `PlannerActionsVisitorImpl::visit`, when processing an
expression list (projection), detect when multiple items resolve to the
same DAG node. For duplicates, add an ALIAS node with a unique
suffix so that each projection item has a distinct output name in the
DAG. This prevents deduplication while preserving the correct
projection semantics (the final rename step maps internal names to
display names anyway).
Closes: https://github.com/ClickHouse/ClickHouse/issues/106403
Co-authored-by: Yakov Olkhovskiy <[email protected]>