From 2783a1e50f08e313a00f1177ff7e9c1ad554b4ac Mon Sep 17 00:00:00 2001 From: Anupam Yadav Date: Wed, 1 Jul 2026 20:22:06 +0000 Subject: [PATCH 1/2] [SPARK-57828][SQL] Add vectorized Parquet reader support for nanosecond-precision timestamps --- .../parquet/ParquetVectorUpdaterFactory.java | 85 +++++++ .../parquet/VectorizedColumnReader.java | 3 + .../types/ops/TimestampNanosParquetOps.scala | 11 +- .../parquet/ParquetTimestampNanosSuite.scala | 221 +++++++++++------- 4 files changed, 229 insertions(+), 91 deletions(-) diff --git a/sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/ParquetVectorUpdaterFactory.java b/sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/ParquetVectorUpdaterFactory.java index 544a60a21222a..a48e9eeddb488 100644 --- a/sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/ParquetVectorUpdaterFactory.java +++ b/sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/ParquetVectorUpdaterFactory.java @@ -163,6 +163,16 @@ public ParquetVectorUpdater getUpdater(ColumnDescriptor descriptor, DataType spa return new LongAsMicrosUpdater(); } else if (sparkType instanceof DayTimeIntervalType) { return new LongUpdater(); + } else if (sparkType instanceof TimestampNTZNanosType && + isTimestampTypeMatched(LogicalTypeAnnotation.TimeUnit.NANOS)) { + // TIMESTAMP(NANOS) postdates the proleptic Gregorian switch, so no datetime rebase is + // needed. The INT64 epoch-nanos value is decomposed into (epochMicros, nanosWithinMicro) + // and truncated to the requested precision, matching the row-based converter path. + return new TimestampNanosUpdater(((TimestampNTZNanosType) sparkType).precision()); + } else if (sparkType instanceof TimestampLTZNanosType && + isTimestampTypeMatched(LogicalTypeAnnotation.TimeUnit.NANOS)) { + // Same as NTZ nanos: no rebase, no timezone conversion at storage level. + return new TimestampNanosUpdater(((TimestampLTZNanosType) sparkType).precision()); } else if (canReadAsDecimal(descriptor, sparkType)) { return new LongToDecimalUpdater(descriptor, (DecimalType) sparkType); } else if (sparkType instanceof TimeType && @@ -922,6 +932,81 @@ public void decodeSingleDictionaryId( } } + // Reads an INT64 TIMESTAMP(NANOS) column. The epoch-nanos value is decomposed into + // (epochMicros, nanosWithinMicro) and the sub-microsecond digits are truncated to the requested + // precision. This matches the row-based converter in TimestampNanosParquetOps.newConverter which + // calls DateTimeUtils.epochNanosToTimestampNanos(value, precision). + // No datetime rebase is applied (TIMESTAMP(NANOS) postdates the proleptic Gregorian switch). + // No timezone conversion is applied at the storage level. + private static class TimestampNanosUpdater implements ParquetVectorUpdater { + private final int precision; + + // Pre-computed truncation divisor for the nanosWithinMicro component. + // precision 7 -> divisor 100, precision 8 -> divisor 10, precision 9 -> divisor 1 + private final int nanosTruncationDivisor; + + TimestampNanosUpdater(int precision) { + this.precision = precision; + switch (precision) { + case 7: + this.nanosTruncationDivisor = 100; + break; + case 8: + this.nanosTruncationDivisor = 10; + break; + case 9: + this.nanosTruncationDivisor = 1; + break; + default: + throw new IllegalArgumentException( + "Invalid nanosecond timestamp precision: " + precision); + } + } + + private void putTimestampNanos(int offset, WritableColumnVector values, long epochNanos) { + long epochMicros = Math.floorDiv(epochNanos, 1000L); + int rawNanosWithinMicro = (int) Math.floorMod(epochNanos, 1000L); + short nanosWithinMicro = + (short) ((rawNanosWithinMicro / nanosTruncationDivisor) * nanosTruncationDivisor); + values.getChild(0).putLong(offset, epochMicros); + values.getChild(1).putShort(offset, nanosWithinMicro); + } + + @Override + public void readValues( + int total, + int offset, + WritableColumnVector values, + VectorizedValuesReader valuesReader) { + for (int i = 0; i < total; i++) { + putTimestampNanos(offset + i, values, valuesReader.readLong()); + } + } + + @Override + public void skipValues(int total, VectorizedValuesReader valuesReader) { + valuesReader.skipLongs(total); + } + + @Override + public void readValue( + int offset, + WritableColumnVector values, + VectorizedValuesReader valuesReader) { + putTimestampNanos(offset, values, valuesReader.readLong()); + } + + @Override + public void decodeSingleDictionaryId( + int offset, + WritableColumnVector values, + WritableColumnVector dictionaryIds, + Dictionary dictionary) { + long epochNanos = dictionary.decodeToLong(dictionaryIds.getDictId(offset)); + putTimestampNanos(offset, values, epochNanos); + } + } + // Reads an INT64 TIME column into the internal nanoseconds-since-midnight representation and // truncates it to the requested TimeType precision. `fileStoresNanos` selects the on-disk unit: // TIME(NANOS) stores nanos directly (identity), TIME(MICROS) stores micros (converted to nanos). diff --git a/sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/VectorizedColumnReader.java b/sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/VectorizedColumnReader.java index 01f4573557dc1..b3efcf256aa6b 100644 --- a/sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/VectorizedColumnReader.java +++ b/sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/VectorizedColumnReader.java @@ -169,8 +169,11 @@ private boolean isLazyDecodingSupported( // TIME columns (both MICROS and NANOS) need per-value processing in the updater: a unit // conversion for MICROS and/or truncation to the requested precision. Lazy dictionary // decoding would bypass the updater, so it must be disabled for them. + // TIMESTAMP(NANOS) columns also need per-value processing: the INT64 epoch-nanos is + // decomposed into a two-child vector (epochMicros, nanosWithinMicro). boolean needsUpcast = (isDecimal && !DecimalType.is64BitDecimalType(sparkType)) || updaterFactory.isTimestampTypeMatched(TimeUnit.MILLIS) || + updaterFactory.isTimestampTypeMatched(TimeUnit.NANOS) || updaterFactory.isTimeTypeMatched(TimeUnit.MICROS) || updaterFactory.isTimeTypeMatched(TimeUnit.NANOS); boolean needsRebase = updaterFactory.isTimestampTypeMatched(TimeUnit.MICROS) && diff --git a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/types/ops/TimestampNanosParquetOps.scala b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/types/ops/TimestampNanosParquetOps.scala index 4b5f01d17374e..6132ab8b3350e 100644 --- a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/types/ops/TimestampNanosParquetOps.scala +++ b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/parquet/types/ops/TimestampNanosParquetOps.scala @@ -27,6 +27,7 @@ import org.apache.spark.sql.catalyst.expressions.SpecializedGetters import org.apache.spark.sql.catalyst.util.DateTimeUtils import org.apache.spark.sql.errors.QueryExecutionErrors import org.apache.spark.sql.execution.datasources.parquet.{HasParentContainerUpdater, ParentContainerUpdater, ParquetPrimitiveConverter} +import org.apache.spark.sql.internal.SQLConf import org.apache.spark.sql.types.{DataType, TimestampLTZNanosType, TimestampNTZNanosType} import org.apache.spark.unsafe.types.TimestampNanosVal @@ -53,9 +54,9 @@ import org.apache.spark.unsafe.types.TimestampNanosVal * * TIMESTAMP(NANOS) postdates Spark's switch to the proleptic Gregorian calendar, so the values are * exempt from datetime rebasing (the rebase modes only cover DATE, TIMESTAMP_MILLIS and - * TIMESTAMP_MICROS). Vectorized read is not supported: the value is a 16-byte composite rather - * than a single long slot, so `isBatchReadSupported` stays false (the trait default) and reads go - * through the row-based converter. + * TIMESTAMP_MICROS). The vectorized reader decomposes the INT64 epoch-nanos into the two-child + * column vector (epochMicros: Long, nanosWithinMicro: Short) via TimestampNanosUpdater in + * ParquetVectorUpdaterFactory. * * @see ParquetTypeOps for the dispatch contract * @since 4.3.0 @@ -77,6 +78,10 @@ private[parquet] trait TimestampNanosParquetOps extends ParquetTypeOps { // The Parquet TIMESTAMP `isAdjustedToUTC` flag: LTZ is UTC-adjusted, NTZ is not. private def isAdjustedToUTC: Boolean = !isNtz + // ==================== Vectorized Read Support ==================== + + override def isBatchReadSupported(sqlConf: SQLConf): Boolean = true + // ==================== Schema Conversion ==================== override def convertToParquetType( diff --git a/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetTimestampNanosSuite.scala b/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetTimestampNanosSuite.scala index b3e8ecdd58682..e62d952a9eb91 100644 --- a/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetTimestampNanosSuite.scala +++ b/sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/parquet/ParquetTimestampNanosSuite.scala @@ -100,37 +100,37 @@ class ParquetTimestampNanosSuite extends QueryTest with ParquetTest with SharedS test("SPARK-57102: read a foreign TIMESTAMP(NANOS) file as nanosecond timestamp types") { withNanosEnabled { - withSQLConf( - SQLConf.SESSION_LOCAL_TIMEZONE.key -> "UTC", - SQLConf.PARQUET_VECTORIZED_READER_ENABLED.key -> "false") { - val values = Seq(Some(123L), Some(1000000123L), Some(-1L), None) + withSQLConf(SQLConf.SESSION_LOCAL_TIMEZONE.key -> "UTC") { + withAllParquetReaders { + val values = Seq(Some(123L), Some(1000000123L), Some(-1L), None) - withTempPath { dir => - val file = new File(dir, "ntz.parquet") - writeForeignNanosParquet(file, isAdjustedToUTC = false, values) - val read = spark.read.parquet(file.getCanonicalPath) - assert(read.schema("ts").dataType === TimestampNTZNanosType(9)) - checkAnswer(read, spark.sql( - """SELECT * FROM VALUES - | (TIMESTAMP_NTZ '1970-01-01 00:00:00.000000123'), - | (TIMESTAMP_NTZ '1970-01-01 00:00:01.000000123'), - | (TIMESTAMP_NTZ '1969-12-31 23:59:59.999999999'), - | (CAST(NULL AS TIMESTAMP_NTZ(9))) - | AS t(ts)""".stripMargin).collect().toSeq) - } + withTempPath { dir => + val file = new File(dir, "ntz.parquet") + writeForeignNanosParquet(file, isAdjustedToUTC = false, values) + val read = spark.read.parquet(file.getCanonicalPath) + assert(read.schema("ts").dataType === TimestampNTZNanosType(9)) + checkAnswer(read, spark.sql( + """SELECT * FROM VALUES + | (TIMESTAMP_NTZ '1970-01-01 00:00:00.000000123'), + | (TIMESTAMP_NTZ '1970-01-01 00:00:01.000000123'), + | (TIMESTAMP_NTZ '1969-12-31 23:59:59.999999999'), + | (CAST(NULL AS TIMESTAMP_NTZ(9))) + | AS t(ts)""".stripMargin).collect().toSeq) + } - withTempPath { dir => - val file = new File(dir, "ltz.parquet") - writeForeignNanosParquet(file, isAdjustedToUTC = true, values) - val read = spark.read.parquet(file.getCanonicalPath) - assert(read.schema("ts").dataType === TimestampLTZNanosType(9)) - checkAnswer(read, spark.sql( - """SELECT * FROM VALUES - | (TIMESTAMP_LTZ '1970-01-01 00:00:00.000000123'), - | (TIMESTAMP_LTZ '1970-01-01 00:00:01.000000123'), - | (TIMESTAMP_LTZ '1969-12-31 23:59:59.999999999'), - | (CAST(NULL AS TIMESTAMP_LTZ(9))) - | AS t(ts)""".stripMargin).collect().toSeq) + withTempPath { dir => + val file = new File(dir, "ltz.parquet") + writeForeignNanosParquet(file, isAdjustedToUTC = true, values) + val read = spark.read.parquet(file.getCanonicalPath) + assert(read.schema("ts").dataType === TimestampLTZNanosType(9)) + checkAnswer(read, spark.sql( + """SELECT * FROM VALUES + | (TIMESTAMP_LTZ '1970-01-01 00:00:00.000000123'), + | (TIMESTAMP_LTZ '1970-01-01 00:00:01.000000123'), + | (TIMESTAMP_LTZ '1969-12-31 23:59:59.999999999'), + | (CAST(NULL AS TIMESTAMP_LTZ(9))) + | AS t(ts)""".stripMargin).collect().toSeq) + } } } } @@ -138,38 +138,38 @@ class ParquetTimestampNanosSuite extends QueryTest with ParquetTest with SharedS test("SPARK-57102: explicit lower-precision read schema truncates sub-precision nanos") { withNanosEnabled { - withSQLConf( - SQLConf.SESSION_LOCAL_TIMEZONE.key -> "UTC", - SQLConf.PARQUET_VECTORIZED_READER_ENABLED.key -> "false") { - // Foreign file with full 9-digit precision: .123456789 after the epoch, and -1ns (which - // floors to epochMicros = -1, nanosWithinMicro = 999). - val values = Seq(Some(123456789L), Some(-1L)) + withSQLConf(SQLConf.SESSION_LOCAL_TIMEZONE.key -> "UTC") { + withAllParquetReaders { + // Foreign file with full 9-digit precision: .123456789 after the epoch, and -1ns (which + // floors to epochMicros = -1, nanosWithinMicro = 999). + val values = Seq(Some(123456789L), Some(-1L)) - withTempPath { dir => - val file = new File(dir, "ntz.parquet") - writeForeignNanosParquet(file, isAdjustedToUTC = false, values) - // Reading with an explicit TIMESTAMP_NTZ(7) schema must floor the sub-microsecond - // digits to precision 7, matching DateTimeUtils.localDateTimeToTimestampNanos. - val read = spark.read.schema("ts TIMESTAMP_NTZ(7)").parquet(file.getCanonicalPath) - assert(read.schema("ts").dataType === TimestampNTZNanosType(7)) - checkAnswer(read, spark.sql( - """SELECT * FROM VALUES - | (TIMESTAMP_NTZ '1970-01-01 00:00:00.123456700'), - | (TIMESTAMP_NTZ '1969-12-31 23:59:59.999999900') - | AS t(ts)""".stripMargin).collect().toSeq) - } + withTempPath { dir => + val file = new File(dir, "ntz.parquet") + writeForeignNanosParquet(file, isAdjustedToUTC = false, values) + // Reading with an explicit TIMESTAMP_NTZ(7) schema must floor the sub-microsecond + // digits to precision 7, matching DateTimeUtils.localDateTimeToTimestampNanos. + val read = spark.read.schema("ts TIMESTAMP_NTZ(7)").parquet(file.getCanonicalPath) + assert(read.schema("ts").dataType === TimestampNTZNanosType(7)) + checkAnswer(read, spark.sql( + """SELECT * FROM VALUES + | (TIMESTAMP_NTZ '1970-01-01 00:00:00.123456700'), + | (TIMESTAMP_NTZ '1969-12-31 23:59:59.999999900') + | AS t(ts)""".stripMargin).collect().toSeq) + } - withTempPath { dir => - val file = new File(dir, "ltz.parquet") - writeForeignNanosParquet(file, isAdjustedToUTC = true, values) - // precision 8 drops only the last digit. - val read = spark.read.schema("ts TIMESTAMP_LTZ(8)").parquet(file.getCanonicalPath) - assert(read.schema("ts").dataType === TimestampLTZNanosType(8)) - checkAnswer(read, spark.sql( - """SELECT * FROM VALUES - | (TIMESTAMP_LTZ '1970-01-01 00:00:00.123456780'), - | (TIMESTAMP_LTZ '1969-12-31 23:59:59.999999990') - | AS t(ts)""".stripMargin).collect().toSeq) + withTempPath { dir => + val file = new File(dir, "ltz.parquet") + writeForeignNanosParquet(file, isAdjustedToUTC = true, values) + // precision 8 drops only the last digit. + val read = spark.read.schema("ts TIMESTAMP_LTZ(8)").parquet(file.getCanonicalPath) + assert(read.schema("ts").dataType === TimestampLTZNanosType(8)) + checkAnswer(read, spark.sql( + """SELECT * FROM VALUES + | (TIMESTAMP_LTZ '1970-01-01 00:00:00.123456780'), + | (TIMESTAMP_LTZ '1969-12-31 23:59:59.999999990') + | AS t(ts)""".stripMargin).collect().toSeq) + } } } } @@ -269,22 +269,22 @@ class ParquetTimestampNanosSuite extends QueryTest with ParquetTest with SharedS test("SPARK-57102: datetime rebase configs do not affect TIMESTAMP(NANOS) reads") { withNanosEnabled { - withSQLConf( - SQLConf.SESSION_LOCAL_TIMEZONE.key -> "UTC", - SQLConf.PARQUET_VECTORIZED_READER_ENABLED.key -> "false") { - withTempPath { dir => - val file = new File(dir, "ltz.parquet") - // 1800-01-01 00:00:00 UTC predates the last Julian-to-Gregorian switch instant of the - // rebase logic, so applying a timestamp rebase (or the EXCEPTION-mode guard) on this - // value would change the result or fail the read. - writeForeignNanosParquet( - file, isAdjustedToUTC = true, Seq(Some(-5364662400000000000L))) - Seq("EXCEPTION", "CORRECTED", "LEGACY").foreach { mode => - withSQLConf(SQLConf.PARQUET_REBASE_MODE_IN_READ.key -> mode) { - checkAnswer( - spark.read.parquet(file.getCanonicalPath), - spark.sql( - "SELECT TIMESTAMP_LTZ '1800-01-01 00:00:00.000000000'").collect().toSeq) + withSQLConf(SQLConf.SESSION_LOCAL_TIMEZONE.key -> "UTC") { + withAllParquetReaders { + withTempPath { dir => + val file = new File(dir, "ltz.parquet") + // 1800-01-01 00:00:00 UTC predates the last Julian-to-Gregorian switch instant of the + // rebase logic, so applying a timestamp rebase (or the EXCEPTION-mode guard) on this + // value would change the result or fail the read. + writeForeignNanosParquet( + file, isAdjustedToUTC = true, Seq(Some(-5364662400000000000L))) + Seq("EXCEPTION", "CORRECTED", "LEGACY").foreach { mode => + withSQLConf(SQLConf.PARQUET_REBASE_MODE_IN_READ.key -> mode) { + checkAnswer( + spark.read.parquet(file.getCanonicalPath), + spark.sql( + "SELECT TIMESTAMP_LTZ '1800-01-01 00:00:00.000000000'").collect().toSeq) + } } } } @@ -294,7 +294,7 @@ class ParquetTimestampNanosSuite extends QueryTest with ParquetTest with SharedS test("SPARK-57102: nanos timestamps round-trip inside a nested (array) column") { withNanosEnabled { - withSQLConf(SQLConf.PARQUET_VECTORIZED_READER_ENABLED.key -> "false") { + withAllParquetReaders { withTempPath { dir => val df = spark.sql( "SELECT array(TIMESTAMP_NTZ '2020-01-01 00:00:00.123456789', " + @@ -311,18 +311,63 @@ class ParquetTimestampNanosSuite extends QueryTest with ParquetTest with SharedS test("SPARK-57102: nanos timestamps round-trip via the V2 file source") { withNanosEnabled { - withSQLConf( - SQLConf.USE_V1_SOURCE_LIST.key -> "", - SQLConf.PARQUET_VECTORIZED_READER_ENABLED.key -> "false") { - withTempPath { dir => - val df = spark.sql( - "SELECT TIMESTAMP_NTZ '2020-01-01 12:34:56.123456789' AS ntz, " + - "TIMESTAMP_LTZ '2020-01-01 12:34:56.123456789' AS ltz") - df.write.parquet(dir.getCanonicalPath) - val read = spark.read.parquet(dir.getCanonicalPath) - assert(read.schema("ntz").dataType === TimestampNTZNanosType(9)) - assert(read.schema("ltz").dataType === TimestampLTZNanosType(9)) - checkAnswer(read, df.collect().toSeq) + withSQLConf(SQLConf.USE_V1_SOURCE_LIST.key -> "") { + withAllParquetReaders { + withTempPath { dir => + val df = spark.sql( + "SELECT TIMESTAMP_NTZ '2020-01-01 12:34:56.123456789' AS ntz, " + + "TIMESTAMP_LTZ '2020-01-01 12:34:56.123456789' AS ltz") + df.write.parquet(dir.getCanonicalPath) + val read = spark.read.parquet(dir.getCanonicalPath) + assert(read.schema("ntz").dataType === TimestampNTZNanosType(9)) + assert(read.schema("ltz").dataType === TimestampLTZNanosType(9)) + checkAnswer(read, df.collect().toSeq) + } + } + } + } + } + + test("SPARK-57828: vectorized reader produces identical results to row-based for nanos") { + withNanosEnabled { + withSQLConf(SQLConf.SESSION_LOCAL_TIMEZONE.key -> "UTC") { + // Write foreign parquet files with TIMESTAMP(NANOS) values covering edge cases: + // positive, negative (pre-epoch), values with precision truncation, and nulls. + val values = Seq( + Some(123456789L), // 1970-01-01 00:00:00.123456789 + Some(-1L), // 1969-12-31 23:59:59.999999999 (pre-epoch, floor semantics) + Some(1000000000L), // 1970-01-01 00:00:01.000000000 (exact second) + Some(-1000000001L), // 1969-12-31 23:59:58.999999999 + None // null + ) + + Seq(7, 8, 9).foreach { p => + Seq(true, false).foreach { isAdjustedToUTC => + val typeName = if (isAdjustedToUTC) "TIMESTAMP_LTZ" else "TIMESTAMP_NTZ" + withClue(s"$typeName($p)") { + withTempPath { dir => + val file = new File(dir, s"ts_nanos_${typeName}_p$p.parquet") + writeForeignNanosParquet(file, isAdjustedToUTC, values) + + val schema = s"ts $typeName($p)" + + // Row-based read + val rowBased = withSQLConf( + SQLConf.PARQUET_VECTORIZED_READER_ENABLED.key -> "false") { + spark.read.schema(schema).parquet(file.getCanonicalPath).collect() + } + + // Vectorized read + val vectorized = withSQLConf( + SQLConf.PARQUET_VECTORIZED_READER_ENABLED.key -> "true") { + spark.read.schema(schema).parquet(file.getCanonicalPath).collect() + } + + assert(vectorized.toSeq === rowBased.toSeq, + s"Vectorized and row-based results differ for $typeName($p)") + } + } + } } } } From 00a3cc996372e0a553a2f2cc3d624b1861d4b0dc Mon Sep 17 00:00:00 2001 From: Anupam Yadav Date: Wed, 1 Jul 2026 23:47:17 +0000 Subject: [PATCH 2/2] [SPARK-57828][SQL] Remove unused precision field in TimestampNanosUpdater --- .../datasources/parquet/ParquetVectorUpdaterFactory.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/ParquetVectorUpdaterFactory.java b/sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/ParquetVectorUpdaterFactory.java index a48e9eeddb488..e40417427564a 100644 --- a/sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/ParquetVectorUpdaterFactory.java +++ b/sql/core/src/main/java/org/apache/spark/sql/execution/datasources/parquet/ParquetVectorUpdaterFactory.java @@ -939,14 +939,11 @@ public void decodeSingleDictionaryId( // No datetime rebase is applied (TIMESTAMP(NANOS) postdates the proleptic Gregorian switch). // No timezone conversion is applied at the storage level. private static class TimestampNanosUpdater implements ParquetVectorUpdater { - private final int precision; - // Pre-computed truncation divisor for the nanosWithinMicro component. // precision 7 -> divisor 100, precision 8 -> divisor 10, precision 9 -> divisor 1 private final int nanosTruncationDivisor; TimestampNanosUpdater(int precision) { - this.precision = precision; switch (precision) { case 7: this.nanosTruncationDivisor = 100;