blob: 5a0bc9f6d342e026f44d657989f0eaa6e5b09611 [file]
/*
* 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.comet.parquet;
import java.io.Closeable;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
import scala.Option;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.arrow.memory.BufferAllocator;
import org.apache.arrow.memory.RootAllocator;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.RecordReader;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.parquet.HadoopReadOptions;
import org.apache.parquet.ParquetReadOptions;
import org.apache.parquet.Preconditions;
import org.apache.parquet.column.ColumnDescriptor;
import org.apache.parquet.column.page.PageReadStore;
import org.apache.parquet.hadoop.metadata.ParquetMetadata;
import org.apache.parquet.schema.MessageType;
import org.apache.parquet.schema.Type;
import org.apache.spark.TaskContext;
import org.apache.spark.TaskContext$;
import org.apache.spark.sql.catalyst.InternalRow;
import org.apache.spark.sql.comet.parquet.CometParquetReadSupport;
import org.apache.spark.sql.comet.shims.ShimTaskMetrics;
import org.apache.spark.sql.execution.datasources.PartitionedFile;
import org.apache.spark.sql.execution.datasources.parquet.ParquetToSparkSchemaConverter;
import org.apache.spark.sql.execution.metric.SQLMetric;
import org.apache.spark.sql.types.*;
import org.apache.spark.sql.vectorized.ColumnarBatch;
import org.apache.spark.util.AccumulatorV2;
import org.apache.comet.CometConf;
import org.apache.comet.CometSchemaImporter;
import org.apache.comet.IcebergApi;
import org.apache.comet.shims.ShimBatchReader;
import org.apache.comet.shims.ShimFileFormat;
import org.apache.comet.vector.CometVector;
/**
* A vectorized Parquet reader that reads a Parquet file in a batched fashion.
*
* <p>Example of how to use this:
*
* <pre>
* BatchReader reader = new BatchReader(parquetFile, batchSize);
* try {
* reader.init();
* while (reader.readBatch()) {
* ColumnarBatch batch = reader.currentBatch();
* // consume the batch
* }
* } finally { // resources associated with the reader should be released
* reader.close();
* }
* </pre>
*
* @deprecated since 0.14.0. This class is kept for Iceberg compatibility only.
*/
@Deprecated
@IcebergApi
public class BatchReader extends RecordReader<Void, ColumnarBatch> implements Closeable {
private static final Logger LOG = LoggerFactory.getLogger(FileReader.class);
protected static final BufferAllocator ALLOCATOR = new RootAllocator();
private Configuration conf;
private int capacity;
private boolean isCaseSensitive;
private boolean useFieldId;
private boolean ignoreMissingIds;
private StructType partitionSchema;
private InternalRow partitionValues;
private PartitionedFile file;
protected Map<String, SQLMetric> metrics;
private long rowsRead;
protected StructType sparkSchema;
private MessageType requestedSchema;
protected CometVector[] vectors;
protected AbstractColumnReader[] columnReaders;
private CometSchemaImporter importer;
protected ColumnarBatch currentBatch;
private FileReader fileReader;
private boolean[] missingColumns;
protected boolean isInitialized;
private ParquetMetadata footer;
/** The total number of rows across all row groups of the input split. */
private long totalRowCount;
/**
* The total number of rows loaded so far, including all the rows from row groups that we've
* processed and the current row group.
*/
private long totalRowsLoaded;
/**
* Whether the native scan should always return decimal represented by 128 bits, regardless of its
* precision. Normally, this should be true if native execution is enabled, since Arrow compute
* kernels doesn't support 32 and 64 bit decimals yet.
*/
private boolean useDecimal128;
/** Whether to use the lazy materialization reader for reading columns. */
private boolean useLazyMaterialization;
/**
* Whether to return dates/timestamps that were written with legacy hybrid (Julian + Gregorian)
* calendar as it is. If this is true, Comet will return them as it is, instead of rebasing them
* to the new Proleptic Gregorian calendar. If this is false, Comet will throw exceptions when
* seeing these dates/timestamps.
*/
private boolean useLegacyDateTimestamp;
/** The TaskContext object for executing this task. */
private TaskContext taskContext;
public BatchReader() {}
// Only for testing
public BatchReader(String file, int capacity) {
this(file, capacity, null, null);
}
// Only for testing
public BatchReader(
String file, int capacity, StructType partitionSchema, InternalRow partitionValues) {
this(new Configuration(), file, capacity, partitionSchema, partitionValues);
}
// Only for testing
public BatchReader(
Configuration conf,
String file,
int capacity,
StructType partitionSchema,
InternalRow partitionValues) {
conf.set("spark.sql.parquet.binaryAsString", "false");
conf.set("spark.sql.parquet.int96AsTimestamp", "false");
conf.set("spark.sql.caseSensitive", "false");
conf.set("spark.sql.parquet.inferTimestampNTZ.enabled", "true");
conf.set("spark.sql.legacy.parquet.nanosAsLong", "false");
this.conf = conf;
this.capacity = capacity;
this.isCaseSensitive = false;
this.useFieldId = false;
this.ignoreMissingIds = false;
this.partitionSchema = partitionSchema;
this.partitionValues = partitionValues;
this.file = ShimBatchReader.newPartitionedFile(partitionValues, file);
this.metrics = new HashMap<>();
this.taskContext = TaskContext$.MODULE$.get();
}
/**
* @see <a href="https://github.com/apache/datafusion-comet/issues/2079">Comet Issue #2079</a>
*/
@IcebergApi
public BatchReader(AbstractColumnReader[] columnReaders) {
// Todo: set useDecimal128 and useLazyMaterialization
int numColumns = columnReaders.length;
this.columnReaders = new AbstractColumnReader[numColumns];
vectors = new CometVector[numColumns];
currentBatch = new ColumnarBatch(vectors);
// This constructor is used by Iceberg only. The columnReaders are
// initialized in Iceberg, so no need to call the init()
isInitialized = true;
this.taskContext = TaskContext$.MODULE$.get();
this.metrics = new HashMap<>();
}
BatchReader(
Configuration conf,
PartitionedFile inputSplit,
ParquetMetadata footer,
int capacity,
StructType sparkSchema,
boolean isCaseSensitive,
boolean useFieldId,
boolean ignoreMissingIds,
boolean useLegacyDateTimestamp,
StructType partitionSchema,
InternalRow partitionValues,
Map<String, SQLMetric> metrics) {
this.conf = conf;
this.capacity = capacity;
this.sparkSchema = sparkSchema;
this.isCaseSensitive = isCaseSensitive;
this.useFieldId = useFieldId;
this.ignoreMissingIds = ignoreMissingIds;
this.useLegacyDateTimestamp = useLegacyDateTimestamp;
this.partitionSchema = partitionSchema;
this.partitionValues = partitionValues;
this.file = inputSplit;
this.footer = footer;
this.metrics = metrics;
this.taskContext = TaskContext$.MODULE$.get();
}
/**
* Initialize this reader. The reason we don't do it in the constructor is that we want to close
* any resource hold by this reader when error happens during the initialization.
*/
public void init() throws URISyntaxException, IOException {
useDecimal128 =
conf.getBoolean(
CometConf.COMET_USE_DECIMAL_128().key(),
(Boolean) CometConf.COMET_USE_DECIMAL_128().defaultValue().get());
useLazyMaterialization =
conf.getBoolean(
CometConf.COMET_USE_LAZY_MATERIALIZATION().key(),
(Boolean) CometConf.COMET_USE_LAZY_MATERIALIZATION().defaultValue().get());
long start = file.start();
long length = file.length();
String filePath = file.filePath().toString();
ParquetReadOptions.Builder builder = HadoopReadOptions.builder(conf, new Path(filePath));
if (start >= 0 && length >= 0) {
builder = builder.withRange(start, start + length);
}
ParquetReadOptions readOptions = builder.build();
// TODO: enable off-heap buffer when they are ready
ReadOptions cometReadOptions = ReadOptions.builder(conf).build();
Path path = new Path(new URI(filePath));
fileReader =
new FileReader(
CometInputFile.fromPath(path, conf), footer, readOptions, cometReadOptions, metrics);
requestedSchema = fileReader.getFileMetaData().getSchema();
MessageType fileSchema = requestedSchema;
if (sparkSchema == null) {
sparkSchema = new ParquetToSparkSchemaConverter(conf).convert(requestedSchema);
} else {
requestedSchema =
CometParquetReadSupport.clipParquetSchema(
requestedSchema, sparkSchema, isCaseSensitive, useFieldId, ignoreMissingIds);
if (requestedSchema.getFieldCount() != sparkSchema.size()) {
throw new IllegalArgumentException(
String.format(
"Spark schema has %d columns while " + "Parquet schema has %d columns",
sparkSchema.size(), requestedSchema.getColumns().size()));
}
}
totalRowCount = fileReader.getRecordCount();
List<ColumnDescriptor> columns = requestedSchema.getColumns();
int numColumns = columns.size();
if (partitionSchema != null) numColumns += partitionSchema.size();
columnReaders = new AbstractColumnReader[numColumns];
// Initialize missing columns and use null vectors for them
missingColumns = new boolean[columns.size()];
List<String[]> paths = requestedSchema.getPaths();
// We do not need the column index of the row index; but this method has the
// side effect of throwing an exception if a column with the same name is
// found which we do want (spark unit tests explicitly test for that).
ShimFileFormat.findRowIndexColumnIndexInSchema(sparkSchema);
StructField[] nonPartitionFields = sparkSchema.fields();
for (int i = 0; i < requestedSchema.getFieldCount(); i++) {
Type t = requestedSchema.getFields().get(i);
Preconditions.checkState(
t.isPrimitive() && !t.isRepetition(Type.Repetition.REPEATED),
"Complex type is not supported");
String[] colPath = paths.get(i);
if (nonPartitionFields[i].name().equals(ShimFileFormat.ROW_INDEX_TEMPORARY_COLUMN_NAME())) {
// Values of ROW_INDEX_TEMPORARY_COLUMN_NAME column are always populated with
// generated row indexes, rather than read from the file.
// TODO(SPARK-40059): Allow users to include columns named
// FileFormat.ROW_INDEX_TEMPORARY_COLUMN_NAME in their schemas.
long[] rowIndices = fileReader.getRowIndices();
columnReaders[i] = new RowIndexColumnReader(nonPartitionFields[i], capacity, rowIndices);
missingColumns[i] = true;
} else if (fileSchema.containsPath(colPath)) {
ColumnDescriptor fd = fileSchema.getColumnDescription(colPath);
if (!fd.equals(columns.get(i))) {
throw new UnsupportedOperationException("Schema evolution is not supported");
}
missingColumns[i] = false;
} else {
if (columns.get(i).getMaxDefinitionLevel() == 0) {
throw new IOException(
"Required column '"
+ Arrays.toString(colPath)
+ "' is missing"
+ " in data file "
+ filePath);
}
ConstantColumnReader reader =
new ConstantColumnReader(nonPartitionFields[i], capacity, useDecimal128);
columnReaders[i] = reader;
missingColumns[i] = true;
}
}
// Initialize constant readers for partition columns
if (partitionSchema != null) {
StructField[] partitionFields = partitionSchema.fields();
for (int i = columns.size(); i < columnReaders.length; i++) {
int fieldIndex = i - columns.size();
StructField field = partitionFields[fieldIndex];
ConstantColumnReader reader =
new ConstantColumnReader(field, capacity, partitionValues, fieldIndex, useDecimal128);
columnReaders[i] = reader;
}
}
vectors = new CometVector[numColumns];
currentBatch = new ColumnarBatch(vectors);
fileReader.setRequestedSchema(requestedSchema.getColumns());
// For test purpose only
// If the last external accumulator is `NumRowGroupsAccumulator`, the row group number to read
// will be updated to the accumulator. So we can check if the row groups are filtered or not
// in test case.
// Note that this tries to get thread local TaskContext object, if this is called at other
// thread, it won't update the accumulator.
if (taskContext != null) {
Option<AccumulatorV2<?, ?>> accu =
ShimTaskMetrics.getTaskAccumulator(taskContext.taskMetrics());
if (accu.isDefined() && accu.get().getClass().getSimpleName().equals("NumRowGroupsAcc")) {
@SuppressWarnings("unchecked")
AccumulatorV2<Integer, Integer> intAccum = (AccumulatorV2<Integer, Integer>) accu.get();
intAccum.add(fileReader.getRowGroups().size());
}
}
isInitialized = true;
}
/**
* @see <a href="https://github.com/apache/datafusion-comet/issues/2079">Comet Issue #2079</a>
*/
@IcebergApi
public void setSparkSchema(StructType schema) {
this.sparkSchema = schema;
}
/**
* @see <a href="https://github.com/apache/datafusion-comet/issues/2079">Comet Issue #2079</a>
*/
@IcebergApi
public AbstractColumnReader[] getColumnReaders() {
return columnReaders;
}
@Override
public void initialize(InputSplit inputSplit, TaskAttemptContext taskAttemptContext)
throws IOException, InterruptedException {
// Do nothing. The initialization work is done in 'init' already.
}
@Override
public boolean nextKeyValue() throws IOException {
return nextBatch();
}
@Override
public Void getCurrentKey() {
return null;
}
@Override
public ColumnarBatch getCurrentValue() {
return currentBatch();
}
@Override
public float getProgress() {
return (float) rowsRead / totalRowCount;
}
/**
* Returns the current columnar batch being read.
*
* <p>Note that this must be called AFTER {@link BatchReader#nextBatch()}.
*/
public ColumnarBatch currentBatch() {
return currentBatch;
}
/**
* Loads the next batch of rows.
*
* @return true if there are no more rows to read, false otherwise.
*/
public boolean nextBatch() throws IOException {
Preconditions.checkState(isInitialized, "init() should be called first!");
if (rowsRead >= totalRowCount) return false;
boolean hasMore;
try {
hasMore = loadNextRowGroupIfNecessary();
} catch (RuntimeException e) {
// Spark will check certain exception e.g. `SchemaColumnConvertNotSupportedException`.
throw e;
} catch (Throwable e) {
throw new IOException(e);
}
if (!hasMore) return false;
int batchSize = (int) Math.min(capacity, totalRowsLoaded - rowsRead);
return nextBatch(batchSize);
}
@IcebergApi
public boolean nextBatch(int batchSize) {
long totalDecodeTime = 0, totalLoadTime = 0;
for (int i = 0; i < columnReaders.length; i++) {
AbstractColumnReader reader = columnReaders[i];
long startNs = System.nanoTime();
reader.readBatch(batchSize);
totalDecodeTime += System.nanoTime() - startNs;
startNs = System.nanoTime();
vectors[i] = reader.currentBatch();
totalLoadTime += System.nanoTime() - startNs;
}
SQLMetric decodeMetric = metrics.get("ParquetNativeDecodeTime");
if (decodeMetric != null) {
decodeMetric.add(totalDecodeTime);
}
SQLMetric loadMetric = metrics.get("ParquetNativeLoadTime");
if (loadMetric != null) {
loadMetric.add(totalLoadTime);
}
currentBatch.setNumRows(batchSize);
rowsRead += batchSize;
return true;
}
@IcebergApi
@Override
public void close() throws IOException {
if (columnReaders != null) {
for (AbstractColumnReader reader : columnReaders) {
if (reader != null) {
reader.close();
}
}
}
if (fileReader != null) {
fileReader.close();
fileReader = null;
}
if (importer != null) {
importer.close();
importer = null;
}
}
private boolean loadNextRowGroupIfNecessary() throws Throwable {
// More rows can be read from loaded row group. No need to load next one.
if (rowsRead != totalRowsLoaded) return true;
SQLMetric rowGroupTimeMetric = metrics.get("ParquetLoadRowGroupTime");
SQLMetric numRowGroupsMetric = metrics.get("ParquetRowGroups");
long startNs = System.nanoTime();
PageReadStore rowGroupReader = fileReader.readNextRowGroup();
if (rowGroupTimeMetric != null) {
rowGroupTimeMetric.add(System.nanoTime() - startNs);
}
if (rowGroupReader == null) {
return false;
}
if (numRowGroupsMetric != null) {
numRowGroupsMetric.add(1);
}
if (importer != null) importer.close();
importer = new CometSchemaImporter(ALLOCATOR);
List<ColumnDescriptor> columns = requestedSchema.getColumns();
for (int i = 0; i < columns.size(); i++) {
if (missingColumns[i]) continue;
if (columnReaders[i] != null) columnReaders[i].close();
// TODO: handle tz, datetime & int96 rebase
// TODO: consider passing page reader via ctor - however we need to fix the shading issue
// from Iceberg side.
DataType dataType = sparkSchema.fields()[i].dataType();
ColumnReader reader =
Utils.getColumnReader(
dataType,
columns.get(i),
importer,
capacity,
useDecimal128,
useLazyMaterialization,
useLegacyDateTimestamp);
reader.setPageReader(rowGroupReader.getPageReader(columns.get(i)));
columnReaders[i] = reader;
}
totalRowsLoaded += rowGroupReader.getRowCount();
return true;
}
}