Typed Accessors¶
RowReader and the nested PqStruct / PqList / PqMap flyweights decode each column to its
logical-type Java representation through typed accessor methods. For the task-oriented walkthrough, see
Read Row by Row.
Try it yourself
Want to run it or explore the capabilities yourself? The Typed Accessors example decodes dates, times, UTC and local timestamps, decimals, UUIDs, intervals, FLOAT16, and JSON/BSON to their natural Java types.
Accessor type mapping¶
All accessors are available in two forms — name-based (getInt("column_name")) and index-based
(getInt(fieldIndex)); see Index-based access.
| Method | Physical type | Logical type | Java type |
|---|---|---|---|
getBoolean |
BOOLEAN |
none | boolean |
getInt |
INT32 |
any | int |
getLong |
INT64 |
any | long |
getFloat |
FLOATFIXED_LEN_BYTE_ARRAY(2) |
noneFLOAT16 |
float |
getDouble |
DOUBLE |
none | double |
getBinary |
BYTE_ARRAYFIXED_LEN_BYTE_ARRAYINT96 |
any | byte[] |
getString |
BYTE_ARRAY |
STRING, ENUM, JSON, or none |
String |
getDate |
INT32 |
DATE |
LocalDate |
getTime |
INT32INT64 |
TIME |
LocalTime |
getTimestamp |
INT64FIXED_LEN_BYTE_ARRAY(12)INT96 |
TIMESTAMP(isAdjustedToUTC = true); none on INT96 |
Instant |
getLocalTimestamp |
INT64FIXED_LEN_BYTE_ARRAY(12) |
TIMESTAMP(isAdjustedToUTC = false) |
LocalDateTime |
getDecimal |
INT32INT64BYTE_ARRAYFIXED_LEN_BYTE_ARRAY |
DECIMAL |
BigDecimal |
getUuid |
FIXED_LEN_BYTE_ARRAY(16) |
UUID |
UUID |
getInterval |
FIXED_LEN_BYTE_ARRAY(12) |
INTERVAL |
PqInterval |
getStruct |
group | none | PqStruct |
getList |
group | LIST |
PqList |
getMap |
group | MAP |
PqMap |
getVariant |
group of two BYTE_ARRAY |
VARIANT |
PqVariant |
isNull |
any | any | boolean |
PqInterval is a record with three long components, months(), days() and milliseconds(),
each an unsigned 32-bit value in the range [0, 4_294_967_295]. The components are independent and
not normalized.
Physical accessors¶
getInt, getLong and getBinary read the stored value of any column their physical type
holds, whatever it is annotated with. getInt reads a DATE, a TIME(MILLIS) and an INT32
DECIMAL as well as a bare INT32; getLong reads an INT64 TIMESTAMP, a TIME(MICROS) or
TIME(NANOS) and an INT64 DECIMAL; getBinary reads every BYTE_ARRAY,
FIXED_LEN_BYTE_ARRAY and INT96 column, including a DECIMAL, a UUID, an INTERVAL, a
FLOAT16, a TIMESTAMP over FIXED_LEN_BYTE_ARRAY(12), a BSON and a GEOMETRY or GEOGRAPHY
payload. Use them to skip the logical-type
decode — see Reading the physical value.
getValue on a signed INT(8) or INT(16) column returns a Byte or a Short. A file can
store a value outside the range its annotation states: getValue keeps the low 8 or 16 bits of
such a value, so a stored 1000 reads as the Byte -24, while getInt returns it as stored.
On an unsigned INT(8) or INT(16) column getValue returns the stored Integer. Filter
predicates compare the value getInt returns.
Generic accessors¶
When the column type is not known ahead of time, the generic accessors return values decoded to their logical-type representation:
RowReader.getValue(name)/getValue(index):Integer/Long/String/LocalDate/LocalTime/Instant/BigDecimal/UUID/PqInterval/PqVariant/ nestedPqStruct/PqList/PqMap, withbyte[]for un-annotatedBYTE_ARRAY/FIXED_LEN_BYTE_ARRAYcolumns.PqStruct.getValue(name): same decoded mapping for nested struct fields.PqMap.Entry.getKey()/getValue(): same decoded mapping for map keys and values.PqList.get(index)/PqList.values(): same decoded mapping for list elements.
The generic accessors, PqList.iterator included, return an un-annotated BYTE_ARRAY as
byte[], since it may hold arbitrary binary payloads (Protobuf, WKB, custom encodings); call
getString when the column is known to hold UTF-8 text from an older writer that omitted the
STRING annotation.
A parallel getRawValue family (RowReader.getRawValue, PqStruct.getRawValue,
PqMap.Entry.getRawKey / getRawValue, PqList.getRaw / rawValues) returns the boxed physical
value. Nested groups (struct / list / map / variant) have no distinct raw form and are returned
through their typed flyweight (PqStruct / PqList / PqMap / PqVariant) in both modes.
Typed accessors on PqList and PqMap.Entry¶
PqList has strings() / dates() / times() / timestamps() / decimals() / uuids() /
intervals() / floats() / booleans(), each returning List<T>, and PqMap.Entry the matching
getStringValue() / getDateValue() / getIntervalValue() / etc., which avoid the boxed Object
return of getValue().
PqMap.Entry's typed key accessors are getStringKey() / getIntKey() / getLongKey() /
getBinaryKey(). Other key types (DATE / TIME / TIMESTAMP / DECIMAL / UUID) are read through
getKey() (decoded) and getRawKey() (raw).
PqList.ints() / longs() / doubles() return PqIntList / PqLongList / PqDoubleList, which
expose PrimitiveIterator.OfInt / OfLong / OfDouble, int get(int), and int[] toArray()
without boxing. For nested list<list<int>> (or <long> / <double>), iterate the outer list via
lists() and call ints() / longs() / doubles() on each inner PqList.
Text columns¶
getString reads a column whose stored bytes are the UTF-8 encoding of a string: a
BYTE_ARRAY annotated STRING, ENUM or JSON, or one carrying no annotation, which older
writers used for text before the annotation existed. On any other column — a DECIMAL, a
UUID, an INTERVAL, a FLOAT16, a BSON, a GEOMETRY or GEOGRAPHY, an INT96, a
fixed-width FIXED_LEN_BYTE_ARRAY, or a column not held as bytes at all — it throws
IllegalArgumentException naming the column and its type; read those through getBinary or the
accessor for their annotation.
The same columns are the ones a String filter literal applies to; see
Query Controls. The rule holds for every string accessor:
getString on RowReader and PqStruct, strings() on PqList, getStringKey /
getStringValue on PqMap.Entry, and ColumnReader.getStrings.
Where the refusal falls depends on the accessor. The per-value ones — getString,
getStringKey, getStringValue — answer the null test first, so a null value reads as null
whatever its column holds and the refusal comes on the first present value. PqList.strings()
and ColumnReader.getStrings() ask about the column once for a whole run of values, and refuse
one that does not hold text whatever its values are.
Null handling¶
Primitive accessors (getInt, getLong, getFloat, getDouble, getBoolean) throw
NullPointerException if the field is null — always check isNull() first. Object accessors
(getString, getDate, getTimestamp, getLocalTimestamp, getDecimal, getUuid,
getInterval, getStruct, getList, getMap) return null for null fields.
A column annotated with the NULL logical type (e.g. PyArrow's pa.null()) holds a null value at
every row: column.logicalType() returns LogicalType.NullType, isNull(name) is always true,
and the object accessors return null.
Type mismatches¶
Requesting the wrong type for a column (e.g. getInt on a LONG column, getDate on a STRING
column) is a programming error; the call fails at runtime with an unchecked exception. The
specific exception type is unspecified and may change between releases — do not catch it as part of
normal control flow. Reading a column that does not hold text as a string is specified: the string
accessors listed under Text columns throw IllegalArgumentException, as their
@throws records. If the column type isn't known statically, check it up front via
reader.getFileSchema().getColumn(name) and inspect the returned ColumnSchema's type() /
logicalType() — see Inspect File Metadata.
A column whose annotation its physical type cannot carry is not an error. FLOAT16 is defined as
a two-byte payload, so a FLOAT16 column that declares three bytes is invalid. So are a
TIME(MILLIS) or an INT(8) on an INT64, and a DECIMAL(12, 2) on an INT32, which holds at
most nine digits. The format tells readers to ignore such an annotation rather than reject the
file, so Hardwood drops it, and drops one it does not recognize at all. The column is then
reported and read as its physical type.
getFileSchema() reports no logical type for it, getValue returns the physical value, and the
physical accessors work. A logical accessor fails as it would on any unannotated column of that
type. Its min / max statistics prune nothing (see
When statistics are ignored), and a logical-type
predicate on the column is rejected at reader creation.
The getTimestamp / getLocalTimestamp pair is split along the column's isAdjustedToUTC flag:
getTimestamp requires isAdjustedToUTC = true and returns Instant; getLocalTimestamp
requires isAdjustedToUTC = false and returns LocalDateTime. Calling the wrong one for a column
throws IllegalStateException naming the column and the actual flag value. If the kind isn't known
statically, branch on ((LogicalType.TimestampType) column.logicalType()).isAdjustedToUTC() before
the accessor call, or use the generic getValue accessor, which returns Instant or
LocalDateTime per the column's flag. For why the pair is split, see
Timestamp Semantics.
The TIME logical type also carries an isAdjustedToUTC flag, but LocalTime has no zone of its
own, so getTime returns LocalTime either way and the flag is informational — inspect
((LogicalType.TimeType) column.logicalType()).isAdjustedToUTC() if the distinction matters to
your application.
FLOAT16 columns¶
getFloat accepts FLOAT16 columns (FIXED_LEN_BYTE_ARRAY(2) annotated with the FLOAT16 logical
type) and decodes the 2-byte IEEE 754 half-precision payload to a single-precision float. The
widening is lossless — half-precision NaN, ±Infinity, and signed zero round-trip cleanly, and the
original NaN bit pattern is preserved (the Parquet spec does not canonicalize NaNs on write). Use
Float.isNaN(value) for NaN checks rather than equality.
Timestamps over FIXED_LEN_BYTE_ARRAY(12)¶
A TIMESTAMP column is stored in an INT64 or in a FIXED_LEN_BYTE_ARRAY(12). The 12 bytes hold a
signed two's complement little-endian 96-bit count of the annotation's time unit since the Unix
epoch, which spans the years 0001 to 9999 at every unit, nanoseconds included.
getTimestamp, getLocalTimestamp, getValue and the PqList / PqStruct / PqMap flyweights
read both carriers alike and return the same Instant or LocalDateTime. getBinary,
getRawValue and ColumnReader return the 12 stored bytes.
A stored count outside the range of Instant or LocalDateTime, about a billion years either
side of the epoch, throws DateTimeException naming the stored bytes; getBinary reads it.
Legacy INT96 timestamps¶
Parquet files written by older versions of Apache Spark and Hive store timestamps in the deprecated
INT96 physical type without a TIMESTAMP logical type annotation. getTimestamp decodes INT96
to an Instant. The generic
getValue accessor and the PqList / PqStruct / PqMap flyweights return the same Instant, at
the top level and in every nested position.
Legacy converted-type annotations¶
Writers predating the modern logical-type union (older parquet-mr / Hive / Impala / Spark) annotate
primitive columns with only a legacy converted_type and no logicalType. Hardwood promotes each
one to its logical type, so the column decodes through the normal typed accessor with no
caller-side opt-in:
converted_type |
Accessor | Java type |
|---|---|---|
UTF8 |
getString |
String |
JSON |
getString |
String |
ENUM |
getString |
String |
BSON |
getBinary |
byte[] |
DATE |
getDate |
LocalDate |
DECIMAL |
getDecimal |
BigDecimal |
TIME_MILLIS, TIME_MICROS |
getTime |
LocalTime |
TIMESTAMP_MILLIS, TIMESTAMP_MICROS |
getTimestamp |
Instant |
INT_8, INT_16, INT_32, INT_64 |
getValue |
Byte / Short / Integer / Long |
UINT_8, UINT_16, UINT_32, UINT_64 |
getValue |
Integer / Long (raw two's-complement bit pattern) |
INTERVAL |
getInterval |
PqInterval |
TIME_* columns decode to a UTC-normalized LocalTime time-of-day and TIMESTAMP_* columns to a
UTC-normalized Instant, matching the parquet-format backward-compatibility rule for these
annotations. Unsigned columns preserve the stored bit pattern — reinterpret with
Integer.toUnsignedLong / Long.toUnsignedString for the unsigned magnitude. When a file carries
both a converted_type and a modern logicalType, the logicalType takes precedence, except where
the logicalType is UNKNOWN: then the converted_type decides. parquet-java writes INTERVAL
columns in that form, as converted_type = INTERVAL beside logicalType = UNKNOWN, and they read
as INTERVAL.
The MAP group annotation has a legacy form too: some older parquet-mr / Hive / Impala files annotate
only the inner repeated key_value group with MAP_KEY_VALUE and leave the outer group unannotated.
Hardwood recognizes this form as a map, so getMap returns a PqMap and the group's
SchemaNode.GroupNode.isMap() reports true (with logicalType() returning a MapType), exactly as
for the modern MAP annotation.
Legacy list encodings¶
Lists predate the modern LIST-annotated three-level group and appear in older files in two legacy
forms. Hardwood reads both as lists, so getList returns a PqList with no caller-side opt-in:
- Unannotated repeated field. A
REPEATEDfield carrying noLIST/MAPannotation is a required list of required elements whose element type is the field itself. A repeated primitive reads as a list of scalars (getList("foo").ints()); a repeated group reads as a list of structs (getList("foo").structs()). A repeated group whose only child is a repeatedMAP_KEY_VALUEgroup reads as a list of maps (getList("attrs").maps()), andgetFileSchema()reports the group as a map. A repeated group outside aLISTorMAPgroup that carries an annotation of its own, such asMAP_KEY_VALUE,LISTorMAP, reads as the same group without that annotation: Hardwood drops the annotation, logging a warning, andgetFileSchema()reports the group as it would the unannotated one. Such a group annotatedVARIANTkeeps its annotation ingetFileSchema(); building a reader whose projection includes one of its columns raises anUnsupportedOperationExceptionnaming the group, and reads of the other columns succeed. A filter on the group or its columns is rejected as on any repeated column (see Query Controls). - Two-level
LISTgroup. ALIST-annotated group whose repeated child is the element directly, with no intermediatelistwrapper. Hardwood applies the parquet-format backward-compatibility rules to resolve the element: a repeated group with multiple fields, a repeated group namedarrayor<list>_tuple, or a repeated group whose single field is itself repeated is the element struct; otherwise the repeated group's single child is the element.