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()) {