Skip to content

Parquet TIMESTAMP_NANOS is registered as timestamptz and no-copy insert keeps nanosecond bounds #227

Open

Description

@ianton-ru

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 Parquet

ts_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_ns

Every 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 unit
  • ice/src/main/java/org/apache/iceberg/parquet/ParquetConversions.java — TIMESTAMP adjusts MILLIS only; NANOS is copied. TIMESTAMP_NANO in convertValue also returns the value unchanged, but schema inference never produces that type
  • ice/src/main/java/org/apache/iceberg/parquet/ParquetUtil.java — footer metrics go through fromParquetPrimitive

Issue #61 fixed millisecond bounds. Nanosecond bounds are still copied unchanged in 0.23.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

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