Files
datastore/ci/docker/sqlancer-test/swap-clickhouse-jdbc.py
Nikita Fomichev b50cff0acc SQLancer++: fix false-positive assertions, result reporting, and attach reproducer logs
The NightlySQLancer / SQLancerPP job had three independent problems, surfaced
while investigating run
https://github.com/ClickHouse/ClickHouse/actions/runs/28698784006 (both jobs
were green on GitHub while the praktika report was actually FAIL/ERROR).

1. SQLancer++ WHERE and QUERY_PARTITIONING oracles always failed with
   `java.lang.AssertionError: null`. When the WHERE-stripped baseline query hits
   an expected/whitelisted ClickHouse error, `executeAndGet` returns null and
   `ComparatorHelper` throws `IgnoreMeException` (a "skip this iteration"
   signal). `GeneralQueryPartitioningWhere.check` catches it and, for simple
   queries, re-throws it as an `AssertionError` - a guaranteed false positive
   against ClickHouse (QUERY_PARTITIONING is a composite that contains this
   oracle). Patch the oracle in `swap-clickhouse-jdbc.py` (the existing build-time
   patch mechanism, alongside the NoREC fix) to let `IgnoreMeException`
   propagate; genuine unexpected errors are unaffected.

2. `sqlancer_pp_job.sh` wrote its result to `result.json` instead of
   `result_<normalized_job_name>.json`, so praktika never read it and reported
   `ERROR "Job killed or terminated, no Result provided"`, discarding every
   oracle status and attached log. It also emitted `null` start_time/duration,
   which the CIDB inserter rejects. Both fixed to mirror `sqlancer_job.sh`.

3. On failure, both jobs now archive SQLancer's per-database reproducer logs
   (`logs/<dbms>/databaseN-cur.log`, written with --log-each-select) as a
   downloadable artifact, so the exact CREATE/INSERT/.../SELECT sequence that
   triggered a bug is one click away instead of requiring reconstruction from
   the interleaved multi-threaded stdout.
2026-07-06 09:31:28 +02:00

175 lines
7.9 KiB
Python

