This is an automated email from the ASF dual-hosted git repository.

xiangfu0 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git


The following commit(s) were added to refs/heads/master by this push:
     new 36c11cea184 Extract ArrowRecordExtractor type conversion into shared 
ArrowToPinotTypeConverter (#18632)
36c11cea184 is described below

commit 36c11cea184e9c2a218e8d9ff602c62314a8c91d
Author: Tony Song <[email protected]>
AuthorDate: Tue Jun 9 15:04:07 2026 -0700

    Extract ArrowRecordExtractor type conversion into shared 
ArrowToPinotTypeConverter (#18632)
    
    Pure refactor — no behavior change. Move the schema-driven Arrow → Pinot 
value
    conversion (originally introduced in #18434) from ArrowRecordExtractor into 
a
    new public static utility ArrowToPinotTypeConverter so it can be reused by
    column-major ColumnReader implementations that wrap Arrow vectors.
    
    ArrowToPinotTypeConverter exposes a single entry point:
    
        public static Object toPinotValue(Field field, Object value, boolean 
extractRawTimeValues)
    
    with all per-type helpers (convertTimestamp / Date / Time / List / Map /
    Struct / byRuntimeType) reduced to private static methods. The
    extractRawTimeValues flag — previously an ArrowRecordExtractor instance
    field — is threaded through as a method parameter so the converter remains
    stateless.
    
    ArrowRecordExtractor.convert(Field, Object) is removed; the per-row dispatch
    in extract() now calls ArrowToPinotTypeConverter.toPinotValue() directly.
    All existing tests in pinot-arrow continue to pass unchanged:
    
    - ArrowRecordReaderTest (2 / 2)
    - ArrowRecordExtractorTest (54 / 54)
    - ArrowMessageDecoderTest (7 / 7)
    
    This precursor lets a follow-up ColumnReader implementation reuse the
    conversion without duplicating ~200 lines of schema-driven dispatch.
---
 .../inputformat/arrow/ArrowRecordExtractor.java    | 254 +----------------
 .../arrow/ArrowToPinotTypeConverter.java           | 313 +++++++++++++++++++++
 2 files changed, 316 insertions(+), 251 deletions(-)

diff --git 
a/pinot-plugins/pinot-input-format/pinot-arrow/src/main/java/org/apache/pinot/plugin/inputformat/arrow/ArrowRecordExtractor.java
 
b/pinot-plugins/pinot-input-format/pinot-arrow/src/main/java/org/apache/pinot/plugin/inputformat/arrow/ArrowRecordExtractor.java
index 4bdfadeb25b..648c1e39e34 100644
--- 
a/pinot-plugins/pinot-input-format/pinot-arrow/src/main/java/org/apache/pinot/plugin/inputformat/arrow/ArrowRecordExtractor.java
+++ 
b/pinot-plugins/pinot-input-format/pinot-arrow/src/main/java/org/apache/pinot/plugin/inputformat/arrow/ArrowRecordExtractor.java
@@ -18,14 +18,7 @@
  */
 package org.apache.pinot.plugin.inputformat.arrow;
 
-import com.google.common.collect.Maps;
 import java.io.IOException;
-import java.sql.Timestamp;
-import java.time.Instant;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.time.LocalTime;
-import java.time.ZoneOffset;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -33,17 +26,13 @@ import javax.annotation.Nullable;
 import org.apache.arrow.vector.FieldVector;
 import org.apache.arrow.vector.ValueVector;
 import org.apache.arrow.vector.VectorSchemaRoot;
-import org.apache.arrow.vector.complex.MapVector;
 import org.apache.arrow.vector.dictionary.Dictionary;
 import org.apache.arrow.vector.dictionary.DictionaryEncoder;
 import org.apache.arrow.vector.ipc.ArrowReader;
-import org.apache.arrow.vector.types.pojo.ArrowType;
 import org.apache.arrow.vector.types.pojo.DictionaryEncoding;
-import org.apache.arrow.vector.types.pojo.Field;
 import org.apache.pinot.spi.data.readers.BaseRecordExtractor;
 import org.apache.pinot.spi.data.readers.GenericRow;
 import org.apache.pinot.spi.data.readers.RecordExtractorConfig;
-import org.apache.pinot.spi.utils.TimestampUtils;
 
 
 /// Extracts a single Arrow row into a [GenericRow]. Reader-scoped state 
([VectorSchemaRoot] +
@@ -208,247 +197,10 @@ public class ArrowRecordExtractor extends 
BaseRecordExtractor<ArrowRecordExtract
       ValueVector activeVector = activeVectors[i];
       Object rawValue = activeVector.getObject(from._rowId);
       to.putValue(fieldVectors[i].getField().getName(),
-          rawValue != null ? convert(activeVector.getField(), rawValue) : 
null);
+          rawValue != null
+              ? 
ArrowToPinotTypeConverter.toPinotValue(activeVector.getField(), rawValue, 
_extractRawTimeValues)
+              : null);
     }
     return to;
   }
-
-  /// Schema-driven dispatch — one branch per [ArrowType.ArrowTypeID]; complex 
types recurse with
-  /// their child [Field]s, scalars normalize per the contract.
-  @Nullable
-  private Object convert(Field field, Object value) {
-    ArrowType type = field.getType();
-    switch (type.getTypeID()) {
-      // Pass-through — Arrow boxes these directly into the contract output 
type.
-      case Bool:            // Boolean
-      case FloatingPoint:   // Float / Double
-      case Decimal:         // BigDecimal
-      case Binary:          // byte[]
-      case LargeBinary:     // byte[]
-      case FixedSizeBinary: // byte[]
-        return value;
-      // toString — `Utf8` / `LargeUtf8` produce `String`; `Interval` / 
`Duration` produce ISO-8601
-      // (`java.time.Period` / `java.time.Duration` / `PeriodDuration` all 
have meaningful toString).
-      case Utf8:
-      case LargeUtf8:
-      case Interval:
-      case Duration:
-        return value.toString();
-      // Integer — `Byte` widens to `Integer` per contract (sign-extended for 
signed `TinyIntVector`,
-      // zero-extended via `& 0xFF` for unsigned `UInt1Vector`); `Short` 
(signed `SmallIntVector`)
-      // sign-extends; `Character` (unsigned 16, from `UInt2Vector`) widens to 
its `int` code point;
-      // `Integer` / `Long` pass through.
-      case Int:
-        if (value instanceof Byte) {
-          int v = (Byte) value;
-          return ((ArrowType.Int) type).getIsSigned() ? v : v & 0xFF;
-        }
-        if (value instanceof Short) {
-          return ((Short) value).intValue();
-        }
-        if (value instanceof Character) {
-          return (int) (Character) value;
-        }
-        return value;
-      // Null — NullVector.getObject always returns null; extractValue 
short-circuits on null, so
-      // this branch is unreachable in practice. Defensive return.
-      case Null:
-        return null;
-      // Logical temporal — schema's `TimeUnit` drives the conversion.
-      case Timestamp:
-        return convertTimestamp((ArrowType.Timestamp) type, value);
-      case Date:
-        return convertDate((ArrowType.Date) type, value);
-      case Time:
-        return convertTime((ArrowType.Time) type, value);
-      // Multi-value — `List` (and primitive-array lists) → `Object[]`.
-      case List:
-      case LargeList:
-      case FixedSizeList:
-        return convertList(field.getChildren().get(0), (List<?>) value);
-      // Map / nested complex types.
-      case Map:
-        // The Map field's children are [entriesStruct]; the entries struct's 
children are
-        // [keyField, valueField] (named per MapVector.KEY_NAME / VALUE_NAME).
-        Field entriesField = field.getChildren().get(0);
-        return convertMap(entriesField.getChildren().get(0), 
entriesField.getChildren().get(1), (List<?>) value);
-      case Struct:
-        return convertStruct(field.getChildren(), (Map<?, ?>) value);
-      case Union:
-        // The chosen branch isn't visible from the resolved value alone — 
dispatch by the value's
-        // runtime Java type. Nested complex sub-branches fall back to 
`value.toString()`.
-        return convertByRuntimeType(value);
-      default:
-        // `NONE` is a placeholder; any other ID is a future Arrow addition.
-        throw new IllegalStateException("Unsupported Arrow type: " + type + " 
for field: " + field.getName());
-    }
-  }
-
-  /// Constructs a [Timestamp] from an Arrow `Timestamp` value. No-TZ vectors 
surface as
-  /// `LocalDateTime` (interpreted as UTC); with-TZ vectors surface as `Long` 
epoch counted in the
-  /// schema's `TimeUnit`. Sub-millisecond precision is preserved via 
[TimestampUtils].
-  /// With [#_extractRawTimeValues] the raw `long` epoch in the schema's 
`TimeUnit` is returned.
-  private Object convertTimestamp(ArrowType.Timestamp type, Object value) {
-    if (_extractRawTimeValues) {
-      if (value instanceof LocalDateTime) {
-        // No-TZ vector — convert the LocalDateTime back to an epoch `long` in 
the declared unit.
-        Instant instant = ((LocalDateTime) value).toInstant(ZoneOffset.UTC);
-        return toEpochInUnit(instant, type.getUnit());
-      }
-      // With-TZ vector — already raw `long` in the declared unit.
-      return value;
-    }
-    if (value instanceof LocalDateTime) {
-      return Timestamp.from(((LocalDateTime) value).toInstant(ZoneOffset.UTC));
-    }
-    long raw = ((Number) value).longValue();
-    switch (type.getUnit()) {
-      case SECOND:
-        return new Timestamp(raw * 1000L);
-      case MILLISECOND:
-        return new Timestamp(raw);
-      case MICROSECOND:
-        return TimestampUtils.fromMicrosSinceEpoch(raw);
-      case NANOSECOND:
-        return TimestampUtils.fromNanosSinceEpoch(raw);
-      default:
-        throw new IllegalStateException("Unsupported Timestamp unit: " + 
type.getUnit());
-    }
-  }
-
-  private static long toEpochInUnit(Instant instant, 
org.apache.arrow.vector.types.TimeUnit unit) {
-    switch (unit) {
-      case SECOND:
-        return instant.getEpochSecond();
-      case MILLISECOND:
-        return instant.toEpochMilli();
-      case MICROSECOND:
-        return Math.addExact(Math.multiplyExact(instant.getEpochSecond(), 
1_000_000L), instant.getNano() / 1_000L);
-      case NANOSECOND:
-        return Math.addExact(Math.multiplyExact(instant.getEpochSecond(), 
1_000_000_000L), instant.getNano());
-      default:
-        throw new IllegalStateException("Unsupported Timestamp unit: " + unit);
-    }
-  }
-
-  /// Reduces an Arrow `Date` value to its contract Java type ([LocalDate]), 
or to `int`
-  /// days-since-epoch when [#_extractRawTimeValues] is set. `DateDayVector` 
surfaces as `Integer`
-  /// raw days; `DateMilliVector` surfaces as `LocalDateTime` at UTC midnight.
-  private Object convertDate(ArrowType.Date type, Object value) {
-    int days;
-    switch (type.getUnit()) {
-      case DAY:
-        days = (Integer) value;
-        break;
-      case MILLISECOND:
-        days = (int) ((LocalDateTime) value).toLocalDate().toEpochDay();
-        break;
-      default:
-        throw new IllegalStateException("Unsupported Date unit: " + 
type.getUnit());
-    }
-    return _extractRawTimeValues ? days : LocalDate.ofEpochDay(days);
-  }
-
-  /// Constructs a [LocalTime] from an Arrow `Time` value, dispatched by the 
schema's `TimeUnit`:
-  /// `TimeMilliVector` surfaces as `LocalDateTime`; `TimeSecVector` as 
`Integer`;
-  /// `TimeMicroVector` / `TimeNanoVector` as `Long`. All collapse onto 
nanoseconds-since-midnight.
-  /// With [#_extractRawTimeValues] the raw count in the schema's `TimeUnit` 
is returned instead.
-  private Object convertTime(ArrowType.Time type, Object value) {
-    if (_extractRawTimeValues) {
-      if (value instanceof LocalDateTime) {
-        // `TimeMilliVector` surfaces as `LocalDateTime`; raw is `int` ms 
since midnight.
-        return (int) (((LocalDateTime) value).toLocalTime().toNanoOfDay() / 
1_000_000L);
-      }
-      // `TimeSecVector` (Integer) / `TimeMicroVector` / `TimeNanoVector` 
(Long) — already raw.
-      return value;
-    }
-    if (value instanceof LocalDateTime) {
-      return ((LocalDateTime) value).toLocalTime();
-    }
-    long raw = ((Number) value).longValue();
-    switch (type.getUnit()) {
-      case SECOND:
-        return LocalTime.ofSecondOfDay(raw);
-      case MILLISECOND:
-        return LocalTime.ofNanoOfDay(raw * 1_000_000L);
-      case MICROSECOND:
-        return LocalTime.ofNanoOfDay(raw * 1_000L);
-      case NANOSECOND:
-        return LocalTime.ofNanoOfDay(raw);
-      default:
-        throw new IllegalStateException("Unsupported Time unit: " + 
type.getUnit());
-    }
-  }
-
-  private Object[] convertList(Field elementField, List<?> list) {
-    int size = list.size();
-    Object[] result = new Object[size];
-    int i = 0;
-    for (Object element : list) {
-      result[i++] = element != null ? convert(elementField, element) : null;
-    }
-    return result;
-  }
-
-  /// Flattens an Arrow `Map` column's entry list (`List<Map<KEY_NAME, 
VALUE_NAME>>`) into a
-  /// `Map<String, Object>`, recursing into each value via [#convert] and 
stringifying each key via
-  /// [BaseRecordExtractor#stringifyMapKey] per the contract. Entries with a 
`null` key (input or
-  /// post-conversion) are dropped.
-  private Map<String, Object> convertMap(Field keyField, Field valueField, 
List<?> entries) {
-    Map<String, Object> result = 
Maps.newLinkedHashMapWithExpectedSize(entries.size());
-    for (Object entry : entries) {
-      if (entry == null) {
-        continue;
-      }
-      Map<?, ?> entryMap = (Map<?, ?>) entry;
-      Object rawKey = entryMap.get(MapVector.KEY_NAME);
-      if (rawKey == null) {
-        continue;
-      }
-      Object convertedKey = convert(keyField, rawKey);
-      if (convertedKey == null) {
-        continue;
-      }
-      Object rawValue = entryMap.get(MapVector.VALUE_NAME);
-      result.put(stringifyMapKey(convertedKey), rawValue != null ? 
convert(valueField, rawValue) : null);
-    }
-    return result;
-  }
-
-  private Map<String, Object> convertStruct(List<Field> childFields, Map<?, ?> 
value) {
-    Map<String, Object> result = 
Maps.newHashMapWithExpectedSize(childFields.size());
-    for (Field childField : childFields) {
-      String name = childField.getName();
-      Object rawValue = value.get(name);
-      result.put(name, rawValue != null ? convert(childField, rawValue) : 
null);
-    }
-    return result;
-  }
-
-  /// Runtime-type dispatch used by the `Union` case (where the chosen branch 
isn't accessible
-  /// from the resolved value). Mirrors the scalar handling of [#convert] for 
the common Arrow
-  /// boxed types; nested complex types fall back to `value.toString()` 
because their child
-  /// [Field]s aren't reachable from here.
-  private static Object convertByRuntimeType(Object value) {
-    if (value instanceof Number) {
-      if (value instanceof Byte || value instanceof Short) {
-        return ((Number) value).intValue();
-      }
-      return value;
-    }
-    if (value instanceof Boolean || value instanceof byte[]) {
-      return value;
-    }
-    if (value instanceof Character) {
-      // `UInt2Vector` surfaces as `Character`; widen to `int` per the Int(16) 
contract.
-      return (int) (Character) value;
-    }
-    if (value instanceof LocalDateTime) {
-      // Ambiguous between Timestamp / Date / Time — best-effort: treat as 
Timestamp UTC.
-      return Timestamp.from(((LocalDateTime) value).toInstant(ZoneOffset.UTC));
-    }
-    // `Text` (Utf8 / LargeUtf8), `Period` / `Duration` / `PeriodDuration` 
(Interval / Duration), and
-    // anything unrecognized fall through to `toString()`.
-    return value.toString();
-  }
 }
diff --git 
a/pinot-plugins/pinot-input-format/pinot-arrow/src/main/java/org/apache/pinot/plugin/inputformat/arrow/ArrowToPinotTypeConverter.java
 
b/pinot-plugins/pinot-input-format/pinot-arrow/src/main/java/org/apache/pinot/plugin/inputformat/arrow/ArrowToPinotTypeConverter.java
new file mode 100644
index 00000000000..3bebd57f4ec
--- /dev/null
+++ 
b/pinot-plugins/pinot-input-format/pinot-arrow/src/main/java/org/apache/pinot/plugin/inputformat/arrow/ArrowToPinotTypeConverter.java
@@ -0,0 +1,313 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.plugin.inputformat.arrow;
+
+import com.google.common.collect.Maps;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.ZoneOffset;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.apache.arrow.vector.complex.MapVector;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+import org.apache.arrow.vector.types.pojo.Field;
+import org.apache.pinot.spi.data.readers.BaseRecordExtractor;
+import org.apache.pinot.spi.utils.TimestampUtils;
+
+
+/**
+ * Stateless schema-driven Arrow → Pinot value converter shared by the 
row-major and
+ * column-major Arrow ingestion paths.
+ *
+ * <p>The conversion mirrors the contract established by {@link 
ArrowRecordExtractor} prior to
+ * this extraction (see apache/pinot#18434 for the original row-major 
refactor): one branch per
+ * {@link ArrowType.ArrowTypeID}, with complex types recursing through their 
child {@link Field}s
+ * and scalars normalising per Pinot's expected JDK types.
+ *
+ * <p>Reused by:
+ * <ul>
+ *   <li>{@link ArrowRecordExtractor} — row-major ingestion via {@code 
ArrowRecordReader}
+ *   <li>Column-major {@code ColumnReader} implementations that wrap Arrow 
vectors and need to
+ *       emit Pinot-canonical JDK types from {@code getValue() / next()}
+ * </ul>
+ *
+ * <p>All conversion methods are static; the only per-extraction state is the
+ * {@code extractRawTimeValues} flag, passed through as a method parameter.
+ */
+public final class ArrowToPinotTypeConverter {
+
+  private ArrowToPinotTypeConverter() {
+  }
+
+  /**
+   * Convert an Arrow vector value to its Pinot-canonical JDK representation.
+   *
+   * @param field the Arrow {@link Field} describing the value's type
+   * @param value the raw value emitted by {@code FieldVector.getObject(docId)}
+   * @param extractRawTimeValues when {@code true}, {@code Date} / {@code 
Time} / {@code Timestamp}
+   *                             surface as raw {@code int} / {@code long} in 
the schema's
+   *                             {@link 
org.apache.arrow.vector.types.TimeUnit} instead of the
+   *                             corresponding {@code java.time} / {@link 
Timestamp} contract type
+   * @return the Pinot-canonical value, or {@code null} for {@link 
ArrowType.Null}
+   */
+  @Nullable
+  public static Object toPinotValue(Field field, Object value, boolean 
extractRawTimeValues) {
+    ArrowType type = field.getType();
+    switch (type.getTypeID()) {
+      // Pass-through — Arrow boxes these directly into the contract output 
type.
+      case Bool:            // Boolean
+      case FloatingPoint:   // Float / Double
+      case Decimal:         // BigDecimal
+      case Binary:          // byte[]
+      case LargeBinary:     // byte[]
+      case FixedSizeBinary: // byte[]
+        return value;
+      // toString — `Utf8` / `LargeUtf8` produce `String`; `Interval` / 
`Duration` produce ISO-8601
+      // (`java.time.Period` / `java.time.Duration` / `PeriodDuration` all 
have meaningful toString).
+      case Utf8:
+      case LargeUtf8:
+      case Interval:
+      case Duration:
+        return value.toString();
+      // Integer — `Byte` widens to `Integer` per contract (sign-extended for 
signed `TinyIntVector`,
+      // zero-extended via `& 0xFF` for unsigned `UInt1Vector`); `Short` 
(signed `SmallIntVector`)
+      // sign-extends; `Character` (unsigned 16, from `UInt2Vector`) widens to 
its `int` code point;
+      // `Integer` / `Long` pass through.
+      case Int:
+        if (value instanceof Byte) {
+          int v = (Byte) value;
+          return ((ArrowType.Int) type).getIsSigned() ? v : v & 0xFF;
+        }
+        if (value instanceof Short) {
+          return ((Short) value).intValue();
+        }
+        if (value instanceof Character) {
+          return (int) (Character) value;
+        }
+        return value;
+      // Null — NullVector.getObject always returns null; callers should 
short-circuit on null,
+      // so this branch is unreachable in practice. Defensive return.
+      case Null:
+        return null;
+      // Logical temporal — schema's `TimeUnit` drives the conversion.
+      case Timestamp:
+        return convertTimestamp((ArrowType.Timestamp) type, value, 
extractRawTimeValues);
+      case Date:
+        return convertDate((ArrowType.Date) type, value, extractRawTimeValues);
+      case Time:
+        return convertTime((ArrowType.Time) type, value, extractRawTimeValues);
+      // Multi-value — `List` (and primitive-array lists) → `Object[]`.
+      case List:
+      case LargeList:
+      case FixedSizeList:
+        return convertList(field.getChildren().get(0), (List<?>) value, 
extractRawTimeValues);
+      // Map / nested complex types.
+      case Map:
+        // The Map field's children are [entriesStruct]; the entries struct's 
children are
+        // [keyField, valueField] (named per MapVector.KEY_NAME / VALUE_NAME).
+        Field entriesField = field.getChildren().get(0);
+        return convertMap(entriesField.getChildren().get(0), 
entriesField.getChildren().get(1),
+            (List<?>) value, extractRawTimeValues);
+      case Struct:
+        return convertStruct(field.getChildren(), (Map<?, ?>) value, 
extractRawTimeValues);
+      case Union:
+        // The chosen branch isn't visible from the resolved value alone — 
dispatch by the value's
+        // runtime Java type. Nested complex sub-branches fall back to 
`value.toString()`.
+        return convertByRuntimeType(value);
+      default:
+        // `NONE` is a placeholder; any other ID is a future Arrow addition.
+        throw new IllegalStateException("Unsupported Arrow type: " + type + " 
for field: " + field.getName());
+    }
+  }
+
+  /// Constructs a [Timestamp] from an Arrow `Timestamp` value. No-TZ vectors 
surface as
+  /// `LocalDateTime` (interpreted as UTC); with-TZ vectors surface as `Long` 
epoch counted in the
+  /// schema's `TimeUnit`. Sub-millisecond precision is preserved via 
[TimestampUtils].
+  /// With `extractRawTimeValues` the raw `long` epoch in the schema's 
`TimeUnit` is returned.
+  private static Object convertTimestamp(ArrowType.Timestamp type, Object 
value, boolean extractRawTimeValues) {
+    if (extractRawTimeValues) {
+      if (value instanceof LocalDateTime) {
+        // No-TZ vector — convert the LocalDateTime back to an epoch `long` in 
the declared unit.
+        Instant instant = ((LocalDateTime) value).toInstant(ZoneOffset.UTC);
+        return toEpochInUnit(instant, type.getUnit());
+      }
+      // With-TZ vector — already raw `long` in the declared unit.
+      return value;
+    }
+    if (value instanceof LocalDateTime) {
+      return Timestamp.from(((LocalDateTime) value).toInstant(ZoneOffset.UTC));
+    }
+    long raw = ((Number) value).longValue();
+    switch (type.getUnit()) {
+      case SECOND:
+        return new Timestamp(raw * 1000L);
+      case MILLISECOND:
+        return new Timestamp(raw);
+      case MICROSECOND:
+        return TimestampUtils.fromMicrosSinceEpoch(raw);
+      case NANOSECOND:
+        return TimestampUtils.fromNanosSinceEpoch(raw);
+      default:
+        throw new IllegalStateException("Unsupported Timestamp unit: " + 
type.getUnit());
+    }
+  }
+
+  private static long toEpochInUnit(Instant instant, 
org.apache.arrow.vector.types.TimeUnit unit) {
+    switch (unit) {
+      case SECOND:
+        return instant.getEpochSecond();
+      case MILLISECOND:
+        return instant.toEpochMilli();
+      case MICROSECOND:
+        return Math.addExact(Math.multiplyExact(instant.getEpochSecond(), 
1_000_000L), instant.getNano() / 1_000L);
+      case NANOSECOND:
+        return Math.addExact(Math.multiplyExact(instant.getEpochSecond(), 
1_000_000_000L), instant.getNano());
+      default:
+        throw new IllegalStateException("Unsupported Timestamp unit: " + unit);
+    }
+  }
+
+  /// Reduces an Arrow `Date` value to its contract Java type ([LocalDate]), 
or to `int`
+  /// days-since-epoch when `extractRawTimeValues` is set. `DateDayVector` 
surfaces as `Integer`
+  /// raw days; `DateMilliVector` surfaces as `LocalDateTime` at UTC midnight.
+  private static Object convertDate(ArrowType.Date type, Object value, boolean 
extractRawTimeValues) {
+    int days;
+    switch (type.getUnit()) {
+      case DAY:
+        days = (Integer) value;
+        break;
+      case MILLISECOND:
+        days = (int) ((LocalDateTime) value).toLocalDate().toEpochDay();
+        break;
+      default:
+        throw new IllegalStateException("Unsupported Date unit: " + 
type.getUnit());
+    }
+    return extractRawTimeValues ? days : LocalDate.ofEpochDay(days);
+  }
+
+  /// Constructs a [LocalTime] from an Arrow `Time` value, dispatched by the 
schema's `TimeUnit`:
+  /// `TimeMilliVector` surfaces as `LocalDateTime`; `TimeSecVector` as 
`Integer`;
+  /// `TimeMicroVector` / `TimeNanoVector` as `Long`. All collapse onto 
nanoseconds-since-midnight.
+  /// With `extractRawTimeValues` the raw count in the schema's `TimeUnit` is 
returned instead.
+  private static Object convertTime(ArrowType.Time type, Object value, boolean 
extractRawTimeValues) {
+    if (extractRawTimeValues) {
+      if (value instanceof LocalDateTime) {
+        // `TimeMilliVector` surfaces as `LocalDateTime`; raw is `int` ms 
since midnight.
+        return (int) (((LocalDateTime) value).toLocalTime().toNanoOfDay() / 
1_000_000L);
+      }
+      // `TimeSecVector` (Integer) / `TimeMicroVector` / `TimeNanoVector` 
(Long) — already raw.
+      return value;
+    }
+    if (value instanceof LocalDateTime) {
+      return ((LocalDateTime) value).toLocalTime();
+    }
+    long raw = ((Number) value).longValue();
+    switch (type.getUnit()) {
+      case SECOND:
+        return LocalTime.ofSecondOfDay(raw);
+      case MILLISECOND:
+        return LocalTime.ofNanoOfDay(raw * 1_000_000L);
+      case MICROSECOND:
+        return LocalTime.ofNanoOfDay(raw * 1_000L);
+      case NANOSECOND:
+        return LocalTime.ofNanoOfDay(raw);
+      default:
+        throw new IllegalStateException("Unsupported Time unit: " + 
type.getUnit());
+    }
+  }
+
+  private static Object[] convertList(Field elementField, List<?> list, 
boolean extractRawTimeValues) {
+    int size = list.size();
+    Object[] result = new Object[size];
+    int i = 0;
+    for (Object element : list) {
+      result[i++] = element != null ? toPinotValue(elementField, element, 
extractRawTimeValues) : null;
+    }
+    return result;
+  }
+
+  /// Flattens an Arrow `Map` column's entry list (`List<Map<KEY_NAME, 
VALUE_NAME>>`) into a
+  /// `Map<String, Object>`, recursing into each value via [#toPinotValue] and 
stringifying each
+  /// key via [BaseRecordExtractor#stringifyMapKey] per the contract. Entries 
with a `null` key
+  /// (input or post-conversion) are dropped.
+  private static Map<String, Object> convertMap(Field keyField, Field 
valueField, List<?> entries,
+      boolean extractRawTimeValues) {
+    Map<String, Object> result = 
Maps.newLinkedHashMapWithExpectedSize(entries.size());
+    for (Object entry : entries) {
+      if (entry == null) {
+        continue;
+      }
+      Map<?, ?> entryMap = (Map<?, ?>) entry;
+      Object rawKey = entryMap.get(MapVector.KEY_NAME);
+      if (rawKey == null) {
+        continue;
+      }
+      Object convertedKey = toPinotValue(keyField, rawKey, 
extractRawTimeValues);
+      if (convertedKey == null) {
+        continue;
+      }
+      Object rawValue = entryMap.get(MapVector.VALUE_NAME);
+      result.put(BaseRecordExtractor.stringifyMapKey(convertedKey),
+          rawValue != null ? toPinotValue(valueField, rawValue, 
extractRawTimeValues) : null);
+    }
+    return result;
+  }
+
+  private static Map<String, Object> convertStruct(List<Field> childFields, 
Map<?, ?> value,
+      boolean extractRawTimeValues) {
+    Map<String, Object> result = 
Maps.newHashMapWithExpectedSize(childFields.size());
+    for (Field childField : childFields) {
+      String name = childField.getName();
+      Object rawValue = value.get(name);
+      result.put(name, rawValue != null ? toPinotValue(childField, rawValue, 
extractRawTimeValues) : null);
+    }
+    return result;
+  }
+
+  /// Runtime-type dispatch used by the `Union` case (where the chosen branch 
isn't accessible
+  /// from the resolved value). Mirrors the scalar handling of [#toPinotValue] 
for the common
+  /// Arrow boxed types; nested complex types fall back to `value.toString()` 
because their child
+  /// [Field]s aren't reachable from here.
+  private static Object convertByRuntimeType(Object value) {
+    if (value instanceof Number) {
+      if (value instanceof Byte || value instanceof Short) {
+        return ((Number) value).intValue();
+      }
+      return value;
+    }
+    if (value instanceof Boolean || value instanceof byte[]) {
+      return value;
+    }
+    if (value instanceof Character) {
+      // `UInt2Vector` surfaces as `Character`; widen to `int` per the Int(16) 
contract.
+      return (int) (Character) value;
+    }
+    if (value instanceof LocalDateTime) {
+      // Ambiguous between Timestamp / Date / Time — best-effort: treat as 
Timestamp UTC.
+      return Timestamp.from(((LocalDateTime) value).toInstant(ZoneOffset.UTC));
+    }
+    // `Text` (Utf8 / LargeUtf8), `Period` / `Duration` / `PeriodDuration` 
(Interval / Duration), and
+    // anything unrecognized fall through to `toString()`.
+    return value.toString();
+  }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to