Files
zeekayandHanzo Dev ee284e8075 finish the rebrand where a person actually reads it
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]>
2026-08-14 14:07:52 -07:00

205 lines
7.5 KiB
Python

#!/usr/bin/env python3
import argparse
import io
import subprocess
import sys
import time
import urllib.request
from pathlib import Path
class DiffToSymbols:
def __init__(self, clickhouse_path: str, pr_number: int):
if Path(clickhouse_path).is_dir():
self.clickhouse_path = clickhouse_path + "/clickhouse"
else:
self.clickhouse_path = clickhouse_path
# TODO: add support for non-API mode (from git)
self.pr_number = pr_number
assert self.pr_number > 0, "Works only for PRs"
assert Path(
self.clickhouse_path
).is_file(), f"clickhouse binary not found at {self.clickhouse_path}"
@staticmethod
def fetch(url: str, retries: int = 5, delay: float = 5.0) -> bytes:
for attempt in range(retries):
try:
with urllib.request.urlopen(url) as resp:
return resp.read()
except urllib.error.HTTPError as e:
if e.code >= 500 and attempt < retries - 1:
print(
f"HTTP {e.code} fetching {url}, retrying in {delay}s (attempt {attempt + 1}/{retries})",
file=sys.stderr,
)
time.sleep(delay)
delay *= 2
else:
raise
@staticmethod
def parse_diff_to_line_numbers(diff_bytes: bytes) -> list:
"""
Return a list of (filename, line_number) tuples for added or removed lines
in C/C++ source files found in the diff.
"""
from unidiff import PatchSet
patch = PatchSet(diff_bytes.decode("utf-8", errors="ignore"))
result = []
exts = (".cpp", ".cc", ".cxx", ".c", ".hpp", ".hh", ".hxx", ".h", ".ipp")
for f in patch:
if not f.path.endswith(exts):
continue
for hunk in f:
for line in hunk:
if line.is_added:
# Added lines use target line number (new file)
result.append((f.path, line.target_line_no))
elif line.is_removed:
# Removed lines use source line number (old file)
result.append((f.path, line.source_line_no))
return result
def run_query(self, line_numbers: list) -> dict:
"""
Execute a Datastore query with the provided (filename, line_number) tuples.
Args:
line_numbers: List of tuples (filename, line_number)
Returns:
Dictionary mapping (filename, line_number) -> (address, linkage_name, symbol)
Example: {('src/foo.cpp', 42): ('0x12345', '_Z...symbol', 'myFunction()')}
"""
# Convert list of tuples to CSV format for Datastore stdin
out = io.StringIO()
out.write("filename,line\n")
for filename, line_no in line_numbers:
out.write("{},{}\n".format(filename, line_no))
csv_payload = out.getvalue()
query = (
"""
SELECT
diff.filename,
diff.line,
binary.address,
binary.linkage_name,
if(empty(binary.linkage_name),
demangle(addressToSymbol(binary.address)),
demangle(binary.linkage_name)) AS symbol
FROM file('stdin', 'CSVWithNames', 'filename String, line UInt32') AS diff
ASOF LEFT JOIN
(
SELECT
decl_file,
decl_line,
linkage_name,
ranges[1].1 AS address
FROM file('{ch_path}', 'DWARF')
WHERE (tag = 'subprogram')
AND (notEmpty(linkage_name) OR address != 0)
AND notEmpty(decl_file)
) AS binary
ON basename(diff.filename) = basename(binary.decl_file)
AND diff.line >= binary.decl_line
FORMAT TSV
""".format(
ch_path=self.clickhouse_path
)
).strip()
import time
n_lines = csv_payload.count("\n") - 1 # minus header
print(f"[find_symbols] DWARF query: {n_lines} changed lines, binary={self.clickhouse_path}")
t0 = time.monotonic()
proc = subprocess.run(
[self.clickhouse_path, "local", "--query", query],
input=csv_payload,
text=True,
capture_output=True,
check=False,
)
print(f"[find_symbols] DWARF query: {time.monotonic()-t0:.2f}s")
if proc.returncode != 0:
print(proc.stderr, file=sys.stderr)
raise SystemExit(proc.returncode)
# Parse TSV output into dictionary
result = {}
for line in proc.stdout.strip().split("\n"):
if not line:
continue
parts = line.split("\t")
if len(parts) >= 5:
filename, line_no, address, linkage_name, symbol = (
parts[0],
parts[1],
parts[2],
parts[3],
parts[4],
)
result[(filename, int(line_no))] = (address, linkage_name, symbol)
elif len(parts) >= 2:
# Handle case where no match was found (LEFT JOIN)
filename, line_no = parts[0], parts[1]
result[(filename, int(line_no))] = (None, None, None)
return result
def get_file_with_line_numbers(self):
import time
diff_url = f"https://patch-diff.githubusercontent.com/raw/ClickHouse/ClickHouse/pull/{self.pr_number}.diff"
t0 = time.monotonic()
diff_bytes = self.fetch(diff_url)
print(f"[find_symbols] fetch diff: {time.monotonic()-t0:.2f}s ({len(diff_bytes)} bytes)")
t0 = time.monotonic()
result = self.parse_diff_to_line_numbers(diff_bytes)
print(f"[find_symbols] parse diff: {time.monotonic()-t0:.2f}s -> {len(result)} changed lines")
return result
def get_map_line_to_symbol(self):
"""
Get symbols mapping for changed lines.
Returns:
Dictionary mapping (filename, line_number) -> (address, linkage_name, symbol)
Empty dict if there are no changes in source code
"""
import time
t_total = time.monotonic()
file_with_line_numbers = self.get_file_with_line_numbers()
if not file_with_line_numbers:
return {}
result = self.run_query(file_with_line_numbers)
resolved = sum(1 for a, l, s in result.values() if s)
print(
f"[find_symbols] total: {time.monotonic()-t_total:.2f}s, "
f"resolved={resolved}/{len(result)} lines"
)
return result
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="List changed symbols for a PR by parsing the diff and querying Datastore."
)
parser.add_argument("pr", help="Pull request number")
parser.add_argument(
"clickhouse_path",
help='Path to the clickhouse binary (executed as "clickhouse local")',
)
args = parser.parse_args()
dts = DiffToSymbols(args.clickhouse_path, int(args.pr))
output = dts.get_map_line_to_symbol()
symbols = set()
print("\n")
for (file, line), (address, linkage_name, symbol) in output.items():
if not address and not linkage_name:
print(f"{file}:{line} ->\n NOT RESOLVED\n")
if symbol not in symbols:
symbols.add(symbol)
print(f"{file}:{line} ->\n {symbol}\n")