#!/usr/bin/env python3
"""Patch the SQLancer++ source tree for use against ClickHouse:
1. Replace the legacy `ru.yandex.clickhouse:clickhouse-jdbc:0.3.2` Maven
dependency in `pom.xml` with the `com.clickhouse:clickhouse-jdbc` shaded-all
artifact, pinned to the same version as the SQLancer job's
`com.clickhouse:client-v2` (0.9.8). At 0.9.x the driver's default
implementation (`com.clickhouse.jdbc.ClickHouseDriver`) is the client-v2
backed one - V1 is only used when `clickhouse.jdbc.v1=true` - so SQLancer++
reaches the server over the same client-v2 transport as the SQLancer job.
The legacy driver's HTTP response parser can't handle current ClickHouse
server output.
2. Extend the `CLICKHOUSE.url` template in `dbconfigs/jdbc.properties` to carry
`user` and `password` query parameters. SQLancer++'s generic provider only
passes the URL string (no JDBC Properties) into `DriverManager.getConnection`,
so the new driver - which requires credentials - needs them embedded here.
3. Replace the `IS TRUE` postfix in `GeneralNoRECOracle.java` with `!= 0`.
NoREC's row-counter must agree with `WHERE <expr>`, which ClickHouse
evaluates by truthiness (any non-zero numeric is included, `NULL` is
excluded). `<expr> != 0` reproduces that exactly: non-zero -> 1,
zero -> 0, `NULL` -> `NULL` (counted as not matching), matching `WHERE`.
Neither `<expr> IS TRUE` nor `<expr> = TRUE` works here: ClickHouse parses
`IS TRUE` as `<expr> <=> true`, i.e. strict equality with `1`, so a truthy
but non-`1` value such as `5` or `LN(x)` would compare unequal and produce
a false-positive oracle mismatch. (`IS TRUE` itself is only accepted since
https://github.com/ClickHouse/ClickHouse/pull/99997; before that the
unpatched oracle was a guaranteed `SYNTAX_ERROR`.)
4. Stop `GeneralQueryPartitioningWhere.check` from turning an `IgnoreMeException`
into an `AssertionError`. When the WHERE-stripped baseline query hits an
expected (whitelisted) error, `ComparatorHelper.getResultSetFirstColumnAsString`
sees a `null` result set and throws `IgnoreMeException` - a "skip this
iteration" control-flow signal, not a bug. The upstream oracle catches it in
`catch (Exception e)` and, for simple queries (no joins, <= 2 tables),
re-throws it as `AssertionError: null` ("You probably triggered an error in
the DBMS ..."). Against ClickHouse that fires constantly, so the WHERE oracle
(and the QUERY_PARTITIONING composite, which contains it) is a guaranteed
false-positive failure. Let the `IgnoreMeException` propagate instead; genuine
unexpected errors (a re-thrown `SQLException`, or an `AssertionError` with a
message) are unaffected and still reported.
"""
import argparse
import pathlib
import re
import sys
# Pin the JDBC driver to the same version the SQLancer job uses for
# `com.clickhouse:client-v2` (see ci/docker/sqlancer-test), so both jobs talk to
# the server over the same client-v2 transport and the build stays reproducible.
# clickhouse-jdbc >= 0.8 defaults to the client-v2-backed implementation.
DEFAULT_JDBC_VERSION = "0.9.8"
LEGACY_DEP = re.compile(
r"\s*<dependency>\s*<groupId>ru\.yandex\.clickhouse</groupId>"
r"\s*<artifactId>clickhouse-jdbc</artifactId>"
r"\s*<version>[^<]+</version>\s*</dependency>",
re.DOTALL,
)
CLICKHOUSE_URL = re.compile(
r"^CLICKHOUSE\.url=.*$",
re.MULTILINE,
)
# clickhouse-jdbc 0.9.8+ rejects URLs with empty `user=` or `password=` query
# parameters, so we must populate both. Also note that SQLancer++'s
# `GeneralJdbcConfigLoader.getUser/getPassword` only honour `--username` /
# `--password` when they differ from the built-in defaults (`sqlancer`), so
# even with explicit CLI flags those defaults fall through to the properties
# file - meaning the entries below need real values.
NEW_CLICKHOUSE_LINES = (
"CLICKHOUSE.url=jdbc:clickhouse://{host}:{port}/{database}"
"?user={user}&password={password}\n"
"CLICKHOUSE.user=sqlancer\n"
"CLICKHOUSE.password=sqlancer"
)
def patch_pom(repo: pathlib.Path, version: str) -> None:
pom = repo / "pom.xml"
text = pom.read_text()
replacement = (
"\n <dependency>\n"
" <groupId>com.clickhouse</groupId>\n"
" <artifactId>clickhouse-jdbc</artifactId>\n"
f" <version>{version}</version>\n"
" <classifier>all</classifier>\n"
" </dependency>"
)
new, count = LEGACY_DEP.subn(replacement, text, count=1)
if count != 1:
sys.exit(f"failed to locate legacy clickhouse-jdbc dependency in {pom}")
pom.write_text(new)
def patch_jdbc_properties(repo: pathlib.Path) -> None:
props = repo / "dbconfigs" / "jdbc.properties"
text = props.read_text()
new, count = CLICKHOUSE_URL.subn(NEW_CLICKHOUSE_LINES, text, count=1)
if count != 1:
sys.exit(f"failed to locate CLICKHOUSE.url in {props}")
props.write_text(new)
def patch_norec_oracle(repo: pathlib.Path) -> None:
src = repo / "src" / "sqlancer" / "general" / "oracle" / "GeneralNoRECOracle.java"
text = src.read_text()
old_postfix = '" IS TRUE "'
# ClickHouse's `WHERE <expr>` accepts any non-zero numeric as truthy and
# rejects NULL. `<expr> != 0` reproduces that exact semantics for the
# row-counter; a naive `= TRUE` would be strict equality with 1 and
# produce false-positive oracle mismatches for things like LN()/SIN().
new_postfix = '" != 0 "'
if old_postfix not in text:
sys.exit(f"failed to locate {old_postfix} in {src}")
if text.count(old_postfix) != 1:
sys.exit(f"expected exactly one {old_postfix} occurrence in {src}")
src.write_text(text.replace(old_postfix, new_postfix))
def patch_where_oracle(repo: pathlib.Path) -> None:
src = (
repo / "src" / "sqlancer" / "general" / "oracle"
/ "GeneralQueryPartitioningWhere.java"
)
text = src.read_text()
# The baseline (WHERE-stripped) query's catch block escalates any exception
# to an AssertionError for simple queries. An IgnoreMeException here is the
# benign "expected error, skip this iteration" signal and must propagate
# instead - otherwise every whitelisted error on a simple select becomes a
# false-positive `AssertionError: null`.
anchor = (
" } catch (Exception e) {\n"
" if (select.getJoinList().size() == 0 && select.getFromList().size() <= 2) {\n"
)
replacement = (
" } catch (Exception e) {\n"
" // ClickHouse patch: an IgnoreMeException means the baseline query hit an\n"
" // expected/whitelisted error and was skipped (null result set); it is a\n"
" // skip-this-iteration signal, not a DBMS bug, so propagate it instead of\n"
" // escalating it into a false-positive AssertionError below.\n"
" if (e instanceof sqlancer.IgnoreMeException) {\n"
" throw e;\n"
" }\n"
" if (select.getJoinList().size() == 0 && select.getFromList().size() <= 2) {\n"
)
if anchor not in text:
sys.exit(f"failed to locate the WHERE-oracle catch block in {src}")
if text.count(anchor) != 1:
sys.exit(f"expected exactly one WHERE-oracle catch block in {src}")
src.write_text(text.replace(anchor, replacement))
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("repo", help="Path to the SQLancer++ checkout")
parser.add_argument(
"--jdbc-version",
default=None,
help=f"clickhouse-jdbc version (default: {DEFAULT_JDBC_VERSION}, matching the SQLancer job's client-v2)",
)
args = parser.parse_args()
version = args.jdbc_version or DEFAULT_JDBC_VERSION
print(f"swap-clickhouse-jdbc: using com.clickhouse:clickhouse-jdbc:{version}")
repo = pathlib.Path(args.repo)
patch_pom(repo, version)
patch_jdbc_properties(repo)
patch_norec_oracle(repo)
patch_where_oracle(repo)
if __name__ == "__main__":
main()