Description
SimpleAggregateFunction(func, T) is a transparent wrapper on the read path — client-v2's BinaryStreamReader delegates straight to the nested column (BinaryStreamReader.java:265), and getValueDataType() exists precisely to expose the underlying type. But ResultSetMetaDataImpl.getPrecision(int) / getScale(int) read the outer ClickHouseColumn:
// jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/ResultSetMetaDataImpl.java:241
public int getPrecision(int column) throws SQLException {
return getColumn(column).getPrecision();
}
public int getScale(int column) throws SQLException {
return getColumn(column).getScale();
}
For a SimpleAggregateFunction(...) column, ClickHouseColumn parsing (clickhouse-data/.../ClickHouseColumn.java:443-449) sets dataType = SimpleAggregateFunction and stores the real type in nested.get(0). Precision/scale are then filled from the wrapper's own data type (ClickHouseColumn.java:147, column.precision = column.dataType.getMaxPrecision()), and the parameter-driven branches for Decimal / DateTime64 never run. So the declared precision and scale of the wrapped type are lost and both accessors return 0.
Measured on a live server (probe over the whole matrix; getColumnTypeName | getPrecision | getScale):
DateTime('Europe/Amsterdam') precision=29 scale=0
SimpleAggregateFunction(any, DateTime('Europe/Amsterdam')) precision=0 scale=0
LowCardinality(DateTime('Europe/Amsterdam')) precision=29 scale=0
DateTime64(3, 'Europe/Amsterdam') precision=29 scale=3
SimpleAggregateFunction(any, DateTime64(3, 'Europe/Amsterdam')) precision=0 scale=0 <- scale lost
Decimal(18, 4) precision=18 scale=4
SimpleAggregateFunction(any, Decimal(18, 4)) precision=0 scale=0 <- both lost
LowCardinality is unaffected because the parser treats it as a flag on the column rather than a nesting level, so dataType is already the inner type.
This matters for any consumer that sizes/rounds numeric columns from metadata (BI tools, Spark/Trino connectors, schema mirroring): a SimpleAggregateFunction(sum, Decimal(18,4)) column in an AggregatingMergeTree table looks like a scale-0 value, and SimpleAggregateFunction(any, DateTime64(3, tz)) looks like second precision.
What is not affected
Worth stating explicitly, since the sibling report in clickhouse-cs covers both halves: the timezone/offset read path is fine here. getTimestamp() and getObject(…, OffsetDateTime.class) return the correct offset for SimpleAggregateFunction- and LowCardinality-wrapped DateTime/DateTime64, because AbstractBinaryFormatReader.getZonedDateTime(int) switches on column.getValueDataType(), which already unwraps SimpleAggregateFunction:
SimpleAggregateFunction(any, DateTime('Europe/Amsterdam')) getObject(OffsetDateTime) = 2024-01-15T12:30:45+01:00
SimpleAggregateFunction(any, DateTime64(3, 'Europe/Amsterdam')) getObject(OffsetDateTime) = 2024-01-15T12:30:45.123+01:00
LowCardinality(DateTime('Europe/Amsterdam')) getObject(OffsetDateTime) = 2024-01-15T12:30:45+01:00
One secondary observation from the same probe, same root cause, mentioned for context rather than as a separate ask: getObject(1) (no type hint) returns a java.time.ZonedDateTime for SimpleAggregateFunction(any, DateTime(tz)) but a java.sql.Timestamp for the plain and LowCardinality-wrapped forms — JdbcUtils.java:118 maps SimpleAggregateFunction to JDBCType.OTHER instead of resolving through the nested type.
ClickHouse server version
26.7.2.59 (official build) — verified against a running server, not by inspection alone.
Reproduction
TestNG integration test in jdbc-v2 (mvn -pl jdbc-v2 verify -Dit.test=SafWrapperMetadataTest -DskipUTs=true):
package com.clickhouse.jdbc;
import org.testng.Assert;
import org.testng.annotations.Test;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.Statement;
@Test(groups = {"integration"})
public class SafWrapperMetadataTest extends JdbcIntegrationTest {
@Test(groups = {"integration"})
void testSimpleAggregateFunctionMetadataPrecisionAndScale() throws Exception {
try (Connection conn = getJdbcConnection(); Statement stmt = conn.createStatement()) {
try (ResultSet rs = stmt.executeQuery("SELECT anySimpleState(toDecimal64(1.2345, 4)) AS v")) {
Assert.assertTrue(rs.next());
ResultSetMetaData md = rs.getMetaData();
Assert.assertEquals(md.getColumnTypeName(1), "SimpleAggregateFunction(any, Decimal(18, 4))");
Assert.assertEquals(md.getPrecision(1), 18, "precision of wrapped Decimal(18,4)");
Assert.assertEquals(md.getScale(1), 4, "scale of wrapped Decimal(18,4)");
}
try (ResultSet rs = stmt.executeQuery(
"SELECT anySimpleState(toDateTime64('2024-01-15 12:30:45.123', 3, 'Europe/Amsterdam')) AS v")) {
Assert.assertTrue(rs.next());
Assert.assertEquals(rs.getMetaData().getScale(1), 3, "scale of wrapped DateTime64(3)");
}
}
}
}
Actual:
java.lang.AssertionError: precision of wrapped Decimal(18,4) expected [18] but found [0]
at com.clickhouse.jdbc.SafWrapperMetadataTest.testSimpleAggregateFunctionMetadataPrecisionAndScale(...)
(and, with the precision assertion removed, scale ... expected [4] but found [0], then expected [3] but found [0] for the DateTime64(3) case)
Expected: 18 / 4 and 3 — the same values the driver already reports for the unwrapped Decimal(18, 4) and DateTime64(3, 'Europe/Amsterdam') columns.
Equivalent on a real table:
CREATE TABLE t (k UInt8, amount SimpleAggregateFunction(sum, Decimal(18,4)))
ENGINE AggregatingMergeTree ORDER BY k;
-- SELECT amount FROM t -> getPrecision(1) == 0, getScale(1) == 0
Suggested fix
Resolve precision/scale through the transparent wrapper rather than the outer type. Two candidate places:
jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/ResultSetMetaDataImpl.java:241,246 — when getColumn(column).getDataType() == SimpleAggregateFunction, take getNestedColumns().get(0) before reading precision/scale (the same unwrap AbstractBinaryFormatReader.setSchema already does at AbstractBinaryFormatReader.java:314).
- or
clickhouse-data/src/main/java/com/clickhouse/data/ClickHouseColumn.java — after column.valueDataType = nestedColumns.get(0).getDataType() at line 447, also copy the nested column's precision/scale (and possibly timeZone) onto the wrapper. This is the broader fix and would also help v1 consumers, but it touches shared parsing, so the jdbc-v2-local unwrap may be the safer scope.
Note this must stay limited to SimpleAggregateFunction; genuine AggregateFunction columns are not pass-throughs and should keep their current metadata. getColumnTypeName() must keep returning the full wrapped type name.
Related closed issue in the same family (nullability of SimpleAggregateFunction(any, Nullable(Int8))): #2110. Test-coverage tracker: #2158.
Link
Found while investigating the analogous report in clickhouse-cs: ClickHouse/clickhouse-cs#518 (there, GetEffectiveClickHouseType unwraps only Nullable, breaking both GetDateTimeOffset() and GetSchemaTable() precision/scale). In clickhouse-java only the metadata half reproduces.
Description
SimpleAggregateFunction(func, T)is a transparent wrapper on the read path — client-v2'sBinaryStreamReaderdelegates straight to the nested column (BinaryStreamReader.java:265), andgetValueDataType()exists precisely to expose the underlying type. ButResultSetMetaDataImpl.getPrecision(int)/getScale(int)read the outerClickHouseColumn:For a
SimpleAggregateFunction(...)column,ClickHouseColumnparsing (clickhouse-data/.../ClickHouseColumn.java:443-449) setsdataType = SimpleAggregateFunctionand stores the real type innested.get(0). Precision/scale are then filled from the wrapper's own data type (ClickHouseColumn.java:147,column.precision = column.dataType.getMaxPrecision()), and the parameter-driven branches forDecimal/DateTime64never run. So the declared precision and scale of the wrapped type are lost and both accessors return0.Measured on a live server (probe over the whole matrix;
getColumnTypeName | getPrecision | getScale):LowCardinalityis unaffected because the parser treats it as a flag on the column rather than a nesting level, sodataTypeis already the inner type.This matters for any consumer that sizes/rounds numeric columns from metadata (BI tools, Spark/Trino connectors, schema mirroring): a
SimpleAggregateFunction(sum, Decimal(18,4))column in anAggregatingMergeTreetable looks like a scale-0 value, andSimpleAggregateFunction(any, DateTime64(3, tz))looks like second precision.What is not affected
Worth stating explicitly, since the sibling report in clickhouse-cs covers both halves: the timezone/offset read path is fine here.
getTimestamp()andgetObject(…, OffsetDateTime.class)return the correct offset forSimpleAggregateFunction- andLowCardinality-wrappedDateTime/DateTime64, becauseAbstractBinaryFormatReader.getZonedDateTime(int)switches oncolumn.getValueDataType(), which already unwrapsSimpleAggregateFunction:One secondary observation from the same probe, same root cause, mentioned for context rather than as a separate ask:
getObject(1)(no type hint) returns ajava.time.ZonedDateTimeforSimpleAggregateFunction(any, DateTime(tz))but ajava.sql.Timestampfor the plain andLowCardinality-wrapped forms —JdbcUtils.java:118mapsSimpleAggregateFunctiontoJDBCType.OTHERinstead of resolving through the nested type.ClickHouse server version
26.7.2.59(official build) — verified against a running server, not by inspection alone.Reproduction
TestNG integration test in
jdbc-v2(mvn -pl jdbc-v2 verify -Dit.test=SafWrapperMetadataTest -DskipUTs=true):Actual:
(and, with the precision assertion removed,
scale ... expected [4] but found [0], thenexpected [3] but found [0]for theDateTime64(3)case)Expected:
18/4and3— the same values the driver already reports for the unwrappedDecimal(18, 4)andDateTime64(3, 'Europe/Amsterdam')columns.Equivalent on a real table:
Suggested fix
Resolve precision/scale through the transparent wrapper rather than the outer type. Two candidate places:
jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/ResultSetMetaDataImpl.java:241,246— whengetColumn(column).getDataType() == SimpleAggregateFunction, takegetNestedColumns().get(0)before reading precision/scale (the same unwrapAbstractBinaryFormatReader.setSchemaalready does atAbstractBinaryFormatReader.java:314).clickhouse-data/src/main/java/com/clickhouse/data/ClickHouseColumn.java— aftercolumn.valueDataType = nestedColumns.get(0).getDataType()at line 447, also copy the nested column'sprecision/scale(and possiblytimeZone) onto the wrapper. This is the broader fix and would also help v1 consumers, but it touches shared parsing, so the jdbc-v2-local unwrap may be the safer scope.Note this must stay limited to
SimpleAggregateFunction; genuineAggregateFunctioncolumns are not pass-throughs and should keep their current metadata.getColumnTypeName()must keep returning the full wrapped type name.Related closed issue in the same family (nullability of
SimpleAggregateFunction(any, Nullable(Int8))): #2110. Test-coverage tracker: #2158.Link
Found while investigating the analogous report in clickhouse-cs: ClickHouse/clickhouse-cs#518 (there,
GetEffectiveClickHouseTypeunwraps onlyNullable, breaking bothGetDateTimeOffset()andGetSchemaTable()precision/scale). In clickhouse-java only the metadata half reproduces.