From 20e3c66793aca0541a060d2caac2ad087dd06908 Mon Sep 17 00:00:00 2001 From: Jakob-al28 Date: Wed, 1 Jul 2026 22:10:36 +0000 Subject: [PATCH 1/2] [SYSTEMDS-3929] Speed up Parquet frame reader/writer Rewrites the Parquet frame reader to read columns via parquet's column API (ColumnReadStoreImpl, ColumnReader) instead of creating a Group object per row. On TPC-H lineitem (~30M rows), read time went from 88.9s to 34.5s (2.6x faster). Also rewrites the writer around a custom WriteSupport, removing the per-row Group allocation, adding INT96 timestamp decoding, and fixing the parallel reader to use the sequential implementation instead of reimplementing row iteration per thread. ParquetWriter batches by buffered size internally, so the old writer's manual batch buffer was redundant and has been removed. Compression, dictionary encoding, and row-group size are benchmarked and configurable. The old Group-based implementations are kept under test scope as a benchmark baseline. Adds tests covering null handling, INT96 decoding, and round trips against public Parquet files. --- .../apache/sysds/parser/DMLTranslator.java | 1 + .../sysds/runtime/io/FrameReaderFactory.java | 2 + .../sysds/runtime/io/FrameReaderParquet.java | 192 ++++++++---- .../io/FrameReaderParquetParallel.java | 56 ++-- .../sysds/runtime/io/FrameWriterFactory.java | 2 + .../sysds/runtime/io/FrameWriterParquet.java | 227 ++++++++++----- .../io/FrameWriterParquetParallel.java | 14 +- .../io/parquet/FrameParquetSchemaTest.java | 71 +---- .../io/parquet/FrameReaderParquetLegacy.java | 186 ++++++++++++ .../parquet/FrameReaderWriterParquetTest.java | 134 +++++++++ .../io/parquet/FrameWriterParquetLegacy.java | 212 ++++++++++++++ .../io/parquet/ParquetReaderBenchmark.java | 187 ++++++++++++ .../io/parquet/ParquetTestUtils.java | 85 ++++++ .../io/parquet/ParquetWriterBenchmark.java | 274 ++++++++++++++++++ .../functions/io/parquet/ReadParquetTest.java | 271 +++++++++++++++++ .../io/parquet/WriteParquetTest.java | 165 +++++++++++ .../resources/datasets/parquet/all.parquet | Bin 0 -> 25613 bytes .../datasets/parquet/alltypes_plain.parquet | Bin 0 -> 1851 bytes .../datasets/parquet/userdata1.parquet | Bin 0 -> 113629 bytes 19 files changed, 1862 insertions(+), 217 deletions(-) create mode 100644 src/test/java/org/apache/sysds/test/functions/io/parquet/FrameReaderParquetLegacy.java create mode 100644 src/test/java/org/apache/sysds/test/functions/io/parquet/FrameReaderWriterParquetTest.java create mode 100755 src/test/java/org/apache/sysds/test/functions/io/parquet/FrameWriterParquetLegacy.java create mode 100644 src/test/java/org/apache/sysds/test/functions/io/parquet/ParquetReaderBenchmark.java create mode 100644 src/test/java/org/apache/sysds/test/functions/io/parquet/ParquetTestUtils.java create mode 100644 src/test/java/org/apache/sysds/test/functions/io/parquet/ParquetWriterBenchmark.java create mode 100644 src/test/java/org/apache/sysds/test/functions/io/parquet/ReadParquetTest.java create mode 100644 src/test/java/org/apache/sysds/test/functions/io/parquet/WriteParquetTest.java create mode 100644 src/test/resources/datasets/parquet/all.parquet create mode 100644 src/test/resources/datasets/parquet/alltypes_plain.parquet create mode 100644 src/test/resources/datasets/parquet/userdata1.parquet diff --git a/src/main/java/org/apache/sysds/parser/DMLTranslator.java b/src/main/java/org/apache/sysds/parser/DMLTranslator.java index a8e1667d049..0db739cc901 100644 --- a/src/main/java/org/apache/sysds/parser/DMLTranslator.java +++ b/src/main/java/org/apache/sysds/parser/DMLTranslator.java @@ -1058,6 +1058,7 @@ public void constructHops(StatementBlock sb) { case LIBSVM: case HDF5: case DELTA: + case PARQUET: // columnar/text formats: no block layout (blocksize -1) ae.setOutputParams(ae.getDim1(), ae.getDim2(), ae.getNnz(), ae.getUpdateType(), -1); break; diff --git a/src/main/java/org/apache/sysds/runtime/io/FrameReaderFactory.java b/src/main/java/org/apache/sysds/runtime/io/FrameReaderFactory.java index 4e21d2c3f60..6afd3836a15 100644 --- a/src/main/java/org/apache/sysds/runtime/io/FrameReaderFactory.java +++ b/src/main/java/org/apache/sysds/runtime/io/FrameReaderFactory.java @@ -51,6 +51,8 @@ public static FrameReader createFrameReader(FileFormat fmt, FileFormatProperties case PROTO: // TODO performance improvement: add parallel reader return new FrameReaderProto(); + case PARQUET: + return binaryParallel ? new FrameReaderParquetParallel() : new FrameReaderParquet(); default: throw new DMLRuntimeException("Failed to create frame reader for unknown format: " + fmt.toString()); } diff --git a/src/main/java/org/apache/sysds/runtime/io/FrameReaderParquet.java b/src/main/java/org/apache/sysds/runtime/io/FrameReaderParquet.java index ff23e9ea316..504602ef713 100644 --- a/src/main/java/org/apache/sysds/runtime/io/FrameReaderParquet.java +++ b/src/main/java/org/apache/sysds/runtime/io/FrameReaderParquet.java @@ -20,15 +20,23 @@ import java.io.IOException; import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.concurrent.TimeUnit; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; -import org.apache.parquet.example.data.Group; +import org.apache.parquet.column.ColumnDescriptor; +import org.apache.parquet.column.ColumnReader; +import org.apache.parquet.column.impl.ColumnReadStoreImpl; +import org.apache.parquet.column.page.PageReadStore; import org.apache.parquet.hadoop.ParquetFileReader; -import org.apache.parquet.hadoop.ParquetReader; -import org.apache.parquet.hadoop.example.GroupReadSupport; import org.apache.parquet.hadoop.metadata.ParquetMetadata; import org.apache.parquet.hadoop.util.HadoopInputFile; +import org.apache.parquet.io.api.Binary; +import org.apache.parquet.io.api.Converter; +import org.apache.parquet.io.api.GroupConverter; +import org.apache.parquet.io.api.PrimitiveConverter; import org.apache.parquet.schema.MessageType; import org.apache.parquet.schema.PrimitiveType; import org.apache.sysds.common.Types.ValueType; @@ -78,7 +86,7 @@ public FrameBlock readFrameFromHDFS(String fname, ValueType[] schema, String[] n /** * Reads data from a Parquet file on HDFS and fills the provided FrameBlock. * The method retrieves the Parquet schema from the file footer, maps the required column names - * to their corresponding indices, and then uses a ParquetReader to iterate over each row. + * to their corresponding indices, and then uses the column API to iterate over each column. * Data is extracted based on the column type and set into the output FrameBlock. * * @param path The HDFS path to the Parquet file. @@ -89,65 +97,147 @@ public FrameBlock readFrameFromHDFS(String fname, ValueType[] schema, String[] n * @param clen The expected number of columns. */ protected void readParquetFrameFromHDFS(Path path, Configuration conf, FrameBlock dest, ValueType[] schema, long rlen, long clen) throws IOException { - // Retrieve schema from Parquet footer - ParquetMetadata metadata = ParquetFileReader.open(HadoopInputFile.fromPath(path, conf)).getFooter(); - MessageType parquetSchema = metadata.getFileMetaData().getSchema(); + int row = readSingleParquetFile(path, conf, dest, clen, 0); - // Map column names to Parquet schema indices - String[] columnNames = dest.getColumnNames(); - int[] columnIndices = new int[columnNames.length]; - for (int i = 0; i < columnNames.length; i++) { - columnIndices[i] = parquetSchema.getFieldIndex(columnNames[i]); + // Check frame dimensions + if (row != rlen) { + throw new IOException("Mismatch in row count: expected " + rlen + ", but got " + row); } + } + + // Constants for decoding legacy INT96 timestamps + private static final int JULIAN_EPOCH_OFFSET_DAYS = 2_440_588; + private static final long MILLIS_IN_DAY = TimeUnit.DAYS.toMillis(1); + private static final long NANOS_PER_MILLISECOND = TimeUnit.MILLISECONDS.toNanos(1); + + /** + * Reads a single Parquet file into the destination FrameBlock using the column API. + * Iterates row groups; within each row group reads each requested column and writes the + * value into the FrameBlock. + * + * @param path The HDFS path to the Parquet file. + * @param conf The Hadoop configuration. + * @param dest The FrameBlock to populate. + * @param clen The number of columns. + * @param rowOffset The starting row offset in the destination FrameBlock. + * @return The number of rows read. + */ + protected int readSingleParquetFile(Path path, Configuration conf, FrameBlock dest, + long clen, long rowOffset) throws IOException + { + String[] columnNames = dest.getColumnNames(); + + try (ParquetFileReader reader = ParquetFileReader.open(HadoopInputFile.fromPath(path, conf))) { + ParquetMetadata metadata = reader.getFooter(); + MessageType parquetSchema = metadata.getFileMetaData().getSchema(); + String createdBy = metadata.getFileMetaData().getCreatedBy(); + + // Map each requested frame column (by name) to its Parquet column descriptor. + ColumnDescriptor[] descriptors = new ColumnDescriptor[(int) clen]; + for (int col = 0; col < clen; col++) { + ColumnDescriptor desc = parquetSchema.getColumnDescription(new String[]{ columnNames[col] }); + // Nested columns cannot be represented by a flat FrameBlock column + if (desc.getMaxRepetitionLevel() > 0) + throw new IOException("Nested Parquet columns are not supported: " + columnNames[col]); + descriptors[col] = desc; + } + + // no-op converter tree, only used by ColumnReadStoreImpl to resolve a converter per column + GroupConverter rootConverter = newNoOpRootConverter(parquetSchema); + + int row = (int) rowOffset; + PageReadStore pages; + + while (true) { + pages = reader.readNextRowGroup(); - // Read data usind ParquetReader - try (ParquetReader rowReader = ParquetReader.builder(new GroupReadSupport(), path) - .withConf(conf) - .build()) { + if (pages == null) + break; + + int rowsInGroup = (int) pages.getRowCount(); + ColumnReadStoreImpl colStore = new ColumnReadStoreImpl(pages, rootConverter, parquetSchema, createdBy); - Group group; - int row = 0; - while ((group = rowReader.read()) != null) { for (int col = 0; col < clen; col++) { - int colIndex = columnIndices[col]; - if (group.getFieldRepetitionCount(colIndex) > 0) { - PrimitiveType.PrimitiveTypeName type = parquetSchema.getType(columnNames[col]).asPrimitiveType().getPrimitiveTypeName(); - switch (type) { - case INT32: - dest.set(row, col, group.getInteger(colIndex, 0)); - break; - case INT64: - dest.set(row, col, group.getLong(colIndex, 0)); - break; - case FLOAT: - dest.set(row, col, group.getFloat(colIndex, 0)); - break; - case DOUBLE: - dest.set(row, col, group.getDouble(colIndex, 0)); - break; - case BOOLEAN: - dest.set(row, col, group.getBoolean(colIndex, 0)); - break; - case BINARY: - dest.set(row, col, group.getBinary(colIndex, 0).toStringUsingUTF8()); - break; - default: - throw new IOException("Unsupported data type: " + type); - } - } else { - dest.set(row, col, null); - } + ColumnDescriptor desc = descriptors[col]; + ColumnReader creader = colStore.getColumnReader(desc); + int maxDef = desc.getMaxDefinitionLevel(); + PrimitiveType.PrimitiveTypeName ptype = desc.getPrimitiveType().getPrimitiveTypeName(); + readColumn(dest, creader, col, row, rowsInGroup, maxDef, ptype); } - row++; + row += rowsInGroup; } + return row - (int) rowOffset; + } + } - // Check frame dimensions - if (row != rlen) { - throw new IOException("Mismatch in row count: expected " + rlen + ", but got " + row); + /** + * Reads one column of a row group, writing each value (or null) into the destination FrameBlock. + */ + private void readColumn(FrameBlock dest, ColumnReader creader, int col, int rowStart, + int rowsInGroup, int maxDef, PrimitiveType.PrimitiveTypeName ptype) throws IOException + { + for (int i = 0; i < rowsInGroup; i++) { + int row = rowStart + i; + if (creader.getCurrentDefinitionLevel() == maxDef) { + switch (ptype) { + case INT32: + dest.set(row, col, creader.getInteger()); + break; + case INT64: + dest.set(row, col, creader.getLong()); + break; + case FLOAT: + dest.set(row, col, creader.getFloat()); + break; + case DOUBLE: + dest.set(row, col, creader.getDouble()); + break; + case BOOLEAN: + dest.set(row, col, creader.getBoolean()); + break; + case INT96: { + // Legacy INT96 timestamp, narrowed to epoch millis. + // See https://github.com/apache/parquet-format/blob/master/LogicalTypes.md#timestamp + Binary binary = creader.getBinary(); + ByteBuffer buf = ByteBuffer.wrap(binary.getBytes()).order(ByteOrder.LITTLE_ENDIAN); + long nanosOfDay = buf.getLong(); + int julianDay = buf.getInt(); + long millis = (julianDay - JULIAN_EPOCH_OFFSET_DAYS) * MILLIS_IN_DAY + + nanosOfDay / NANOS_PER_MILLISECOND; + dest.set(row, col, millis); + break; + } + case BINARY: + dest.set(row, col, creader.getBinary().toStringUsingUTF8()); + break; + default: + throw new IOException("Unsupported data type: " + ptype); + } + } + else { + dest.set(row, col, null); } + creader.consume(); } } + /** + * Builds a no-op converter tree matching the (flat) Parquet schema. The converter + * callbacks are never invoked because values are read through the typed creader.getX accessors in readColumn(). + * The tree merely serves to satisfy the ColumnReadStoreImpl constructor + */ + private static GroupConverter newNoOpRootConverter(MessageType schema) { + final int n = schema.getFieldCount(); + final PrimitiveConverter[] leaves = new PrimitiveConverter[n]; + for (int i = 0; i < n; i++) + leaves[i] = new PrimitiveConverter() {}; + return new GroupConverter() { + @Override public Converter getConverter(int fieldIndex) { return leaves[fieldIndex]; } + @Override public void start() {} + @Override public void end() {} + }; + } + //not implemented @Override public FrameBlock readFrameFromInputStream(InputStream is, ValueType[] schema, String[] names, long rlen, long clen) diff --git a/src/main/java/org/apache/sysds/runtime/io/FrameReaderParquetParallel.java b/src/main/java/org/apache/sysds/runtime/io/FrameReaderParquetParallel.java index 3d40f53c626..da25157512a 100644 --- a/src/main/java/org/apache/sysds/runtime/io/FrameReaderParquetParallel.java +++ b/src/main/java/org/apache/sysds/runtime/io/FrameReaderParquetParallel.java @@ -20,6 +20,8 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; @@ -27,9 +29,10 @@ import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; -import org.apache.parquet.example.data.Group; -import org.apache.parquet.hadoop.ParquetReader; -import org.apache.parquet.hadoop.example.GroupReadSupport; +import org.apache.parquet.hadoop.ParquetFileReader; +import org.apache.parquet.hadoop.metadata.BlockMetaData; +import org.apache.parquet.hadoop.metadata.ParquetMetadata; +import org.apache.parquet.hadoop.util.HadoopInputFile; import org.apache.sysds.common.Types.ValueType; import org.apache.sysds.hops.OptimizerUtils; import org.apache.sysds.runtime.DMLRuntimeException; @@ -59,14 +62,35 @@ public class FrameReaderParquetParallel extends FrameReaderParquet { protected void readParquetFrameFromHDFS(Path path, Configuration conf, FrameBlock dest, ValueType[] schema, long rlen, long clen) throws IOException, DMLRuntimeException { FileSystem fs = IOUtilFunctions.getFileSystem(path); Path[] files = IOUtilFunctions.getSequenceFilePaths(fs, path); + Arrays.sort(files, Comparator.comparing(Path::getName)); int numThreads = Math.min(OptimizerUtils.getParallelBinaryReadParallelism(), files.length); + long[] offsets = new long[files.length]; + long cumulative = 0; + + for (int i = 0; i < files.length; i++) { + long fileRows = 0; + + try (ParquetFileReader reader = + ParquetFileReader.open(HadoopInputFile.fromPath(files[i], conf))) { + + ParquetMetadata meta = reader.getFooter(); + + for (BlockMetaData block : meta.getBlocks()) { + fileRows += block.getRowCount(); + } + } + + offsets[i] = cumulative; + cumulative += fileRows; + } + // Create and execute read tasks ExecutorService pool = CommonThreadPool.get(numThreads); try { List tasks = new ArrayList<>(); - for (Path file : files) { - tasks.add(new ReadFileTask(file, conf, dest, schema, clen)); + for (int i = 0; i < files.length; i++) { + tasks.add(new ReadFileTask(files[i], conf, dest, clen, offsets[i])); } for (Future task : pool.invokeAll(tasks)) { @@ -83,35 +107,21 @@ private class ReadFileTask implements Callable { private Path path; private Configuration conf; private FrameBlock dest; - @SuppressWarnings("unused") - private ValueType[] schema; private long clen; + private long rowOffset; - public ReadFileTask(Path path, Configuration conf, FrameBlock dest, ValueType[] schema, long clen) { + public ReadFileTask(Path path, Configuration conf, FrameBlock dest, long clen, long rowOffset) { this.path = path; this.conf = conf; this.dest = dest; - this.schema = schema; this.clen = clen; + this.rowOffset = rowOffset; } // When executed, a ParquetReader for the assigned file opens and iterates over each row processing every column. @Override public Object call() throws Exception { - try (ParquetReader reader = ParquetReader.builder(new GroupReadSupport(), path).withConf(conf).build()) { - Group group; - int row = 0; - while ((group = reader.read()) != null) { - for (int col = 0; col < clen; col++) { - if (group.getFieldRepetitionCount(col) > 0) { - dest.set(row, col, group.getValueToString(col, 0)); - } else { - dest.set(row, col, null); - } - } - row++; - } - } + FrameReaderParquetParallel.super.readSingleParquetFile(path, conf, dest, clen, rowOffset); return null; } } diff --git a/src/main/java/org/apache/sysds/runtime/io/FrameWriterFactory.java b/src/main/java/org/apache/sysds/runtime/io/FrameWriterFactory.java index 3fb3968c96f..6e60af8288b 100644 --- a/src/main/java/org/apache/sysds/runtime/io/FrameWriterFactory.java +++ b/src/main/java/org/apache/sysds/runtime/io/FrameWriterFactory.java @@ -50,6 +50,8 @@ public static FrameWriter createFrameWriter(FileFormat fmt, FileFormatProperties return binaryParallel ? new FrameWriterBinaryBlockParallel() : new FrameWriterBinaryBlock(); case PROTO: return new FrameWriterProto(); + case PARQUET: + return binaryParallel ? new FrameWriterParquetParallel() : new FrameWriterParquet(); default: throw new DMLRuntimeException("Failed to create frame writer for unknown format: " + fmt.toString()); } diff --git a/src/main/java/org/apache/sysds/runtime/io/FrameWriterParquet.java b/src/main/java/org/apache/sysds/runtime/io/FrameWriterParquet.java index ccaeeb56d51..35e457e334d 100644 --- a/src/main/java/org/apache/sysds/runtime/io/FrameWriterParquet.java +++ b/src/main/java/org/apache/sysds/runtime/io/FrameWriterParquet.java @@ -19,19 +19,23 @@ package org.apache.sysds.runtime.io; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; +import java.util.HashMap; +import java.util.Map; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.mapred.JobConf; -import org.apache.parquet.example.data.Group; -import org.apache.parquet.example.data.simple.SimpleGroupFactory; +import org.apache.parquet.hadoop.ParquetOutputFormat; import org.apache.parquet.hadoop.ParquetWriter; -import org.apache.parquet.hadoop.example.ExampleParquetWriter; +import org.apache.parquet.hadoop.api.WriteSupport; +import org.apache.parquet.hadoop.metadata.CompressionCodecName; +import org.apache.parquet.io.api.Binary; +import org.apache.parquet.io.api.RecordConsumer; +import org.apache.parquet.schema.LogicalTypeAnnotation; import org.apache.parquet.schema.MessageType; -import org.apache.parquet.schema.MessageTypeParser; +import org.apache.parquet.schema.PrimitiveType.PrimitiveTypeName; +import org.apache.parquet.schema.Types; import org.apache.sysds.conf.ConfigurationManager; import org.apache.sysds.runtime.DMLRuntimeException; import org.apache.sysds.runtime.frame.data.FrameBlock; @@ -44,6 +48,26 @@ */ public class FrameWriterParquet extends FrameWriter { + public enum DictEncoding { ALL_ON, ALL_OFF, STRING_ONLY } + + private final CompressionCodecName codec; + private final DictEncoding dictEncoding; + private final long rowGroupSize; + + public FrameWriterParquet() { + this(CompressionCodecName.ZSTD, DictEncoding.STRING_ONLY, ParquetWriter.DEFAULT_BLOCK_SIZE); + } + + public FrameWriterParquet(CompressionCodecName codec, DictEncoding dictEncoding) { + this(codec, dictEncoding, ParquetWriter.DEFAULT_BLOCK_SIZE); + } + + public FrameWriterParquet(CompressionCodecName codec, DictEncoding dictEncoding, long rowGroupSize) { + this.codec = codec; + this.dictEncoding = dictEncoding; + this.rowGroupSize = rowGroupSize; + } + /** * Writes a FrameBlock to a Parquet file on HDFS. * @@ -73,7 +97,7 @@ public final void writeFrameToHDFS(FrameBlock src, String fname, long rlen, long /** * Writes the FrameBlock data to a Parquet file using a ParquetWriter. * The method generates a Parquet schema based on the metadata of the FrameBlock, initializes a ParquetWriter with specified configurations, - * iterates over each row and column, adding values (in batches for improved performance) using type-specific conversions. + * iterates over each row and column, writing directly to the RecordConsumer, using type-specific conversions. * * @param path The HDFS path where the Parquet file will be written. * @param conf The Hadoop configuration. @@ -87,70 +111,25 @@ protected void writeParquetFrameToHDFS(Path path, Configuration conf, FrameBlock // Create schema based on frame block metadata MessageType schema = createParquetSchema(src); - // TODO:Experiment with different batch sizes? - int batchSize = 1000; - int rowCount = 0; + String[] columnNames = src.getColumnNames(); + ValueType[] columnTypes = src.getSchema(); - // Write data using ParquetWriter //FIXME replace example writer? - try (ParquetWriter writer = ExampleParquetWriter.builder(path) + FrameParquetWriterBuilder writerBuilder = new FrameParquetWriterBuilder(path, schema, src) .withConf(conf) - .withType(schema) - .withCompressionCodec(ParquetWriter.DEFAULT_COMPRESSION_CODEC_NAME) - .withRowGroupSize((long) ParquetWriter.DEFAULT_BLOCK_SIZE) - .withPageSize(ParquetWriter.DEFAULT_PAGE_SIZE) - .withDictionaryEncoding(true) - .build()) - { - - SimpleGroupFactory groupFactory = new SimpleGroupFactory(schema); - - List rowBuffer = new ArrayList<>(batchSize); - - for (int i = 0; i < src.getNumRows(); i++) { - Group group = groupFactory.newGroup(); - for (int j = 0; j < src.getNumColumns(); j++) { - Object value = src.get(i, j); - if (value != null) { - ValueType type = src.getSchema()[j]; - switch (type) { - case STRING: - group.add(src.getColumnNames()[j], value.toString()); - break; - case INT32: - group.add(src.getColumnNames()[j], (int) value); - break; - case INT64: - group.add(src.getColumnNames()[j], (long) value); - break; - case FP32: - group.add(src.getColumnNames()[j], (float) value); - break; - case FP64: - group.add(src.getColumnNames()[j], (double) value); - break; - case BOOLEAN: - group.add(src.getColumnNames()[j], (boolean) value); - break; - default: - throw new IOException("Unsupported value type: " + type); - } - } - } - rowBuffer.add(group); - rowCount++; + .withCompressionCodec(CompressionCodecName.fromConf(conf.get(ParquetOutputFormat.COMPRESSION, codec.name()))) + .withRowGroupSize(conf.getLong(ParquetOutputFormat.BLOCK_SIZE, rowGroupSize)) + .withPageSize(conf.getInt(ParquetOutputFormat.PAGE_SIZE, ParquetWriter.DEFAULT_PAGE_SIZE)) + .withDictionaryPageSize(conf.getInt(ParquetOutputFormat.DICTIONARY_PAGE_SIZE, ParquetWriter.DEFAULT_PAGE_SIZE)) + .withDictionaryEncoding(conf.getBoolean(ParquetOutputFormat.ENABLE_DICTIONARY, dictEncoding == DictEncoding.ALL_ON)); - if (rowCount >= batchSize) { - for (Group g : rowBuffer) { - writer.write(g); - } - rowBuffer.clear(); - rowCount = 0; - } - } - - for (Group g : rowBuffer) { - writer.write(g); - } + if (dictEncoding == DictEncoding.STRING_ONLY) + for (int j = 0; j < src.getNumColumns(); j++) + if (columnTypes[j] == ValueType.STRING) + writerBuilder = writerBuilder.withDictionaryEncoding(columnNames[j], true); + + try (ParquetWriter writer = writerBuilder.build()) { + for (int i = 0; i < src.getNumRows(); i++) + writer.write(i); } // Delete CRC files created by Hadoop if necessary @@ -164,36 +143,128 @@ protected void writeParquetFrameToHDFS(Path path, Configuration conf, FrameBlock * @return The generated Parquet MessageType schema. */ protected MessageType createParquetSchema(FrameBlock src) { - StringBuilder schemaBuilder = new StringBuilder("message FrameSchema {"); String[] columnNames = src.getColumnNames(); ValueType[] columnTypes = src.getSchema(); + Types.MessageTypeBuilder builder = Types.buildMessage(); for (int i = 0; i < src.getNumColumns(); i++) { - schemaBuilder.append("optional "); switch (columnTypes[i]) { case STRING: - schemaBuilder.append("binary ").append(columnNames[i]).append(" (UTF8);"); + builder.optional(PrimitiveTypeName.BINARY) + .as(LogicalTypeAnnotation.stringType()) + .named(columnNames[i]); break; case INT32: - schemaBuilder.append("int32 ").append(columnNames[i]).append(";"); + builder.optional(PrimitiveTypeName.INT32).named(columnNames[i]); break; case INT64: - schemaBuilder.append("int64 ").append(columnNames[i]).append(";"); + builder.optional(PrimitiveTypeName.INT64).named(columnNames[i]); break; case FP32: - schemaBuilder.append("float ").append(columnNames[i]).append(";"); + builder.optional(PrimitiveTypeName.FLOAT).named(columnNames[i]); break; case FP64: - schemaBuilder.append("double ").append(columnNames[i]).append(";"); + builder.optional(PrimitiveTypeName.DOUBLE).named(columnNames[i]); break; case BOOLEAN: - schemaBuilder.append("boolean ").append(columnNames[i]).append(";"); + builder.optional(PrimitiveTypeName.BOOLEAN).named(columnNames[i]); break; default: throw new IllegalArgumentException("Unsupported data type: " + columnTypes[i]); } } - schemaBuilder.append("}"); - return MessageTypeParser.parseMessageType(schemaBuilder.toString()); + return builder.named("FrameSchema"); + } + + /** + * WriteSupport implementation that writes rows from a FrameBlock directly to the + * Parquet RecordConsumer. + */ + private static class FrameWriteSupport extends WriteSupport { + private final MessageType schema; + private final FrameBlock src; + private RecordConsumer recordConsumer; + // constant across all rows + private String[] colNames; + private ValueType[] colTypes; + private int numCols; + + FrameWriteSupport(MessageType schema, FrameBlock src) { + this.schema = schema; + this.src = src; + } + + @Override + public WriteContext init(Configuration configuration) { + Map metadata = new HashMap<>(); + return new WriteContext(schema, metadata); + } + + @Override + public void prepareForWrite(RecordConsumer consumer) { + this.recordConsumer = consumer; + this.colNames = src.getColumnNames(); + this.colTypes = src.getSchema(); + this.numCols = src.getNumColumns(); + } + + @Override + public void write(Integer rowIndex) { + recordConsumer.startMessage(); + for (int j = 0; j < numCols; j++) { + Object value = src.get(rowIndex, j); + if (value != null) { + recordConsumer.startField(colNames[j], j); + switch (colTypes[j]) { + case STRING: + recordConsumer.addBinary(Binary.fromString(value.toString())); + break; + case INT32: + recordConsumer.addInteger((int) value); + break; + case INT64: + recordConsumer.addLong((long) value); + break; + case FP32: + recordConsumer.addFloat((float) value); + break; + case FP64: + recordConsumer.addDouble((double) value); + break; + case BOOLEAN: + recordConsumer.addBoolean((boolean) value); + break; + default: + throw new IllegalArgumentException("Unsupported value type: " + colTypes[j]); + } + recordConsumer.endField(colNames[j], j); + } + } + recordConsumer.endMessage(); + } + } + + /** + * ParquetWriter builder wired to FrameWriteSupport. + */ + private static class FrameParquetWriterBuilder extends ParquetWriter.Builder { + private final MessageType schema; + private final FrameBlock src; + + FrameParquetWriterBuilder(Path path, MessageType schema, FrameBlock src) { + super(path); + this.schema = schema; + this.src = src; + } + + @Override + protected FrameParquetWriterBuilder self() { + return this; + } + + @Override + protected WriteSupport getWriteSupport(Configuration conf) { + return new FrameWriteSupport(schema, src); + } } } diff --git a/src/main/java/org/apache/sysds/runtime/io/FrameWriterParquetParallel.java b/src/main/java/org/apache/sysds/runtime/io/FrameWriterParquetParallel.java index 0ef4431ef47..441ec2db550 100644 --- a/src/main/java/org/apache/sysds/runtime/io/FrameWriterParquetParallel.java +++ b/src/main/java/org/apache/sysds/runtime/io/FrameWriterParquetParallel.java @@ -35,14 +35,14 @@ import org.apache.sysds.utils.stats.InfrastructureAnalyzer; /** - * Multi-threaded frame parquet reader. + * Multi-threaded frame parquet writer. * */ public class FrameWriterParquetParallel extends FrameWriterParquet { /** * Writes the FrameBlock data to HDFS in parallel. - * The method estimates the number of output partitions by comparing the total number of cells in the FrameBlock with the + * The method estimates the number of output partitions by comparing the estimated output size of the FrameBlock with the * HDFS block size. It then determines the number of threads to use based on the parallelism configuration and the * number of partitions. In case of parallelism, it divides the FrameBlock into chunks and a thread pool is created to * execute a write task for each partition concurrently. @@ -55,14 +55,14 @@ public class FrameWriterParquetParallel extends FrameWriterParquet { protected void writeParquetFrameToHDFS(Path path, Configuration conf, FrameBlock src) throws IOException, DMLRuntimeException { - // Estimate number of output partitions - int numPartFiles = Math.max((int) (src.getNumRows() * src.getNumColumns() / InfrastructureAnalyzer.getHDFSBlockSize()), 1); - + // Estimate output partitions from output size in bytes + int numPartFiles = Math.max((int) (OptimizerUtils.estimateSizeExactFrame(src.getNumRows(), src.getNumColumns()) + / InfrastructureAnalyzer.getHDFSBlockSize()), 1); + // Determine parallelism int numThreads = Math.min(OptimizerUtils.getParallelBinaryWriteParallelism(), numPartFiles); - // Fall back to sequential write if numThreads <= 1 - if (numThreads <= 1) { + if (!_forcedParallel && numThreads <= 1) { super.writeParquetFrameToHDFS(path, conf, src); return; } diff --git a/src/test/java/org/apache/sysds/test/functions/io/parquet/FrameParquetSchemaTest.java b/src/test/java/org/apache/sysds/test/functions/io/parquet/FrameParquetSchemaTest.java index 1e4334891ed..54945f1689a 100644 --- a/src/test/java/org/apache/sysds/test/functions/io/parquet/FrameParquetSchemaTest.java +++ b/src/test/java/org/apache/sysds/test/functions/io/parquet/FrameParquetSchemaTest.java @@ -22,6 +22,7 @@ import org.junit.Assert; import org.junit.Test; +import org.apache.sysds.test.TestUtils; import org.apache.sysds.common.Types.ValueType; import org.apache.sysds.runtime.frame.data.FrameBlock; import org.apache.sysds.runtime.io.FrameReader; @@ -76,11 +77,11 @@ public void testParquetWriteReadAllSchemaTypes() { // Populate frame block Object[][] rows = new Object[][] { - { 1.0, 1.1f, 10, 100L, true, "A" }, - { 2.0, 2.1f, 20, 200L, false, "B" }, - { 3.0, 3.1f, 30, 300L, true, "C" }, - { 4.0, 4.1f, 40, 400L, false, "D" }, - { 5.0, 5.1f, 50, 500L, true, "E" } + { 1.0, 1.1f, 10, 100L, true, "A" }, + { 2.0, 2.1f, 20, 200L, false, "B" }, + { 3.0, 3.1f, 30, 300L, true, "C" }, + { 4.0, 4.1f, 40, 400L, false, "D" }, + { 5.0, 5.1f, 50, 500L, true, "E" } }; for (Object[] row : rows) { @@ -115,7 +116,7 @@ public void testParquetWriteReadAllSchemaTypes() { } // Compare the original and the read frame blocks - compareFrameBlocks(fb, fbRead, 1e-6); + TestUtils.compareFrames(fb, fbRead, false); } /** @@ -138,11 +139,11 @@ public void testParquetWriteReadAllSchemaTypesParallel() { FrameBlock fb = new FrameBlock(schema); Object[][] rows = new Object[][] { - { 1.0, 1.1f, 10, 100L, true, "A" }, - { 2.0, 2.1f, 20, 200L, false, "B" }, - { 3.0, 3.1f, 30, 300L, true, "C" }, - { 4.0, 4.1f, 40, 400L, false, "D" }, - { 5.0, 5.1f, 50, 500L, true, "E" } + { 1.0, 1.1f, 10, 100L, true, "A" }, + { 2.0, 2.1f, 20, 200L, false, "B" }, + { 3.0, 3.1f, 30, 300L, true, "C" }, + { 4.0, 4.1f, 40, 400L, false, "D" }, + { 5.0, 5.1f, 50, 500L, true, "E" } }; for (Object[] row : rows) { @@ -172,52 +173,6 @@ public void testParquetWriteReadAllSchemaTypesParallel() { Assert.fail("Failed to read frame block from Parquet (parallel): " + e.getMessage()); } - compareFrameBlocks(fb, fbRead, 1e-6); - } - - private void compareFrameBlocks(FrameBlock expected, FrameBlock actual, double eps) { - Assert.assertEquals("Number of rows mismatch", expected.getNumRows(), actual.getNumRows()); - Assert.assertEquals("Number of columns mismatch", expected.getNumColumns(), actual.getNumColumns()); - - int rows = expected.getNumRows(); - int cols = expected.getNumColumns(); - - for (int i = 0; i < rows; i++) { - for (int j = 0; j < cols; j++) { - Object expVal = expected.get(i, j); - Object actVal = actual.get(i, j); - ValueType vt = expected.getSchema()[j]; - - // Handle nulls first - if(expVal == null || actVal == null) { - Assert.assertEquals("Mismatch at (" + i + "," + j + ")", expVal, actVal); - } else { - switch(vt) { - case FP64: - case FP32: - double dExp = ((Number) expVal).doubleValue(); - double dAct = ((Number) actVal).doubleValue(); - Assert.assertEquals("Mismatch at (" + i + "," + j + ")", dExp, dAct, eps); - break; - case INT32: - case INT64: - long lExp = ((Number) expVal).longValue(); - long lAct = ((Number) actVal).longValue(); - Assert.assertEquals("Mismatch at (" + i + "," + j + ")", lExp, lAct); - break; - case BOOLEAN: - boolean bExp = (Boolean) expVal; - boolean bAct = (Boolean) actVal; - Assert.assertEquals("Mismatch at (" + i + "," + j + ")", bExp, bAct); - break; - case STRING: - Assert.assertEquals("Mismatch at (" + i + "," + j + ")", expVal.toString(), actVal.toString()); - break; - default: - Assert.fail("Unsupported type in comparison: " + vt); - } - } - } - } + TestUtils.compareFrames(fb, fbRead, false); } } diff --git a/src/test/java/org/apache/sysds/test/functions/io/parquet/FrameReaderParquetLegacy.java b/src/test/java/org/apache/sysds/test/functions/io/parquet/FrameReaderParquetLegacy.java new file mode 100644 index 00000000000..0b2510e4508 --- /dev/null +++ b/src/test/java/org/apache/sysds/test/functions/io/parquet/FrameReaderParquetLegacy.java @@ -0,0 +1,186 @@ +/* + * 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.sysds.test.functions.io.parquet; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.concurrent.TimeUnit; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.Path; +import org.apache.parquet.example.data.Group; +import org.apache.parquet.hadoop.ParquetFileReader; +import org.apache.parquet.hadoop.ParquetReader; +import org.apache.parquet.hadoop.example.GroupReadSupport; +import org.apache.parquet.hadoop.metadata.ParquetMetadata; +import org.apache.parquet.hadoop.util.HadoopInputFile; +import org.apache.parquet.schema.MessageType; +import org.apache.parquet.schema.PrimitiveType; +import org.apache.sysds.common.Types.ValueType; +import org.apache.sysds.conf.ConfigurationManager; +import org.apache.sysds.runtime.DMLRuntimeException; +import org.apache.sysds.runtime.frame.data.FrameBlock; +import org.apache.sysds.runtime.io.FrameReader; +import org.apache.sysds.runtime.util.HDFSTool; +import org.apache.parquet.io.api.Binary; + +/** + * Single-threaded frame parquet reader using the {@code Group} record API (legacy baseline). + * + */ +public class FrameReaderParquetLegacy extends FrameReader { + + /** + * Reads a Parquet file from HDFS and converts it into a FrameBlock. + * + * @param fname The HDFS file path to the Parquet file. + * @param schema The expected data types of the columns. + * @param names The names of the columns. + * @param rlen The expected number of rows. + * @param clen The expected number of columns. + * @return A FrameBlock containing the data read from the Parquet file. + */ + @Override + public FrameBlock readFrameFromHDFS(String fname, ValueType[] schema, String[] names, long rlen, long clen) throws IOException, DMLRuntimeException { + // Prepare file access + Configuration conf = ConfigurationManager.getCachedJobConf(); + Path path = new Path(fname); + + // Check existence and non-empty file + if (!HDFSTool.existsFileOnHDFS(path.toString())) { + throw new IOException("File does not exist on HDFS: " + fname); + } + + // Allocate output frame block + ValueType[] lschema = createOutputSchema(schema, clen); + String[] lnames = createOutputNames(names, clen); + FrameBlock ret = createOutputFrameBlock(lschema, lnames, rlen); + + // Read Parquet file + readParquetFrameFromHDFS(path, conf, ret, lschema, rlen, clen); + + return ret; + } + + /** + * Reads data from a Parquet file on HDFS and fills the provided FrameBlock. + * The method retrieves the Parquet schema from the file footer, maps the required column names + * to their corresponding indices, and then uses a ParquetReader to iterate over each row. + * Data is extracted based on the column type and set into the output FrameBlock. + * + * @param path The HDFS path to the Parquet file. + * @param conf The Hadoop configuration. + * @param dest The FrameBlock to populate with data. + * @param schema The expected value types for the output columns. + * @param rlen The expected number of rows. + * @param clen The expected number of columns. + */ + protected void readParquetFrameFromHDFS(Path path, Configuration conf, FrameBlock dest, ValueType[] schema, long rlen, long clen) throws IOException { + int row = readSingleParquetFile(path, conf, dest, clen, 0); + + // Check frame dimensions + if (row != rlen) { + throw new IOException("Mismatch in row count: expected " + rlen + ", but got " + row); + } + } + + // Constants for decoding legacy INT96 timestamps + private static final int JULIAN_EPOCH_OFFSET_DAYS = 2_440_588; + private static final long MILLIS_IN_DAY = TimeUnit.DAYS.toMillis(1); + private static final long NANOS_PER_MILLISECOND = TimeUnit.MILLISECONDS.toNanos(1); + + protected int readSingleParquetFile(Path path, Configuration conf, FrameBlock dest, + long clen, long rowOffset) throws IOException { + // Retrieve schema from Parquet footer + ParquetMetadata metadata; + try (ParquetFileReader reader = ParquetFileReader.open(HadoopInputFile.fromPath(path, conf))) {metadata = reader.getFooter();} + MessageType parquetSchema = metadata.getFileMetaData().getSchema(); + + // Map column names to Parquet schema indices + String[] columnNames = dest.getColumnNames(); + int[] columnIndices = new int[columnNames.length]; + for (int i = 0; i < columnNames.length; i++) { + columnIndices[i] = parquetSchema.getFieldIndex(columnNames[i]); + } + + // Read data usind ParquetReader + try (ParquetReader rowReader = ParquetReader.builder(new GroupReadSupport(), path) + .withConf(conf) + .build()) { + + Group group; + int row = (int) rowOffset; + while ((group = rowReader.read()) != null) { + for (int col = 0; col < clen; col++) { + int colIndex = columnIndices[col]; + if (group.getFieldRepetitionCount(colIndex) > 0) { + PrimitiveType.PrimitiveTypeName type = parquetSchema.getType(columnNames[col]).asPrimitiveType().getPrimitiveTypeName(); + switch (type) { + case INT32: + dest.set(row, col, group.getInteger(colIndex, 0)); + break; + case INT64: + dest.set(row, col, group.getLong(colIndex, 0)); + break; + case FLOAT: + dest.set(row, col, group.getFloat(colIndex, 0)); + break; + case DOUBLE: + dest.set(row, col, group.getDouble(colIndex, 0)); + break; + case BOOLEAN: + dest.set(row, col, group.getBoolean(colIndex, 0)); + break; + case INT96: { + // Legacy INT96 timestamp (Spark/Impala), narrowed to epoch millis. + // See https://github.com/apache/parquet-format/blob/master/LogicalTypes.md#timestamp + Binary binary = group.getInt96(colIndex, 0); + ByteBuffer buf = ByteBuffer.wrap(binary.getBytes()).order(ByteOrder.LITTLE_ENDIAN); + long nanosOfDay = buf.getLong(); + int julianDay = buf.getInt(); + long millis = (julianDay - JULIAN_EPOCH_OFFSET_DAYS) * MILLIS_IN_DAY + + nanosOfDay / NANOS_PER_MILLISECOND; + dest.set(row, col, millis); + break; + } + case BINARY: + dest.set(row, col, group.getBinary(colIndex, 0).toStringUsingUTF8()); + break; + default: + throw new IOException("Unsupported data type: " + type); + } + } else { + dest.set(row, col, null); + } + } + row++; + } + return row - (int) rowOffset; + } + } + + //not implemented + @Override + public FrameBlock readFrameFromInputStream(InputStream is, ValueType[] schema, String[] names, long rlen, long clen) + throws IOException, DMLRuntimeException { + throw new UnsupportedOperationException("Unimplemented method 'readFrameFromInputStream'"); + } +} diff --git a/src/test/java/org/apache/sysds/test/functions/io/parquet/FrameReaderWriterParquetTest.java b/src/test/java/org/apache/sysds/test/functions/io/parquet/FrameReaderWriterParquetTest.java new file mode 100644 index 00000000000..b4d87e9f905 --- /dev/null +++ b/src/test/java/org/apache/sysds/test/functions/io/parquet/FrameReaderWriterParquetTest.java @@ -0,0 +1,134 @@ +/* + * 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.sysds.test.functions.io.parquet; + +import static org.junit.Assert.fail; + +import java.io.IOException; + +import org.apache.sysds.common.Types.ValueType; +import org.apache.sysds.runtime.frame.data.FrameBlock; +import org.apache.sysds.runtime.io.FrameReaderParquet; +import org.apache.sysds.runtime.io.FrameWriterParquet; +import org.apache.sysds.test.TestUtils; +import org.junit.Test; + +/** + * Random-frame write/read round-trip test for the parquet frame reader/writer. + */ +public class FrameReaderWriterParquetTest { + + private static final String FILENAME = "target/testTemp/functions/io/parquet/FrameReaderWriterParquetTest/frame.parquet"; + + // Parquet-supported value types + private static final ValueType[] SCHEMA = { + ValueType.FP64, ValueType.FP32, ValueType.INT32, ValueType.INT64, ValueType.BOOLEAN, ValueType.STRING + }; + + @Test + public void testSingleRowSingleCol() throws IOException { + runWriteReadRoundTrip(1, 1, 4669201); + } + + @Test + public void testSingleRowMultiCol() throws IOException { + runWriteReadRoundTrip(1, 6, 4669201); + } + + @Test + public void testMultiRowSingleCol() throws IOException { + runWriteReadRoundTrip(21, 1, 4669201); + } + + @Test + public void testMultiRowMultiCol() throws IOException { + runWriteReadRoundTrip(42, 5, 4669201); + } + + @Test + public void testLargerFrame() throws IOException { + runWriteReadRoundTrip(694, 6, 4669201); + } + + @Test + public void testValueTypeEdgeCases() throws IOException { + // type min/max, empty string, special chars (comma/quote/newline/unicode) + ValueType[] schema = { ValueType.FP32, ValueType.FP64, ValueType.INT32, ValueType.INT64, + ValueType.BOOLEAN, ValueType.STRING }; + String[] names = { "f32", "f64", "i32", "i64", "b", "s" }; + String[][] data = { + { String.valueOf(Float.MAX_VALUE), String.valueOf(Double.MAX_VALUE), + String.valueOf(Integer.MAX_VALUE), String.valueOf(Long.MAX_VALUE), "true", "" }, + { String.valueOf(-Float.MAX_VALUE), String.valueOf(-Double.MAX_VALUE), + String.valueOf(Integer.MIN_VALUE), String.valueOf(Long.MIN_VALUE), "false", "a,b\"c\nd" }, + { "0.0", "0.0", "0", "0", "true", "unicode_é中" } + }; + FrameBlock in = new FrameBlock(schema, names, data); + + new FrameWriterParquet().writeFrameToHDFS(in, FILENAME, 3, 6); + FrameBlock out = new FrameReaderParquet().readFrameFromHDFS(FILENAME, schema, names, 3, 6); + + TestUtils.compareFrames(in, out, false); + } + + @Test + public void testNullsInStringColumn() throws IOException { + // Numeric columns from String[][] convert null to 0. Only test string nulls here. + // Numeric null round-trips are tested in ReadParquetTest. + ValueType[] schema = { ValueType.STRING, ValueType.STRING }; + String[] names = { "a", "b" }; + String[][] data = { + { "x", "y" }, + { null, null }, + { "p", null } + }; + FrameBlock in = new FrameBlock(schema, names, data); + + new FrameWriterParquet().writeFrameToHDFS(in, FILENAME, 3, 2); + FrameBlock out = new FrameReaderParquet().readFrameFromHDFS(FILENAME, schema, names, 3, 2); + + org.junit.Assert.assertNull(out.get(1, 0)); + org.junit.Assert.assertNull(out.get(1, 1)); + org.junit.Assert.assertNull(out.get(2, 1)); + org.junit.Assert.assertNotNull(out.get(0, 0)); + org.junit.Assert.assertNotNull(out.get(2, 0)); + TestUtils.compareFrames(in, out, false); + } + + private void runWriteReadRoundTrip(int rows, int cols, long seed) throws IOException { + try { + ValueType[] schema = new ValueType[cols]; + for(int i = 0; i < cols; i++) + schema[i] = SCHEMA[i % SCHEMA.length]; + + FrameBlock writeBlock = TestUtils.generateRandomFrameBlock(rows, schema, seed); + + new FrameWriterParquet().writeFrameToHDFS(writeBlock, FILENAME, rows, cols); + FrameBlock readBlock = new FrameReaderParquet() + .readFrameFromHDFS(FILENAME, schema, writeBlock.getColumnNames(), rows, cols); + + TestUtils.compareFrames(writeBlock, readBlock, false); + } + catch(Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } + } +} diff --git a/src/test/java/org/apache/sysds/test/functions/io/parquet/FrameWriterParquetLegacy.java b/src/test/java/org/apache/sysds/test/functions/io/parquet/FrameWriterParquetLegacy.java new file mode 100755 index 00000000000..e57e2c99846 --- /dev/null +++ b/src/test/java/org/apache/sysds/test/functions/io/parquet/FrameWriterParquetLegacy.java @@ -0,0 +1,212 @@ +/* + * 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.sysds.test.functions.io.parquet; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.mapred.JobConf; +import org.apache.parquet.example.data.Group; +import org.apache.parquet.example.data.simple.SimpleGroupFactory; +import org.apache.parquet.hadoop.ParquetWriter; +import org.apache.parquet.hadoop.example.ExampleParquetWriter; +import org.apache.parquet.schema.LogicalTypeAnnotation; +import org.apache.parquet.schema.MessageType; +import org.apache.parquet.schema.PrimitiveType.PrimitiveTypeName; +import org.apache.parquet.schema.Types; +import org.apache.sysds.conf.ConfigurationManager; +import org.apache.sysds.runtime.DMLRuntimeException; +import org.apache.sysds.runtime.frame.data.FrameBlock; +import org.apache.sysds.runtime.io.FrameWriter; +import org.apache.sysds.runtime.io.IOUtilFunctions; +import org.apache.sysds.runtime.util.HDFSTool; +import org.apache.sysds.common.Types.ValueType; + +/** + * Single-threaded frame parquet writer. + * + */ +public class FrameWriterParquetLegacy extends FrameWriter { + + private final int batchSizeOverride; + + public FrameWriterParquetLegacy() { + this.batchSizeOverride = 1000; + } + + public FrameWriterParquetLegacy(int batchSize) { + this.batchSizeOverride = batchSize; + } + + /** + * Writes a FrameBlock to a Parquet file on HDFS. + * + * @param src The FrameBlock containing the data to write. + * @param fname The HDFS file path where the Parquet file will be stored. + * @param rlen The expected number of rows. + * @param clen The expected number of columns. + */ + @Override + public final void writeFrameToHDFS(FrameBlock src, String fname, long rlen, long clen) throws IOException, DMLRuntimeException { + // Prepare file access + JobConf conf = ConfigurationManager.getCachedJobConf(); + Path path = new Path(fname); + + // If the file already exists on HDFS, remove it + HDFSTool.deleteFileIfExistOnHDFS(path, conf); + + // Check frame dimensions + if (src.getNumRows() != rlen || src.getNumColumns() != clen) { + throw new IOException("Frame dimensions mismatch with metadata: " + src.getNumRows() + "x" + src.getNumColumns() + " vs " + rlen + "x" + clen + "."); + } + + // Write parquet file + writeParquetFrameToHDFS(path, conf, src); + } + + /** + * Writes the FrameBlock data to a Parquet file using a ParquetWriter. + * The method generates a Parquet schema based on the metadata of the FrameBlock, initializes a ParquetWriter with specified configurations, + * iterates over each row and column, adding values (in batches for improved performance) using type-specific conversions. + * + * @param path The HDFS path where the Parquet file will be written. + * @param conf The Hadoop configuration. + * @param src The FrameBlock containing the data to write. + */ + protected void writeParquetFrameToHDFS(Path path, Configuration conf, FrameBlock src) + throws IOException + { + FileSystem fs = IOUtilFunctions.getFileSystem(path, conf); + + // Create schema based on frame block metadata + MessageType schema = createParquetSchema(src); + + int batchSize = batchSizeOverride; + int rowCount = 0; + + // Write data using ParquetWriter //FIXME replace example writer? + try (ParquetWriter writer = ExampleParquetWriter.builder(path) + .withConf(conf) + .withType(schema) + .withCompressionCodec(ParquetWriter.DEFAULT_COMPRESSION_CODEC_NAME) + .withRowGroupSize((long) ParquetWriter.DEFAULT_BLOCK_SIZE) + .withPageSize(ParquetWriter.DEFAULT_PAGE_SIZE) + .withDictionaryEncoding(true) + .build()) + { + + SimpleGroupFactory groupFactory = new SimpleGroupFactory(schema); + + List rowBuffer = new ArrayList<>(batchSize); + + for (int i = 0; i < src.getNumRows(); i++) { + Group group = groupFactory.newGroup(); + for (int j = 0; j < src.getNumColumns(); j++) { + Object value = src.get(i, j); + if (value != null) { + ValueType type = src.getSchema()[j]; + switch (type) { + case STRING: + group.add(src.getColumnNames()[j], value.toString()); + break; + case INT32: + group.add(src.getColumnNames()[j], (int) value); + break; + case INT64: + group.add(src.getColumnNames()[j], (long) value); + break; + case FP32: + group.add(src.getColumnNames()[j], (float) value); + break; + case FP64: + group.add(src.getColumnNames()[j], (double) value); + break; + case BOOLEAN: + group.add(src.getColumnNames()[j], (boolean) value); + break; + default: + throw new IOException("Unsupported value type: " + type); + } + } + } + rowBuffer.add(group); + rowCount++; + + if (rowCount >= batchSize) { + for (Group g : rowBuffer) { + writer.write(g); + } + rowBuffer.clear(); + rowCount = 0; + } + } + + for (Group g : rowBuffer) { + writer.write(g); + } + } + + // Delete CRC files created by Hadoop if necessary + IOUtilFunctions.deleteCrcFilesFromLocalFileSystem(fs, path); + } + + /** + * Creates a Parquet schema based on the metadata of a FrameBlock. + * + * @param src The FrameBlock whose metadata is used to create the Parquet schema. + * @return The generated Parquet MessageType schema. + */ + protected MessageType createParquetSchema(FrameBlock src) { + String[] columnNames = src.getColumnNames(); + ValueType[] columnTypes = src.getSchema(); + Types.MessageTypeBuilder builder = Types.buildMessage(); + + for (int i = 0; i < src.getNumColumns(); i++) { + switch (columnTypes[i]) { + case STRING: + builder.optional(PrimitiveTypeName.BINARY) + .as(LogicalTypeAnnotation.stringType()) + .named(columnNames[i]); + break; + case INT32: + builder.optional(PrimitiveTypeName.INT32).named(columnNames[i]); + break; + case INT64: + builder.optional(PrimitiveTypeName.INT64).named(columnNames[i]); + break; + case FP32: + builder.optional(PrimitiveTypeName.FLOAT).named(columnNames[i]); + break; + case FP64: + builder.optional(PrimitiveTypeName.DOUBLE).named(columnNames[i]); + break; + case BOOLEAN: + builder.optional(PrimitiveTypeName.BOOLEAN).named(columnNames[i]); + break; + default: + throw new IllegalArgumentException("Unsupported data type: " + columnTypes[i]); + } + } + return builder.named("FrameSchema"); + } +} diff --git a/src/test/java/org/apache/sysds/test/functions/io/parquet/ParquetReaderBenchmark.java b/src/test/java/org/apache/sysds/test/functions/io/parquet/ParquetReaderBenchmark.java new file mode 100644 index 00000000000..592e610185f --- /dev/null +++ b/src/test/java/org/apache/sysds/test/functions/io/parquet/ParquetReaderBenchmark.java @@ -0,0 +1,187 @@ +/* + * 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.sysds.test.functions.io.parquet; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.apache.sysds.common.Types.ValueType; +import org.apache.sysds.runtime.frame.data.FrameBlock; +import org.apache.sysds.runtime.io.FrameReaderParquet; +import org.apache.sysds.runtime.io.FrameWriterParquet; +import org.junit.After; +import org.junit.Assume; + +/** + * Parquet reader benchmark comparing the column-API reader against + * the Group-based reader. + * + * Results report median and min/max across runs and are appended to + * temp/benchmark_results.csv for plotting. + * + * If a dataset is not present the corresponding test is skipped with instructions. + */ +public class ParquetReaderBenchmark { + + private static final String TPCH_FILE = "temp/lineitem.tbl"; + private static final String RESULTS_CSV = "temp/benchmark_results.csv"; + private static final String TEMP_FILE = System.getProperty("java.io.tmpdir") + "/systemds_read_bench.parquet"; + private static final int RUNS = 7; + // override on larger machine with -DmaxRows=... (e.g. -DmaxRows=20000000) to test larger row groups. + private static final int MAX_ROWS = Integer.getInteger("maxRows", 2_000_000); + + // TPC-H lineitem schema + private static final ValueType[] LINEITEM_SCHEMA = { + ValueType.INT64, ValueType.INT64, ValueType.INT64, ValueType.INT32, + ValueType.FP64, ValueType.FP64, ValueType.FP64, ValueType.FP64, + ValueType.STRING, ValueType.STRING, ValueType.STRING, ValueType.STRING, + ValueType.STRING, ValueType.STRING, ValueType.STRING, ValueType.STRING + }; + private static final String[] LINEITEM_NAMES = { + "orderkey", "partkey", "suppkey", "linenumber", + "quantity", "extendedprice", "discount", "tax", + "returnflag", "linestatus", "shipdate", "commitdate", + "receiptdate", "shipinstruct", "shipmode", "comment" + }; + + private PrintWriter csv; + + @After + public void cleanup() { + new File(TEMP_FILE).delete(); + if (csv != null) csv.close(); + } + + // @Test + public void benchmarkReadTpch() throws Exception { + ReadSpec spec = writeTempParquet(loadLineitemOrSkip()); + runReadBenchmark("read_tpch", spec); + } + + private static final class ReadSpec { + final ValueType[] schema; final String[] names; final int rows; final int cols; + ReadSpec(ValueType[] schema, String[] names, int rows, int cols) { + this.schema = schema; this.names = names; this.rows = rows; this.cols = cols; + } + } + + /** + * Writes a frame to Parquet, then times reading it back with both sequential and parallel readers. + */ + private ReadSpec writeTempParquet(FrameBlock data) throws Exception { + int rows = data.getNumRows(), cols = data.getNumColumns(); + ReadSpec spec = new ReadSpec(data.getSchema(), data.getColumnNames(), rows, cols); + new File(TEMP_FILE).delete(); + new FrameWriterParquet().writeFrameToHDFS(data, TEMP_FILE, rows, cols); + return spec; + } + + /** + * Writes a frame to a Parquet, then benchmarks sequential and parallel reads + */ + private void runReadBenchmark(String category, ReadSpec spec) throws Exception { + final ValueType[] schema = spec.schema; + final String[] names = spec.names; + final int rows = spec.rows, cols = spec.cols; + + FrameBlock probe = new FrameReaderParquet().readFrameFromHDFS(TEMP_FILE, schema, names, rows, cols); + org.junit.Assert.assertEquals("Row count mismatch", rows, probe.getNumRows()); + org.junit.Assert.assertEquals("Column count mismatch", cols, probe.getNumColumns()); + probe = null; + + openCsv(); + System.out.println("\n=== Parquet Read Benchmark [" + category + "] (" + rows + " rows, median of " + RUNS + " runs) ===\n"); + System.out.printf("%-24s %12s %15s %s%n", "Reader", "Median (ms)", "Rows/sec", "[runs]"); + System.out.println("-".repeat(72)); + + timeRead(category, "Legacy (Group)", () -> + new FrameReaderParquetLegacy().readFrameFromHDFS(TEMP_FILE, schema, names, rows, cols), rows); + timeRead(category, "Column API", () -> + new FrameReaderParquet().readFrameFromHDFS(TEMP_FILE, schema, names, rows, cols), rows); + System.out.println(); + } + + private interface ReadAction { FrameBlock run() throws Exception; } + + private void timeRead(String category, String label, ReadAction action, int rows) throws Exception { + action.run(); // warmup + + long[] times = new long[RUNS]; + for (int run = 0; run < RUNS; run++) { + long start = System.currentTimeMillis(); + action.run(); + times[run] = System.currentTimeMillis() - start; + } + long med = median(times); + long min = Arrays.stream(times).min().orElse(med); + long max = Arrays.stream(times).max().orElse(med); + System.out.printf("%-24s %12d %15.0f %14s %s%n", label, med, rows * 1000.0 / med, meanStd(times), Arrays.toString(times)); + // columns: benchmark,label,time_ms(median),rows_per_sec,min_ms,max_ms + csv.printf("%s,%s,%d,%.0f,%d,%d%n", category, label, med, rows * 1000.0 / med, min, max); + } + + private static long median(long[] times) { + long[] sorted = times.clone(); + Arrays.sort(sorted); + return sorted[sorted.length / 2]; + } + + private static String meanStd(long[] times) { + double mean = Arrays.stream(times).average().orElse(0); + double var = Arrays.stream(times).mapToDouble(t -> (t - mean) * (t - mean)).average().orElse(0); + return String.format("%.0f+-%.0f ms", mean, Math.sqrt(var)); + } + + private void openCsv() throws Exception { + new File("temp").mkdirs(); + boolean exists = new File(RESULTS_CSV).exists(); + csv = new PrintWriter(new FileWriter(RESULTS_CSV, true)); + if (!exists) + csv.println("benchmark,label,time_ms,rows_per_sec,size_mb,compression_ratio"); + csv.flush(); + } + + private FrameBlock loadLineitemOrSkip() throws Exception { + File f = new File(TPCH_FILE); + if (!f.exists()) { + System.out.println("=== TPC-H read benchmark skipped, dataset not found at " + TPCH_FILE + " ==="); + Assume.assumeTrue("TPC-H dataset not found at " + TPCH_FILE, false); + } + System.out.print("Loading " + f.getPath() + " ... "); + List rows = new ArrayList<>(); + try (BufferedReader br = new BufferedReader(new FileReader(f))) { + String line; + while ((line = br.readLine()) != null && rows.size() < MAX_ROWS) { + if (line.isEmpty()) continue; + if (line.endsWith("|")) line = line.substring(0, line.length() - 1); + rows.add(line.split("\\|", -1)); + } + } + String[][] arr = rows.toArray(new String[0][]); + System.out.println(arr.length + " rows loaded."); + return new FrameBlock(LINEITEM_SCHEMA, LINEITEM_NAMES, arr); + } + +} diff --git a/src/test/java/org/apache/sysds/test/functions/io/parquet/ParquetTestUtils.java b/src/test/java/org/apache/sysds/test/functions/io/parquet/ParquetTestUtils.java new file mode 100644 index 00000000000..da28809333e --- /dev/null +++ b/src/test/java/org/apache/sysds/test/functions/io/parquet/ParquetTestUtils.java @@ -0,0 +1,85 @@ +/* + * 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.sysds.test.functions.io.parquet; + +import java.io.IOException; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.Path; +import org.apache.parquet.hadoop.ParquetFileReader; +import org.apache.parquet.hadoop.metadata.BlockMetaData; +import org.apache.parquet.hadoop.metadata.ParquetMetadata; +import org.apache.parquet.hadoop.util.HadoopInputFile; +import org.apache.parquet.schema.MessageType; +import org.apache.parquet.schema.PrimitiveType; +import org.apache.sysds.common.Types.ValueType; +import org.apache.sysds.conf.ConfigurationManager; + +class ParquetTestUtils { + + static class ParquetMetadataInfo { + String[] names; + ValueType[] schema; + long rlen; + long clen; + } + + static ParquetMetadataInfo inferMetadata(String fname) throws IOException { + Configuration conf = ConfigurationManager.getCachedJobConf(); + Path path = new Path(fname); + + ParquetMetadata metadata; + try (ParquetFileReader r = ParquetFileReader.open(HadoopInputFile.fromPath(path, conf))) { + metadata = r.getFooter(); + } + MessageType parquetSchema = metadata.getFileMetaData().getSchema(); + + int fieldCount = parquetSchema.getFieldCount(); + String[] names = new String[fieldCount]; + ValueType[] schema = new ValueType[fieldCount]; + + for (int i = 0; i < fieldCount; i++) { + names[i] = parquetSchema.getFieldName(i); + PrimitiveType.PrimitiveTypeName type = parquetSchema.getType(i).asPrimitiveType().getPrimitiveTypeName(); + switch (type) { + case INT32: schema[i] = ValueType.INT32; break; + case INT64: schema[i] = ValueType.INT64; break; + case FLOAT: schema[i] = ValueType.FP32; break; + case DOUBLE: schema[i] = ValueType.FP64; break; + case BOOLEAN: schema[i] = ValueType.BOOLEAN; break; + case BINARY: schema[i] = ValueType.STRING; break; + case INT96: schema[i] = ValueType.INT64; break; + default: + throw new IOException("Unsupported parquet type: " + type + " in column " + names[i]); + } + } + + long rlen = 0; + for (BlockMetaData block : metadata.getBlocks()) + rlen += block.getRowCount(); + + ParquetMetadataInfo info = new ParquetMetadataInfo(); + info.names = names; + info.schema = schema; + info.rlen = rlen; + info.clen = fieldCount; + return info; + } +} diff --git a/src/test/java/org/apache/sysds/test/functions/io/parquet/ParquetWriterBenchmark.java b/src/test/java/org/apache/sysds/test/functions/io/parquet/ParquetWriterBenchmark.java new file mode 100644 index 00000000000..902931a5adf --- /dev/null +++ b/src/test/java/org/apache/sysds/test/functions/io/parquet/ParquetWriterBenchmark.java @@ -0,0 +1,274 @@ +/* + * 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.sysds.test.functions.io.parquet; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.apache.parquet.hadoop.metadata.CompressionCodecName; +import org.apache.sysds.common.Types.ValueType; +import org.apache.sysds.runtime.frame.data.FrameBlock; +import org.apache.sysds.runtime.io.FrameWriterParquet; +import org.apache.sysds.runtime.io.FrameWriterParquet.DictEncoding; +import org.junit.After; +import org.junit.Assume; + +/** + * Parquet writer benchmark using the TPC-H lineitem dataset. + * Writes results to temp/benchmark_results.csv for plotting. + * + * The benchmark methods are disabled by default; uncomment to run manually. + * If a dataset is not present inside the temp dir the corresponding test is skipped with instructions. + * + * The default maxRows=2_000_000, to benchmark row-group + * size properly, run on a machine with enough RAM for a larger frame: + * # 1. Generate a larger TPC-H lineitem into temp/lineitem.tbl + * # 2. Raise the maxRows cap, then run the benchmark: + * mvn test -Dtest=ParquetWriterBenchmark#benchmarkRowGroupSizes \ + * -DmaxRows=60000000 -DargLine="-Xms24g -Xmx24g" -DfailIfNoTests=false + */ +public class ParquetWriterBenchmark { + + private static final String TPCH_FILE = "temp/lineitem.tbl"; + private static final String RESULTS_CSV = "temp/benchmark_results.csv"; + private static final String TEMP_FILE = System.getProperty("java.io.tmpdir") + "/systemds_tpch_bench.parquet"; + private static final int RUNS = 3; + private static final int MAX_ROWS = Integer.getInteger("maxRows", 2_000_000); + private static final int[] BATCH_SIZES = { 1, 100, 500, 1000, 5000, 10_000, 50_000, 100_000, 200_000 }; + private static final long[] ROW_GROUP_SIZES = { + 1024 * 1024, // 1 MB + 8L * 1024 * 1024, // 8 MB + 16L * 1024 * 1024, // 16 MB + 32L * 1024 * 1024, // 32 MB + 64L * 1024 * 1024, // 64 MB + 128L * 1024 * 1024, // 128 MB (Parquet default) + 256L * 1024 * 1024, // 256 MB + 512L * 1024 * 1024 // 512 MB + }; + + // TPC-H lineitem schema + private static final ValueType[] LINEITEM_SCHEMA = { + ValueType.INT64, // orderkey + ValueType.INT64, // partkey + ValueType.INT64, // suppkey + ValueType.INT32, // linenumber + ValueType.FP64, // quantity + ValueType.FP64, // extendedprice + ValueType.FP64, // discount + ValueType.FP64, // tax + ValueType.STRING, // returnflag (3 unique values) + ValueType.STRING, // linestatus (2 unique values) + ValueType.STRING, // shipdate + ValueType.STRING, // commitdate + ValueType.STRING, // receiptdate + ValueType.STRING, // shipinstruct (4 unique values) + ValueType.STRING, // shipmode (7 unique values) + ValueType.STRING // comment + }; + + private static final String[] LINEITEM_NAMES = { + "orderkey", "partkey", "suppkey", "linenumber", + "quantity", "extendedprice", "discount", "tax", + "returnflag", "linestatus", "shipdate", "commitdate", + "receiptdate", "shipinstruct", "shipmode", "comment" + }; + + private PrintWriter csv; + + @After + public void cleanup() { + new File(TEMP_FILE).delete(); + if (csv != null) csv.close(); + } + + // @Test + public void benchmarkWriters() throws Exception { + FrameBlock data = loadOrSkip(); + int rows = data.getNumRows(); + + openCsv(); + System.out.println("\n=== TPC-H Writer Benchmark (" + rows + " rows, median of " + RUNS + " runs) ===\n"); + System.out.printf("%-38s %12s %15s%n", "Configuration", "Time (ms)", "Rows/sec"); + System.out.println("-".repeat(70)); + + FrameWriterParquet newWriter = new FrameWriterParquet(CompressionCodecName.UNCOMPRESSED, DictEncoding.ALL_ON); + + // Warmup + new File(TEMP_FILE).delete(); + new FrameWriterParquetLegacy(1000).writeFrameToHDFS(data, TEMP_FILE, rows, data.getNumColumns()); + new File(TEMP_FILE).delete(); + newWriter.writeFrameToHDFS(data, TEMP_FILE, rows, data.getNumColumns()); + + for (int batchSize : BATCH_SIZES) { + long[] times = new long[RUNS]; + for (int run = 0; run < RUNS; run++) { + new File(TEMP_FILE).delete(); + long start = System.currentTimeMillis(); + new FrameWriterParquetLegacy(batchSize).writeFrameToHDFS(data, TEMP_FILE, rows, data.getNumColumns()); + times[run] = System.currentTimeMillis() - start; + } + long med = median(times); + String label = "Legacy batchSize=" + batchSize; + System.out.printf("%-38s %12d %15.0f%n", label, med, rows * 1000.0 / med); + csv.printf("batch_sizes,%s,%d,%.0f,,%n", label, med, rows * 1000.0 / med); + } + + System.out.println("-".repeat(70)); + + long[] times = new long[RUNS]; + for (int run = 0; run < RUNS; run++) { + new File(TEMP_FILE).delete(); + long start = System.currentTimeMillis(); + newWriter.writeFrameToHDFS(data, TEMP_FILE, rows, data.getNumColumns()); + times[run] = System.currentTimeMillis() - start; + } + long med = median(times); + System.out.printf("%-38s %12d %15.0f%n", "New WriteSupport", med, rows * 1000.0 / med); + csv.printf("batch_sizes,New WriteSupport,%d,%.0f,,%n", med, rows * 1000.0 / med); + System.out.println(); + } + + // @Test + public void benchmarkDictionaryEncoding() throws Exception { + FrameBlock data = loadOrSkip(); + int rows = data.getNumRows(); + + openCsv(); + System.out.println("\n=== TPC-H Dictionary Encoding Benchmark (" + rows + " rows, median of " + RUNS + " runs) ===\n"); + System.out.printf("%-20s %12s %15s%n", "Strategy", "Time (ms)", "Rows/sec"); + System.out.println("-".repeat(52)); + + time("encoding", "ALL_ON", new FrameWriterParquet(CompressionCodecName.UNCOMPRESSED, DictEncoding.ALL_ON), data, rows); + time("encoding", "ALL_OFF", new FrameWriterParquet(CompressionCodecName.UNCOMPRESSED, DictEncoding.ALL_OFF), data, rows); + time("encoding", "STRING_ONLY", new FrameWriterParquet(CompressionCodecName.UNCOMPRESSED, DictEncoding.STRING_ONLY), data, rows); + System.out.println(); + } + + + // @Test + public void benchmarkRowGroupSizes() throws Exception { + FrameBlock data = loadOrSkip(); + int rows = data.getNumRows(); + + openCsv(); + System.out.println("\n=== TPC-H Row Group Size Benchmark (" + rows + " rows, median of " + RUNS + " runs) ===\n"); + System.out.printf("%-20s %12s %15s%n", "Row Group Size", "Time (ms)", "Rows/sec"); + System.out.println("-".repeat(52)); + + for (long rowGroupSize : ROW_GROUP_SIZES) { + String label = (rowGroupSize / (1024 * 1024)) + "MB"; + time("row_group_sizes", label, + new FrameWriterParquet(CompressionCodecName.ZSTD, DictEncoding.ALL_ON, rowGroupSize), data, rows); + } + System.out.println(); + } + + private FrameBlock loadOrSkip() throws Exception { + File f = new File(TPCH_FILE); + if (!f.exists()) { + System.out.println(); + System.out.println("==================================================="); + System.out.println("TPC-H benchmark skipped, dataset not found"); + System.out.println("To reproduce:"); + System.out.println(" 1. Install DuckDB: https://duckdb.org"); + System.out.println(" 2. Open shell: duckdb"); + System.out.println(" 3. Run in DuckDB: INSTALL tpch;"); + System.out.println(" LOAD tpch;"); + System.out.println(" CALL dbgen(sf=1);"); + System.out.println(" COPY lineitem TO '/temp/lineitem.tbl'"); + System.out.println(" (DELIMITER '|', HEADER false);"); + System.out.println("==================================================="); + Assume.assumeTrue("TPC-H dataset not found at " + TPCH_FILE, false); + } + return loadLineitem(f); + } + + private void openCsv() throws Exception { + new File("temp").mkdirs(); + boolean exists = new File(RESULTS_CSV).exists(); + csv = new PrintWriter(new FileWriter(RESULTS_CSV, true)); + if (!exists) + csv.println("benchmark,label,time_ms,rows_per_sec,size_mb,compression_ratio"); + csv.flush(); + } + + private void time(String category, String label, FrameWriterParquet writer, FrameBlock data, int rows) throws Exception { + new File(TEMP_FILE).delete(); + writer.writeFrameToHDFS(data, TEMP_FILE, rows, data.getNumColumns()); // warmup + + long[] times = new long[RUNS]; + for (int run = 0; run < RUNS; run++) { + new File(TEMP_FILE).delete(); + long start = System.currentTimeMillis(); + writer.writeFrameToHDFS(data, TEMP_FILE, rows, data.getNumColumns()); + times[run] = System.currentTimeMillis() - start; + } + long med = median(times); + System.out.printf("%-20s %12d %15.0f%n", label, med, rows * 1000.0 / med); + csv.printf("%s,%s,%d,%.0f,,%n", category, label, med, rows * 1000.0 / med); + } + + private long timeWithSize(String category, String label, FrameWriterParquet writer, FrameBlock data, int rows, long baseSize) throws Exception { + new File(TEMP_FILE).delete(); + writer.writeFrameToHDFS(data, TEMP_FILE, rows, data.getNumColumns()); // warmup + + long[] times = new long[RUNS]; + for (int run = 0; run < RUNS; run++) { + new File(TEMP_FILE).delete(); + long start = System.currentTimeMillis(); + writer.writeFrameToHDFS(data, TEMP_FILE, rows, data.getNumColumns()); + times[run] = System.currentTimeMillis() - start; + } + long med = median(times); + long size = new File(TEMP_FILE).length(); + double mb = size / (1024.0 * 1024.0); + String ratio = baseSize > 0 ? String.format("%.2fx", (double) baseSize / size) : "baseline"; + System.out.printf("%-20s %12d %15.0f %12.2f %10s%n", label, med, rows * 1000.0 / med, mb, ratio); + csv.printf("%s,%s,%d,%.0f,%.2f,%s%n", category, label, med, rows * 1000.0 / med, mb, ratio); + return size; + } + + private static long median(long[] times) { + long[] sorted = times.clone(); + Arrays.sort(sorted); + return sorted[sorted.length / 2]; + } + + private static FrameBlock loadLineitem(File f) throws Exception { + System.out.print("Loading " + f.getPath() + " ... "); + List rows = new ArrayList<>(); + try (BufferedReader br = new BufferedReader(new FileReader(f))) { + String line; + while ((line = br.readLine()) != null && rows.size() < MAX_ROWS) { + if (line.isEmpty()) continue; + if (line.endsWith("|")) line = line.substring(0, line.length() - 1); + rows.add(line.split("\\|", -1)); + } + } + String[][] data = rows.toArray(new String[0][]); + System.out.println(data.length + " rows loaded."); + return new FrameBlock(LINEITEM_SCHEMA, LINEITEM_NAMES, data); + } +} diff --git a/src/test/java/org/apache/sysds/test/functions/io/parquet/ReadParquetTest.java b/src/test/java/org/apache/sysds/test/functions/io/parquet/ReadParquetTest.java new file mode 100644 index 00000000000..c6b4ffe6ef5 --- /dev/null +++ b/src/test/java/org/apache/sysds/test/functions/io/parquet/ReadParquetTest.java @@ -0,0 +1,271 @@ +/* + * 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.sysds.test.functions.io.parquet; + +import java.io.File; +import java.nio.file.Files; +import java.util.HashSet; +import java.util.Set; + +import org.apache.sysds.common.Types.ValueType; +import org.apache.sysds.runtime.frame.data.FrameBlock; +import org.apache.sysds.runtime.io.FrameReaderParquet; +import org.apache.sysds.runtime.io.FrameReaderParquetParallel; +import org.apache.sysds.runtime.io.FrameWriterParquet; +import org.apache.sysds.runtime.io.FrameWriterParquetParallel; +import org.apache.sysds.test.functions.io.parquet.ParquetTestUtils.ParquetMetadataInfo; +import org.apache.sysds.test.TestUtils; +import org.junit.Assert; +import org.junit.Test; + +public class ReadParquetTest { + + private static final String[] FILENAMES = { + "src/test/resources/datasets/parquet/userdata1.parquet", // https://github.com/duckdb/duckdb/blob/main/data/parquet-testing/userdata1.parquet + "src/test/resources/datasets/parquet/alltypes_plain.parquet", // https://github.com/apache/parquet-testing/blob/master/data/alltypes_plain.parquet + "src/test/resources/datasets/parquet/all.parquet" // https://huggingface.co/datasets/cardiffnlp/databench/blob/main/data/002_Titanic/all.parquet + }; + + @Test + public void testReadParquet() throws Exception { + for (String filename : FILENAMES) { + ParquetMetadataInfo info = ParquetTestUtils.inferMetadata(filename); + + FrameReaderParquet reader = new FrameReaderParquet(); + FrameBlock frame = reader.readFrameFromHDFS(filename, info.schema, info.names, info.rlen, info.clen); + + Assert.assertEquals("Row count mismatch for " + filename, info.rlen, frame.getNumRows()); + Assert.assertEquals("Column count mismatch for " + filename, info.clen, frame.getNumColumns()); + } + } + + @Test + public void testInt96ColumnsDecodedCorrectly() throws Exception { + assertColumnIsEpochMillis("src/test/resources/datasets/parquet/userdata1.parquet", 0); + assertColumnIsEpochMillis("src/test/resources/datasets/parquet/alltypes_plain.parquet", 10); + } + + private void assertColumnIsEpochMillis(String filename, int colIdx) throws Exception { + ParquetMetadataInfo info = ParquetTestUtils.inferMetadata(filename); + FrameReaderParquet reader = new FrameReaderParquet(); + FrameBlock frame = reader.readFrameFromHDFS(filename, info.schema, info.names, info.rlen, info.clen); + int decoded = 0; + for (int r = 0; r < frame.getNumRows(); r++) { + Object val = frame.get(r, colIdx); + if (val == null) continue; + decoded++; + Assert.assertTrue( + "Expected Long (epoch millis) at row " + r + " col " + colIdx + ", got: " + val.getClass().getSimpleName(), + val instanceof Long + ); + } + Assert.assertTrue("No INT96 values were decoded in " + filename, decoded > 0); + } + + @Test + public void testNullHandling() throws Exception { + File temp = Files.createTempFile("systemds_null_parquet", ".parquet").toFile(); + try { + ValueType[] schema = { ValueType.STRING, ValueType.STRING }; + String[] names = { "a", "b" }; + FrameBlock original = new FrameBlock(schema, names, + new String[][] { { "x", "y" }, { null, null }, { "p", "q" } }); + + new FrameWriterParquet().writeFrameToHDFS(original, temp.getPath(), 3, 2); + + FrameBlock result = new FrameReaderParquet() + .readFrameFromHDFS(temp.getPath(), schema, names, 3, 2); + + Assert.assertNotNull("Row 0 col 0 should be non-null", result.get(0, 0)); + Assert.assertNotNull("Row 0 col 1 should be non-null", result.get(0, 1)); + Assert.assertNull("Row 1 col 0 should be null", result.get(1, 0)); + Assert.assertNull("Row 1 col 1 should be null", result.get(1, 1)); + Assert.assertNotNull("Row 2 col 0 should be non-null", result.get(2, 0)); + Assert.assertNotNull("Row 2 col 1 should be non-null", result.get(2, 1)); + } finally { + temp.delete(); + } + + } + + @Test + public void testColumnReaderMatchesLegacy() throws Exception { + for (String filename : FILENAMES) { + ParquetMetadataInfo info = ParquetTestUtils.inferMetadata(filename); + + FrameBlock legacy = new FrameReaderParquetLegacy() + .readFrameFromHDFS(filename, info.schema, info.names, info.rlen, info.clen); + FrameBlock current = new FrameReaderParquet() + .readFrameFromHDFS(filename, info.schema, info.names, info.rlen, info.clen); + + TestUtils.compareFrames(legacy, current, false); + } + } + + @Test + public void testColumnSubsetProjection() throws Exception { + // request a reordered subset of columns (d, a, c; skip b) + File temp = Files.createTempFile("systemds_subset_parquet", ".parquet").toFile(); + try { + ValueType[] fullSchema = { ValueType.INT64, ValueType.STRING, ValueType.FP64, ValueType.BOOLEAN }; + String[] fullNames = { "a", "b", "c", "d" }; + FrameBlock original = new FrameBlock(fullSchema, fullNames, new String[][] { + { "10", "x", "1.5", "true" }, + { "20", "y", "2.5", "false" }, + { "30", "z", "3.5", "true" } + }); + new FrameWriterParquet().writeFrameToHDFS(original, temp.getPath(), 3, 4); + + ValueType[] subSchema = { ValueType.BOOLEAN, ValueType.INT64, ValueType.FP64 }; + String[] subNames = { "d", "a", "c" }; + + FrameBlock legacy = new FrameReaderParquetLegacy() + .readFrameFromHDFS(temp.getPath(), subSchema, subNames, 3, 3); + FrameBlock current = new FrameReaderParquet() + .readFrameFromHDFS(temp.getPath(), subSchema, subNames, 3, 3); + + TestUtils.compareFrames(legacy, current, false); + + Assert.assertEquals(true, current.get(0, 0)); // d + Assert.assertEquals(10L, current.get(0, 1)); // a + Assert.assertEquals(3.5, ((Number) current.get(2, 2)).doubleValue(), 0.0); // c + } finally { + temp.delete(); + } + } + + @Test + public void testEmptyFile() throws Exception { + File temp = Files.createTempFile("systemds_empty_parquet", ".parquet").toFile(); + try { + ValueType[] schema = { ValueType.INT64, ValueType.STRING }; + String[] names = { "a", "b" }; + FrameBlock empty = new FrameBlock(schema, names, new String[0][]); + new FrameWriterParquet().writeFrameToHDFS(empty, temp.getPath(), 0, 2); + + FrameBlock legacy = new FrameReaderParquetLegacy() + .readFrameFromHDFS(temp.getPath(), schema, names, 0, 2); + FrameBlock current = new FrameReaderParquet() + .readFrameFromHDFS(temp.getPath(), schema, names, 0, 2); + + Assert.assertEquals("Empty file should yield 0 rows (legacy)", 0, legacy.getNumRows()); + Assert.assertEquals("Empty file should yield 0 rows (column API)", 0, current.getNumRows()); + Assert.assertEquals(2, current.getNumColumns()); + } finally { + temp.delete(); + } + } + + @Test + public void testParallelReaderMatchesSequential() throws Exception { + for (String filename : FILENAMES) { + ParquetMetadataInfo info = ParquetTestUtils.inferMetadata(filename); + + FrameReaderParquet sequential = new FrameReaderParquet(); + FrameBlock expected = sequential.readFrameFromHDFS(filename, info.schema, info.names, info.rlen, info.clen); + + FrameReaderParquetParallel parallel = new FrameReaderParquetParallel(); + FrameBlock actual = parallel.readFrameFromHDFS(filename, info.schema, info.names, info.rlen, info.clen); + + TestUtils.compareFrames(expected, actual, false); + } + } + + @Test + public void testParallelReaderMultiFileOffsets() throws Exception { + // each file must use its own row range, else the parallel reader overwrites/loses rows + File tempDir = Files.createTempDirectory("systemds_parallel_parquet").toFile(); + try { + ValueType[] schema = { ValueType.STRING, ValueType.INT32 }; + String[] names = { "label", "value" }; + + FrameBlock block1 = new FrameBlock(schema, names, + new String[][] { { "a", "1" }, { "b", "2" }, { "c", "3" } }); + FrameBlock block2 = new FrameBlock(schema, names, + new String[][] { { "d", "4" }, { "e", "5" }, { "f", "6" } }); + + String path1 = tempDir + "/part-0.parquet"; + String path2 = tempDir + "/part-1.parquet"; + FrameWriterParquet writer = new FrameWriterParquet(); + writer.writeFrameToHDFS(block1, path1, 3, 2); + writer.writeFrameToHDFS(block2, path2, 3, 2); + + FrameReaderParquet seq = new FrameReaderParquet(); + Set expectedLabels = new HashSet<>(); + String[] parts = { path1, path2 }; + for (String p : parts) { + FrameBlock fb = seq.readFrameFromHDFS(p, schema, names, 3, 2); + for (int r = 0; r < fb.getNumRows(); r++) + expectedLabels.add((String) fb.get(r, 0)); + } + + FrameReaderParquetParallel parallel = new FrameReaderParquetParallel(); + FrameBlock result = parallel.readFrameFromHDFS(tempDir.toString(), schema, names, 6, 2); + + Assert.assertEquals("Expected 6 total rows", 6, result.getNumRows()); + + Set actualLabels = new HashSet<>(); + for (int r = 0; r < result.getNumRows(); r++) { + Object label = result.get(r, 0); + Assert.assertNotNull("Row " + r + " is null, row-offset bug suspected", label); + actualLabels.add((String) label); + } + Assert.assertEquals("Parallel result does not match sequential ground truth", expectedLabels, actualLabels); + } finally { + for (File f : tempDir.listFiles()) + f.delete(); + tempDir.delete(); + } + } + + @Test + public void testParallelWriterRoundTrip() throws Exception { + File tempDir = Files.createTempDirectory("systemds_parallel_write").toFile(); + try { + ValueType[] schema = { ValueType.INT64, ValueType.STRING, ValueType.FP64 }; + String[] names = { "id", "name", "val" }; + String[][] data = new String[20][]; + for (int i = 0; i < 20; i++) + data[i] = new String[] { String.valueOf(i), "row" + i, String.valueOf(i + 0.5) }; + FrameBlock original = new FrameBlock(schema, names, data); + + FrameWriterParquetParallel writer = new FrameWriterParquetParallel(); + writer.setForcedParallel(true); + writer.writeFrameToHDFS(original, tempDir.getPath(), 20, 3); + + FrameBlock result = new FrameReaderParquetParallel() + .readFrameFromHDFS(tempDir.getPath(), schema, names, 20, 3); + + Assert.assertEquals("Row count mismatch after parallel write", 20, result.getNumRows()); + // Rows may be out of order in parallel reads, validate by comparing tuples, not row positions + Set expected = new HashSet<>(); + for (int i = 0; i < 20; i++) + expected.add(i + "|row" + i + "|" + (i + 0.5)); + Set actual = new HashSet<>(); + for (int r = 0; r < result.getNumRows(); r++) + actual.add(result.get(r, 0) + "|" + result.get(r, 1) + "|" + result.get(r, 2)); + Assert.assertEquals("Parallel-written data does not round-trip", expected, actual); + } finally { + for (File f : tempDir.listFiles()) + f.delete(); + tempDir.delete(); + } + } +} diff --git a/src/test/java/org/apache/sysds/test/functions/io/parquet/WriteParquetTest.java b/src/test/java/org/apache/sysds/test/functions/io/parquet/WriteParquetTest.java new file mode 100644 index 00000000000..576253a754d --- /dev/null +++ b/src/test/java/org/apache/sysds/test/functions/io/parquet/WriteParquetTest.java @@ -0,0 +1,165 @@ +/* + * 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.sysds.test.functions.io.parquet; + +import java.io.File; + +import org.apache.sysds.test.TestUtils; +import org.apache.sysds.common.Types.ValueType; +import org.apache.sysds.runtime.frame.data.FrameBlock; +import org.apache.sysds.runtime.frame.data.columns.Array; +import org.apache.sysds.runtime.frame.data.columns.ArrayFactory; +import org.apache.sysds.runtime.io.FrameReaderParquet; +import org.apache.sysds.runtime.io.FrameReaderParquetParallel; +import org.apache.sysds.runtime.io.FrameWriterParquet; +import org.apache.sysds.runtime.io.FrameWriterParquetParallel; +import org.apache.sysds.test.functions.io.parquet.ParquetTestUtils.ParquetMetadataInfo; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; + +public class WriteParquetTest { + + private static final String TEMP_FILE = System.getProperty("java.io.tmpdir") + "/systemds_write_parquet_test.parquet"; + private static final String TEMP_PAR_PATH = System.getProperty("java.io.tmpdir") + "/systemds_write_parquet_test_par"; + + @After + public void cleanup() { + new File(TEMP_FILE).delete(); + deleteRecursive(new File(TEMP_PAR_PATH)); + } + + private static void deleteRecursive(File f) { + if (f.isDirectory()) + for (File c : f.listFiles()) deleteRecursive(c); + f.delete(); + } + + @Test + public void testRoundtripPublicFiles() throws Exception { + String[] files = { + "src/test/resources/datasets/parquet/userdata1.parquet", + "src/test/resources/datasets/parquet/alltypes_plain.parquet", + "src/test/resources/datasets/parquet/all.parquet" + }; + + for (String filename : files) { + ParquetMetadataInfo info = ParquetTestUtils.inferMetadata(filename); + + FrameReaderParquet reader = new FrameReaderParquet(); + FrameBlock original = reader.readFrameFromHDFS(filename, info.schema, info.names, info.rlen, info.clen); + + FrameWriterParquet writer = new FrameWriterParquet(); + writer.writeFrameToHDFS(original, TEMP_FILE, original.getNumRows(), original.getNumColumns()); + + FrameBlock result = reader.readFrameFromHDFS(TEMP_FILE, info.schema, info.names, info.rlen, info.clen); + + TestUtils.compareFrames(original, result, false); + } + } + + @Test + public void testMultiPartFileRoundtrip() throws Exception { + // Create two parquet part files and verify that the parallel reader + // reads both files correctly and combines them into the expected result. + ValueType[] schema = { + ValueType.STRING, + ValueType.INT32, + ValueType.INT64, + ValueType.FP32, + ValueType.FP64, + ValueType.BOOLEAN + }; + String[] names = { "name", "age", "id", "score", "ratio", "active" }; + String[][] data = { + { "Alice", "30", "1000", "1.5", "0.75", "true" }, + { "Bob", "25", "2000", "2.5", "0.50", "false" }, + { "Carol", "40", "3000", "3.5", "0.25", "true" }, + { "Dave", "35", "4000", "4.5", "0.10", "false" }, + { "Eve", "28", "5000", "5.5", "0.90", "true" }, + { "Frank", "45", "6000", "6.5", "0.60", "false" } + }; + FrameBlock original = new FrameBlock(schema, names, data); + + new File(TEMP_PAR_PATH).mkdir(); + FrameWriterParquet writer = new FrameWriterParquet(); + writer.writeFrameToHDFS(original.slice(0, 2), TEMP_PAR_PATH + "/part-0.parquet", 3, schema.length); + writer.writeFrameToHDFS(original.slice(3, 5), TEMP_PAR_PATH + "/part-1.parquet", 3, schema.length); + + FrameBlock result = new FrameReaderParquetParallel() + .readFrameFromHDFS(TEMP_PAR_PATH, schema, names, 6, schema.length); + + TestUtils.compareFrames(original, result, false); + } + + @Test + public void testParallelRoundtrip() throws Exception { + ValueType[] schema = { + ValueType.STRING, + ValueType.INT32, + ValueType.INT64, + ValueType.FP32, + ValueType.FP64, + ValueType.BOOLEAN + }; + String[] names = { "name", "age", "id", "score", "ratio", "active" }; + String[][] data = { + { "Alice", "30", "1000", "1.5", "0.75", "true" }, + { "Bob", "25", "2000", "2.5", "0.50", "false" }, + { "Carol", "40", "3000", "3.5", "0.25", "true" } + }; + FrameBlock original = new FrameBlock(schema, names, data); + + new FrameWriterParquetParallel().writeFrameToHDFS(original, TEMP_PAR_PATH, original.getNumRows(), original.getNumColumns()); + FrameBlock result = new FrameReaderParquetParallel().readFrameFromHDFS(TEMP_PAR_PATH, schema, names, original.getNumRows(), original.getNumColumns()); + + TestUtils.compareFrames(original, result, false); + } + + @Test + public void testRoundtrip() throws Exception { + ValueType[] schema = { + ValueType.STRING, + ValueType.INT32, + ValueType.INT64, + ValueType.FP32, + ValueType.FP64, + ValueType.BOOLEAN + }; + String[] names = { "name test", "age", "id", "score", "ratio", "active" }; + String[][] data = { + { "Alice", "30", "1000", "1.5", "0.75", "true" }, + { "Bob", "25", "2000", "2.5", "0.50", "false" }, + { "Carol", "40", "3000", "3.5", "0.25", "true" } + }; + + FrameBlock original = new FrameBlock(schema, names, data); + + // Write + FrameWriterParquet writer = new FrameWriterParquet(); + writer.writeFrameToHDFS(original, TEMP_FILE, original.getNumRows(), original.getNumColumns()); + + // Read back + FrameReaderParquet reader = new FrameReaderParquet(); + FrameBlock result = reader.readFrameFromHDFS(TEMP_FILE, schema, names, original.getNumRows(), original.getNumColumns()); + + TestUtils.compareFrames(original, result, false); + } +} diff --git a/src/test/resources/datasets/parquet/all.parquet b/src/test/resources/datasets/parquet/all.parquet new file mode 100644 index 0000000000000000000000000000000000000000..e139fcd00cc4ec10df6bd15991318db4380499c5 GIT binary patch literal 25613 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 E86*m+KL7v# literal 0 HcmV?d00001 diff --git a/src/test/resources/datasets/parquet/alltypes_plain.parquet b/src/test/resources/datasets/parquet/alltypes_plain.parquet new file mode 100644 index 0000000000000000000000000000000000000000..a63f5dca7c3821909748f34752966a0d7e08d47f GIT binary patch literal 1851 zcmb7F&ui0g6#pje+AM2l7<*qVv_zM;YQD;X!1G_`Ye@W}TbqmweOL$NPTX=lg!8G{0y<66Rp8 z2pS|Aqlfj;PSH-&mT4zwizU$p1|0Y`VGJoyS_YbwNId;`jBg|z+DN8!b`S=|Sr$Ee51+|8u<)E>*X#arx$Xz24Q}8O`6X`}Xhr%F1Zjm_hG6In z7b&rg?-Cs*15K~?$g4Hmpi6uSk7fKqck31Rd$NO@X;dxW?*`sZ;$!02EAVEj1Dx*0 z{MLuNloZ0uLp~A&5eQYhXi-eh3&y9k4#_aQs_m^t;cL8xuhMu#`94GW^Wlpd7r_2h zbWlRre%G&Crz3oz;A@fee~~T(>&n~(=)0;8>IvyeeZ%&hb^-l_Dsj zFvuM<3gd=3Zp;SqWJI2b$YdaF$o()3pD7?YQ98!mj1HO5|D}r6be0>LFTr05hN163Gw zN`^s(6x}&&X(N%RnM7u%??nSFr{~`PZ?MG}V6i6>#vU;kXJ%l`=Er#5j4|7?$M(UP z-GIH6Am3BD#zq&s>YC+S`G?MW!>iZw=2&6OxPE8h?#;!8`C@+5-thcNe#V-dsZ?y! oaow58so4p;;FgVPyFBeqnNHc9FdWl$-SX^Jc0`|z5`8xR0vs|-V*mgE literal 0 HcmV?d00001 diff --git a/src/test/resources/datasets/parquet/userdata1.parquet b/src/test/resources/datasets/parquet/userdata1.parquet new file mode 100644 index 0000000000000000000000000000000000000000..2ae23dac0ffe794068f26c163b7245b3509f31c5 GIT binary patch literal 113629 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 DbfIxZ literal 0 HcmV?d00001 From e0916e16e0343e14e6b6375a4e48d305acaf0ad0 Mon Sep 17 00:00:00 2001 From: Jakob-al28 Date: Thu, 2 Jul 2026 09:41:59 +0000 Subject: [PATCH 2/2] Generate parquet test files with Spark --- .../io/parquet/ParquetTestUtils.java | 133 ++++++++++++++++++ .../functions/io/parquet/ReadParquetTest.java | 31 +++- .../io/parquet/WriteParquetTest.java | 35 +++-- .../resources/datasets/parquet/all.parquet | Bin 25613 -> 0 bytes .../datasets/parquet/alltypes_plain.parquet | Bin 1851 -> 0 bytes .../datasets/parquet/userdata1.parquet | Bin 113629 -> 0 bytes 6 files changed, 182 insertions(+), 17 deletions(-) delete mode 100644 src/test/resources/datasets/parquet/all.parquet delete mode 100644 src/test/resources/datasets/parquet/alltypes_plain.parquet delete mode 100644 src/test/resources/datasets/parquet/userdata1.parquet diff --git a/src/test/java/org/apache/sysds/test/functions/io/parquet/ParquetTestUtils.java b/src/test/java/org/apache/sysds/test/functions/io/parquet/ParquetTestUtils.java index da28809333e..9ce52887cee 100644 --- a/src/test/java/org/apache/sysds/test/functions/io/parquet/ParquetTestUtils.java +++ b/src/test/java/org/apache/sysds/test/functions/io/parquet/ParquetTestUtils.java @@ -19,7 +19,15 @@ package org.apache.sysds.test.functions.io.parquet; +import java.io.File; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.sql.Timestamp; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; @@ -29,8 +37,18 @@ import org.apache.parquet.hadoop.util.HadoopInputFile; import org.apache.parquet.schema.MessageType; import org.apache.parquet.schema.PrimitiveType; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.RowFactory; +import org.apache.spark.sql.SaveMode; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.types.DataTypes; +import org.apache.spark.sql.types.Metadata; +import org.apache.spark.sql.types.StructField; +import org.apache.spark.sql.types.StructType; import org.apache.sysds.common.Types.ValueType; import org.apache.sysds.conf.ConfigurationManager; +import org.apache.sysds.test.AutomatedTestBase; class ParquetTestUtils { @@ -82,4 +100,119 @@ static ParquetMetadataInfo inferMetadata(String fname) throws IOException { info.clen = fieldCount; return info; } + + /** + * Generates the public test files (userdata1, alltypes_plain, all) with Spark's + * DataFrameWriter. userdata1 and alltypes_plain each include a TimestampType column, + * which Spark 3.5 encodes as INT96 by default. + * + * @param outDir directory the generated files are written into + * @return map from file name (e.g. "userdata1") to its generated file path + */ + static Map generatePublicTestFiles(File outDir) throws Exception { + SparkSession spark = AutomatedTestBase.createSystemDSSparkSession("parquet-test-files", "local[1]"); + + Map files = new LinkedHashMap<>(); + files.put("userdata1", writeTestFile(spark, outDir, "userdata1", userdata1Rows(), userdata1Schema())); + files.put("alltypes_plain", writeTestFile(spark, outDir, "alltypes_plain", alltypesPlainRows(), alltypesPlainSchema())); + files.put("all", writeTestFile(spark, outDir, "all", allRows(), allSchema())); + return files; + } + + private static StructType userdata1Schema() { + return new StructType(new StructField[] { + new StructField("registration_dttm", DataTypes.TimestampType, true, Metadata.empty()), + new StructField("id", DataTypes.IntegerType, true, Metadata.empty()), + new StructField("first_name", DataTypes.StringType, true, Metadata.empty()), + new StructField("salary", DataTypes.DoubleType, true, Metadata.empty()), + }); + } + + private static List userdata1Rows() { + return Arrays.asList( + RowFactory.create(Timestamp.valueOf("2016-02-03 07:55:29"), 1, "Amanda", 49756.53), + RowFactory.create(Timestamp.valueOf("2016-02-03 17:04:03"), 2, "Albert", 150280.17), + RowFactory.create(Timestamp.valueOf("2016-02-03 01:09:31"), 3, "Evelyn", 144972.51), + RowFactory.create((Timestamp) null, 4, "Denise", 90263.05), + RowFactory.create(Timestamp.valueOf("2016-02-03 05:07:25"), 5, "Carlos", 75500.34) + ); + } + + private static StructType alltypesPlainSchema() { + return new StructType(new StructField[] { + new StructField("id", DataTypes.IntegerType, true, Metadata.empty()), + new StructField("bool_col", DataTypes.BooleanType, true, Metadata.empty()), + new StructField("tinyint_col", DataTypes.IntegerType, true, Metadata.empty()), + new StructField("smallint_col", DataTypes.IntegerType, true, Metadata.empty()), + new StructField("bigint_col", DataTypes.LongType, true, Metadata.empty()), + new StructField("float_col", DataTypes.FloatType, true, Metadata.empty()), + new StructField("double_col", DataTypes.DoubleType, true, Metadata.empty()), + new StructField("date_string_col", DataTypes.StringType, true, Metadata.empty()), + new StructField("string_col", DataTypes.StringType, true, Metadata.empty()), + new StructField("timestamp_col", DataTypes.TimestampType, true, Metadata.empty()), + }); + } + + private static List alltypesPlainRows() { + return Arrays.asList( + RowFactory.create(1, true, 1, 10, 100L, 1.5f, 2.25, "03/01/09", "row-1", Timestamp.valueOf("2009-03-01 00:00:00")), + RowFactory.create(2, false, 2, 20, 200L, 2.5f, 4.5, "03/02/09", "row-2", Timestamp.valueOf("2009-03-02 05:15:30")), + RowFactory.create(3, true, 3, 30, 300L, 3.5f, 6.75, "03/03/09", "row-3", Timestamp.valueOf("2009-03-03 10:30:00")), + RowFactory.create(4, false, 4, 40, 400L, 4.5f, 9.0, "03/04/09", "row-4", Timestamp.valueOf("2009-03-04 15:45:15")), + RowFactory.create(5, true, 5, 50, 500L, 5.5f, 11.25, "03/05/09", "row-5", Timestamp.valueOf("2009-03-05 21:00:45")), + RowFactory.create(6, false, 6, 60, 600L, 6.5f, 13.5, "03/06/09", "row-6", Timestamp.valueOf("2009-03-06 02:20:10")), + RowFactory.create(7, true, 7, 70, 700L, 7.5f, 15.75, "03/07/09", "row-7", Timestamp.valueOf("2009-03-07 08:35:50")), + RowFactory.create(8, false, 8, 80, 800L, 8.5f, 18.0, "03/08/09", "row-8", Timestamp.valueOf("2009-03-08 13:50:25")) + ); + } + + private static StructType allSchema() { + return new StructType(new StructField[] { + new StructField("PassengerId", DataTypes.IntegerType, true, Metadata.empty()), + new StructField("Survived", DataTypes.IntegerType, true, Metadata.empty()), + new StructField("Pclass", DataTypes.IntegerType, true, Metadata.empty()), + new StructField("Name", DataTypes.StringType, true, Metadata.empty()), + new StructField("Sex", DataTypes.StringType, true, Metadata.empty()), + new StructField("Age", DataTypes.DoubleType, true, Metadata.empty()), + new StructField("Fare", DataTypes.DoubleType, true, Metadata.empty()), + new StructField("Embarked", DataTypes.StringType, true, Metadata.empty()), + }); + } + + private static List allRows() { + return Arrays.asList( + RowFactory.create(1, 0, 3, "Braund, Mr. Owen Harris", "male", 22.0, 7.25, "S"), + RowFactory.create(2, 1, 1, "Cumings, Mrs. John Bradley", "female", 38.0, 71.2833, "C"), + RowFactory.create(3, 1, 3, "Heikkinen, Miss. Laina", "female", 26.0, 7.925, "S"), + RowFactory.create(4, 1, 1, "Futrelle, Mrs. Jacques Heath", "female", 35.0, 53.1, "S"), + RowFactory.create(5, 0, 3, "Allen, Mr. William Henry", "male", null, 8.05, "S"), + RowFactory.create(6, 0, 3, "Moran, Mr. James", "male", null, 8.4583, "Q"), + RowFactory.create(7, 0, 1, "McCarthy, Mr. Timothy J", "male", 54.0, 51.8625, "S"), + RowFactory.create(8, 0, 3, "Palsson, Master. Gosta Leonard", "male", 2.0, 21.075, "S") + ); + } + + // Spark writes a directory of part files, so we force one partition and rename it to a single file. + private static String writeTestFile(SparkSession spark, File outDir, String name, List rows, StructType schema) throws IOException { + Dataset df = spark.createDataFrame(rows, schema); + File tmpDir = new File(outDir, "_tmp_" + name); + df.coalesce(1).write().mode(SaveMode.Overwrite).parquet(tmpDir.getPath()); + + File[] parts = tmpDir.listFiles((d, n) -> n.startsWith("part-") && n.endsWith(".parquet")); + if (parts == null || parts.length != 1) + throw new IOException("expected exactly 1 part file in " + tmpDir); + + File dest = new File(outDir, name + ".parquet"); + Files.copy(parts[0].toPath(), dest.toPath(), StandardCopyOption.REPLACE_EXISTING); + deleteRecursive(tmpDir); + return dest.getPath(); + } + + private static void deleteRecursive(File f) { + File[] children = f.listFiles(); + if (children != null) + for (File c : children) + deleteRecursive(c); + f.delete(); + } } diff --git a/src/test/java/org/apache/sysds/test/functions/io/parquet/ReadParquetTest.java b/src/test/java/org/apache/sysds/test/functions/io/parquet/ReadParquetTest.java index c6b4ffe6ef5..4c7f2ccae2e 100644 --- a/src/test/java/org/apache/sysds/test/functions/io/parquet/ReadParquetTest.java +++ b/src/test/java/org/apache/sysds/test/functions/io/parquet/ReadParquetTest.java @@ -22,6 +22,7 @@ import java.io.File; import java.nio.file.Files; import java.util.HashSet; +import java.util.Map; import java.util.Set; import org.apache.sysds.common.Types.ValueType; @@ -32,16 +33,32 @@ import org.apache.sysds.runtime.io.FrameWriterParquetParallel; import org.apache.sysds.test.functions.io.parquet.ParquetTestUtils.ParquetMetadataInfo; import org.apache.sysds.test.TestUtils; +import org.junit.AfterClass; import org.junit.Assert; +import org.junit.BeforeClass; import org.junit.Test; public class ReadParquetTest { - private static final String[] FILENAMES = { - "src/test/resources/datasets/parquet/userdata1.parquet", // https://github.com/duckdb/duckdb/blob/main/data/parquet-testing/userdata1.parquet - "src/test/resources/datasets/parquet/alltypes_plain.parquet", // https://github.com/apache/parquet-testing/blob/master/data/alltypes_plain.parquet - "src/test/resources/datasets/parquet/all.parquet" // https://huggingface.co/datasets/cardiffnlp/databench/blob/main/data/002_Titanic/all.parquet - }; + // Generated once per test class with Spark's DataFrameWriter + private static File testFileDir; + private static String[] FILENAMES; + + @BeforeClass + public static void generateTestFiles() throws Exception { + testFileDir = Files.createTempDirectory("systemds_parquet_public_test_files").toFile(); + Map files = ParquetTestUtils.generatePublicTestFiles(testFileDir); + FILENAMES = new String[] { files.get("userdata1"), files.get("alltypes_plain"), files.get("all") }; + } + + @AfterClass + public static void cleanupTestFiles() { + File[] children = testFileDir.listFiles(); + if (children != null) + for (File f : children) + f.delete(); + testFileDir.delete(); + } @Test public void testReadParquet() throws Exception { @@ -58,8 +75,8 @@ public void testReadParquet() throws Exception { @Test public void testInt96ColumnsDecodedCorrectly() throws Exception { - assertColumnIsEpochMillis("src/test/resources/datasets/parquet/userdata1.parquet", 0); - assertColumnIsEpochMillis("src/test/resources/datasets/parquet/alltypes_plain.parquet", 10); + assertColumnIsEpochMillis(FILENAMES[0], 0); // userdata1: registration_dttm + assertColumnIsEpochMillis(FILENAMES[1], 9); // alltypes_plain: timestamp_col } private void assertColumnIsEpochMillis(String filename, int colIdx) throws Exception { diff --git a/src/test/java/org/apache/sysds/test/functions/io/parquet/WriteParquetTest.java b/src/test/java/org/apache/sysds/test/functions/io/parquet/WriteParquetTest.java index 576253a754d..e8a6c5b3599 100644 --- a/src/test/java/org/apache/sysds/test/functions/io/parquet/WriteParquetTest.java +++ b/src/test/java/org/apache/sysds/test/functions/io/parquet/WriteParquetTest.java @@ -20,19 +20,20 @@ package org.apache.sysds.test.functions.io.parquet; import java.io.File; +import java.nio.file.Files; +import java.util.Map; import org.apache.sysds.test.TestUtils; import org.apache.sysds.common.Types.ValueType; import org.apache.sysds.runtime.frame.data.FrameBlock; -import org.apache.sysds.runtime.frame.data.columns.Array; -import org.apache.sysds.runtime.frame.data.columns.ArrayFactory; import org.apache.sysds.runtime.io.FrameReaderParquet; import org.apache.sysds.runtime.io.FrameReaderParquetParallel; import org.apache.sysds.runtime.io.FrameWriterParquet; import org.apache.sysds.runtime.io.FrameWriterParquetParallel; import org.apache.sysds.test.functions.io.parquet.ParquetTestUtils.ParquetMetadataInfo; import org.junit.After; -import org.junit.Assert; +import org.junit.AfterClass; +import org.junit.BeforeClass; import org.junit.Test; public class WriteParquetTest { @@ -40,6 +41,26 @@ public class WriteParquetTest { private static final String TEMP_FILE = System.getProperty("java.io.tmpdir") + "/systemds_write_parquet_test.parquet"; private static final String TEMP_PAR_PATH = System.getProperty("java.io.tmpdir") + "/systemds_write_parquet_test_par"; + // See ParquetTestUtils.generatePublicTestFiles(): these are generated with Spark's DataFrameWriter + private static File testFileDir; + private static String[] PUBLIC_FILES; + + @BeforeClass + public static void generateTestFiles() throws Exception { + testFileDir = Files.createTempDirectory("systemds_parquet_public_test_files").toFile(); + Map files = ParquetTestUtils.generatePublicTestFiles(testFileDir); + PUBLIC_FILES = new String[] { files.get("userdata1"), files.get("alltypes_plain"), files.get("all") }; + } + + @AfterClass + public static void cleanupTestFiles() { + File[] children = testFileDir.listFiles(); + if (children != null) + for (File f : children) + f.delete(); + testFileDir.delete(); + } + @After public void cleanup() { new File(TEMP_FILE).delete(); @@ -54,13 +75,7 @@ private static void deleteRecursive(File f) { @Test public void testRoundtripPublicFiles() throws Exception { - String[] files = { - "src/test/resources/datasets/parquet/userdata1.parquet", - "src/test/resources/datasets/parquet/alltypes_plain.parquet", - "src/test/resources/datasets/parquet/all.parquet" - }; - - for (String filename : files) { + for (String filename : PUBLIC_FILES) { ParquetMetadataInfo info = ParquetTestUtils.inferMetadata(filename); FrameReaderParquet reader = new FrameReaderParquet(); diff --git a/src/test/resources/datasets/parquet/all.parquet b/src/test/resources/datasets/parquet/all.parquet deleted file mode 100644 index e139fcd00cc4ec10df6bd15991318db4380499c5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25613 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 E86*m+KL7v# diff --git a/src/test/resources/datasets/parquet/alltypes_plain.parquet b/src/test/resources/datasets/parquet/alltypes_plain.parquet deleted file mode 100644 index a63f5dca7c3821909748f34752966a0d7e08d47f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1851 zcmb7F&ui0g6#pje+AM2l7<*qVv_zM;YQD;X!1G_`Ye@W}TbqmweOL$NPTX=lg!8G{0y<66Rp8 z2pS|Aqlfj;PSH-&mT4zwizU$p1|0Y`VGJoyS_YbwNId;`jBg|z+DN8!b`S=|Sr$Ee51+|8u<)E>*X#arx$Xz24Q}8O`6X`}Xhr%F1Zjm_hG6In z7b&rg?-Cs*15K~?$g4Hmpi6uSk7fKqck31Rd$NO@X;dxW?*`sZ;$!02EAVEj1Dx*0 z{MLuNloZ0uLp~A&5eQYhXi-eh3&y9k4#_aQs_m^t;cL8xuhMu#`94GW^Wlpd7r_2h zbWlRre%G&Crz3oz;A@fee~~T(>&n~(=)0;8>IvyeeZ%&hb^-l_Dsj zFvuM<3gd=3Zp;SqWJI2b$YdaF$o()3pD7?YQ98!mj1HO5|D}r6be0>LFTr05hN163Gw zN`^s(6x}&&X(N%RnM7u%??nSFr{~`PZ?MG}V6i6>#vU;kXJ%l`=Er#5j4|7?$M(UP z-GIH6Am3BD#zq&s>YC+S`G?MW!>iZw=2&6OxPE8h?#;!8`C@+5-thcNe#V-dsZ?y! oaow58so4p;;FgVPyFBeqnNHc9FdWl$-SX^Jc0`|z5`8xR0vs|-V*mgE diff --git a/src/test/resources/datasets/parquet/userdata1.parquet b/src/test/resources/datasets/parquet/userdata1.parquet deleted file mode 100644 index 2ae23dac0ffe794068f26c163b7245b3509f31c5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 113629 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 DbfIxZ