Skip to content

[jdbc-v2] ResultSetMetaData.getPrecision()/getScale() return 0 for SimpleAggregateFunction-wrapped Decimal and DateTime64 columns #3042

Description

@claude

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions