Repository navigation
Description
ice create-table --schema-from-parquet maps a Parquet timestamp / timestamptz column with unit NANOS to Iceberg timestamp / timestamptz (microseconds). The unit is ignored in MessageTypeToType.ParquetLogicalTypeVisitor.visit(TimestampLogicalTypeAnnotation), so the column is never timestamp_ns / timestamptz_ns.
ice insert --no-copy and --force-no-copy then copy the Parquet footer min/max through ParquetConversions.fromParquetPrimitive / convertValue. Those methods multiply MILLIS by 1000 and leave NANOS unchanged. The manifest bounds stay nanoseconds on a microsecond column, about 1000x too large. A reader that treats the bounds as microseconds can drop the file during min/max pruning.
A normal insert rewrites the file to timestamp[us] and the bounds are then correct. The bad bounds come from the no-copy path.
ClickHouse DateTime64(9) writes Parquet TIMESTAMP_NANOS. The Parquet logical type still describes nanoseconds, while a no-copy manifest stores the nanosecond integers. ClickHouse issue 2747 and PR 2777 are the reader-side workaround.
Reproduction
Ice 0.23.0 (altinity/ice:debug-0.23.0, Apache Iceberg 1.9.2) and altinity/ice-rest-catalog:0.23.0.
SELECT toDateTime64('2026-08-11 19:02:06.576815622', 9) AS ts
INTO OUTFILE 'ts_ns.parquet' TRUNCATE FORMAT Parquet
SELECT toDateTime64('2026-08-11 19:02:06.576815', 6) AS ts
INTO OUTFILE 'ts_us.parquet' TRUNCATE FORMAT Parquetts_ns.parquet is timestamp[ns, tz=UTC]. In this session the stored instant is 2026-08-11 17:02:06.576815622 UTC. ts_us.parquet is timestamp[us, tz=UTC] for the same instant.
ice create-table demo.ts_ns -p --schema-from-parquet file://ts_ns.parquet
ice insert demo.ts_ns --force-no-copy file://ts_ns.parquet
ice create-table demo.ts_us -p --schema-from-parquet file://ts_us.parquet
ice insert demo.ts_us --force-no-copy file://ts_us.parquet
ice create-table demo.ts_ns_rewrite -p --schema-from-parquet file://ts_ns.parquet
ice insert demo.ts_ns_rewrite file://ts_ns.parquet
ice describe -a demo.ts_nsEvery table is created as:
table {
1: ts: required timestamptz
}
| insert | lower/upper bound |
|---|---|
ts_ns.parquet, --force-no-copy |
1786467726576815622 |
ts_us.parquet, --force-no-copy |
1786467726576815 |
ts_ns.parquet, default insert (rewrite) |
1786467726576815 |
1786467726576815 microseconds is 2026-08-11 17:02:06.576815. The no-copy bound of the nanosecond file is that instant in nanoseconds. The rewritten file is timestamp[us, tz=UTC] and its footer min/max match the microsecond bound.
Expected
When the Iceberg column is timestamp / timestamptz, divide nanosecond footer stats by 1000, widening the range so it does not shrink, the same way MILLIS is multiplied by 1000. That matches what the rewrite path already produces.
If the table format can store timestamp_ns / timestamptz_ns, mapping Parquet TIMESTAMP_NANOS to that type and keeping the nanosecond bounds is the other consistent choice. Mapping to a microsecond type and keeping the raw nanosecond stats is not.
Where
ice/src/main/java/org/apache/iceberg/parquet/MessageTypeToType.java— timestamp visitor ignores the unitice/src/main/java/org/apache/iceberg/parquet/ParquetConversions.java—TIMESTAMPadjustsMILLISonly;NANOSis copied.TIMESTAMP_NANOinconvertValuealso returns the value unchanged, but schema inference never produces that typeice/src/main/java/org/apache/iceberg/parquet/ParquetUtil.java— footer metrics go throughfromParquetPrimitive
Issue #61 fixed millisecond bounds. Nanosecond bounds are still copied unchanged in 0.23.0.
Activity