From 3b4ce2f608019b4aa687db8d54a8e82e388e2cce Mon Sep 17 00:00:00 2001 From: Alexey Milovidov Date: Wed, 23 Sep 2026 00:55:57 +0000 Subject: [PATCH] fix(clickhouse): tolerate the current no-digits parse message; skip tables with an unfinished mutation Two false-positive classes in the `NightlySQLancer` job on ClickHouse master: 1. `Cannot parse number without any digits` (`CANNOT_PARSE_NUMBER`) is what the integer reader throws for a `+` with no digit after it since https://github.com/ClickHouse/ClickHouse/pull/113910 - e.g. `-1 = '+['`, `'+kui' < 917426774`. The expected-expression list already meant to tolerate this shape but carried an old wording that is no longer in the ClickHouse tree, so it never matched. This hit `CODDTest`, `TLPHaving` and `Cast` on 2026-09-19 and 2026-09-22. 2. An `ALTER ... MODIFY COLUMN` whose conversion cannot succeed on the stored values (`String` -> `UInt64` over `''`) commits the new type to the metadata, and the mutation keeps failing. Every read of the column then converts the old parts on the fly and throws `ATTEMPT_TO_READ_AFTER_EOF`, `CANNOT_PARSE_NUMBER` or `CANNOT_PARSE_TEXT` "while reading from part". `MaterializedCte` picked such a column because it now has an exact integer type (2026-09-04, 2026-09-22 twice). A table with an unfinished mutation in `system.mutations` is now not stable for repeated reads, next to the `FINAL`-engine exclusion, so all the schema-picking oracles skip it, rather than whitelisting parse errors that the `Cast` oracle needs to see. Reports: https://s3.amazonaws.com/clickhouse-test-reports/praktika.html?REF=master&sha=de0be4574c3fbe20c7c28c8633416906392e3dd7&name_0=NightlySQLancer&name_1=SQLancer%20%28arm_release%29 https://s3.amazonaws.com/clickhouse-test-reports/praktika.html?REF=master&sha=de0be4574c3fbe20c7c28c8633416906392e3dd7&name_0=NightlySQLancer&name_1=SQLancer%20%28arm_asan_ubsan%29 https://s3.amazonaws.com/clickhouse-test-reports/praktika.html?REF=master&sha=112492e63e0866e7f63a4f777358702cbd2b5844&name_0=NightlySQLancer&name_1=SQLancer%20%28arm_asan_ubsan%29 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/sqlancer/clickhouse/ClickHouseErrors.java | 1 + src/sqlancer/clickhouse/ClickHouseSchema.java | 38 ++++++++++++++++++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/sqlancer/clickhouse/ClickHouseErrors.java b/src/sqlancer/clickhouse/ClickHouseErrors.java index a26f0cb39..7e41ca955 100644 --- a/src/sqlancer/clickhouse/ClickHouseErrors.java +++ b/src/sqlancer/clickhouse/ClickHouseErrors.java @@ -20,6 +20,7 @@ public static List getExpectedExpressionErrors() { "Unknown element '", "Cannot parse infinity.", "Cannot parse number with a sign character but without any numeric character", + "Cannot parse number without any digits", "Cannot parse number with multiple sign (+/-) characters or intermediate sign character", "Cannot parse string", "Cannot read floating point value", "Cyclic aliases: default expression and column type are incompatible", "Directory for table data", diff --git a/src/sqlancer/clickhouse/ClickHouseSchema.java b/src/sqlancer/clickhouse/ClickHouseSchema.java index 06e3ec9e5..2dc541e27 100644 --- a/src/sqlancer/clickhouse/ClickHouseSchema.java +++ b/src/sqlancer/clickhouse/ClickHouseSchema.java @@ -435,6 +435,7 @@ public static class ClickHouseTable private final String engine; private final String samplingKey; + private final boolean hasUnfinishedMutations; public ClickHouseTable(String tableName, List columns, List indexes, boolean isView) { @@ -448,9 +449,15 @@ public ClickHouseTable(String tableName, List columns, List columns, List indexes, boolean isView, String engine, String samplingKey) { + this(tableName, columns, indexes, isView, engine, samplingKey, false); + } + + public ClickHouseTable(String tableName, List columns, List indexes, + boolean isView, String engine, String samplingKey, boolean hasUnfinishedMutations) { super(tableName, columns, indexes, isView); this.engine = engine == null ? "" : engine; this.samplingKey = samplingKey == null ? "" : samplingKey; + this.hasUnfinishedMutations = hasUnfinishedMutations; } public String getEngine() { @@ -471,8 +478,19 @@ public boolean supportsFinal() { || engine.equals("VersionedCollapsingMergeTree"); } + public boolean hasUnfinishedMutations() { + return hasUnfinishedMutations; + } + + /* + * A table with an unfinished mutation is not stable either. The typical one is an `ALTER ... MODIFY COLUMN` + * whose conversion cannot succeed on the stored values (`String` -> `UInt64` over `''`): the new type is + * committed to the metadata before the mutation runs, the mutation keeps failing, and every read of the column + * converts the old parts on the fly and throws `CANNOT_PARSE_NUMBER`, `ATTEMPT_TO_READ_AFTER_EOF` or + * `CANNOT_PARSE_TEXT` "while reading from part". That is the server working as designed, not a finding. + */ public boolean isStableForRepeatedReads() { - return !supportsFinal(); + return !supportsFinal() && !hasUnfinishedMutations; } } @@ -480,13 +498,14 @@ public static ClickHouseSchema fromConnection(SQLConnection con, String database List databaseTables = new ArrayList<>(); List tableNames = getTableNames(con); java.util.Map metaByName = getTableMeta(con, databaseName); + java.util.Set tablesWithUnfinishedMutations = getTablesWithUnfinishedMutations(con, databaseName); for (String tableName : tableNames) { List databaseColumns = getTableColumns(con, tableName); List indexes = Collections.emptyList(); boolean isView = matchesViewName(tableName); TableMeta meta = metaByName.getOrDefault(tableName, TableMeta.EMPTY); ClickHouseTable t = new ClickHouseTable(tableName, databaseColumns, indexes, isView, meta.engine, - meta.samplingKey); + meta.samplingKey, tablesWithUnfinishedMutations.contains(tableName)); for (ClickHouseColumn c : databaseColumns) { c.setTable(t); } @@ -522,6 +541,21 @@ private static java.util.Map getTableMeta(SQLConnection con, return meta; } + private static java.util.Set getTablesWithUnfinishedMutations(SQLConnection con, String databaseName) + throws SQLException { + java.util.Set tables = new java.util.HashSet<>(); + try (Statement s = con.createStatement()) { + String q = "SELECT DISTINCT table FROM system.mutations WHERE database = '" + + databaseName.replace("'", "''") + "' AND NOT is_done"; + try (ResultSet rs = s.executeQuery(q)) { + while (rs.next()) { + tables.add(rs.getString(1)); + } + } + } + return tables; + } + private static List getTableNames(SQLConnection con) throws SQLException { List tableNames = new ArrayList<>(); try (Statement s = con.createStatement()) {