ParquetSharp

repository·master·Indexed 42 days ago

https://github.com/g-research/parquetsharp

A high-performance, cross-platform .NET library for reading and writing Apache Parquet files. It acts as a PInvoke wrapper around the official Apache Parquet C++ implementation and provides both a low-level API and an Arrow-based API for zero-copy performance using Apache Arrow.

Tokens
23.3K
Snippets
58
Records
72
Agent score
53%

What's inside ParquetSharp

  1. Work with Map (Dictionary) data in Parquet

    master

    The Parquet Map logical type is a special case of nested data. Because ParquetSharp's API works with leaf-level columns, .NET Dictionary objects cannot be represented as a single column. Instead, a Map is represented as two separate columns:

    1. A column containing arrays of keys.
    2. A column containing arrays of values.

    For a valid Map, the arrays in both columns must have the same length for every row.

    Writing Map data

    To write a dictionary, you must split your Dictionary<K, V> collection into two arrays (keys and values) and define a schema with a GroupNode annotated with LogicalType.Map() containing a repeated key_value group.

    Reading Map data

    To read a Map, read the key column and the value column separately using LogicalReader, then use LINQ (e.g., Zip) to reconstruct the Dictionary objects in memory.

    // Writing: Split dictionaries into keys and values
    var dictionaries = new[] { new Dictionary<string, int>{{"a", 0}} };
    var keys = dictionaries.Select(d => d.Keys.ToArray()).ToArray();
    var values = dictionaries.Select(d => d.Values.ToArray()).ToArray();
    
    // Schema for Map
    using var keyNode = new PrimitiveNode("key", Repetition.Required, LogicalType.String(), PhysicalType.ByteArray);
    using var valueNode = new PrimitiveNode("value", Repetition.Required, LogicalType.None(), PhysicalType.Int32);
    using var keyValueNode = new GroupNode("key_value", Repetition.Repeated, new Node[] {keyNode, valueNode});
    using var mapNode = new GroupNode("map_column", Repetition.Required, new Node[] {keyValueNode}, LogicalType.Map());
    using var schema = new GroupNode("schema", Repetition.Required, new Node[] {mapNode});
    
    // Reading: Reconstruct dictionaries
    var readDictionaries = readKeys.Zip(
        readValues, (dictKeys, dictValues) => new Dictionary<string, int>(
            dictKeys.Zip(dictValues, (k, v) => new KeyValuePair<string, int>(k, v)))
        ).ToArray();
  2. Choosing the right Visitor pattern in ParquetSharp

    master

    Select a visitor based on the level of abstraction and the information required:

    Visitor TypeUse Case
    ILogicalColumnWriterVisitor / ILogicalColumnReaderVisitorHigh-level, type-safe reading/writing when types are unknown at compile time. Best for generic tooling and data exporters.
    IColumnWriterVisitor / IColumnReaderVisitorLow-level operations requiring access to definition levels, repetition levels, or physical encodings.
    IColumnDescriptorVisitorMetadata only. Inspecting schema without performing I/O (validation, type checking).

    When to avoid visitors: If you already know the schema at compile time, use the generic LogicalWriter<T> or LogicalReader<T> APIs instead. They are simpler and more maintainable.

  3. Compare ParquetSharp reading APIs

    master

    ParquetSharp provides three primary ways to read Parquet data, each with different trade-offs between ease of use and memory efficiency:

    1. LogicalColumnReader (ParquetFileReader): Provides direct, column-oriented access. It is type-safe and offers predictable memory usage. This is generally the best choice for high-performance, memory-optimized workloads.
    2. Arrow API (FileReader): Uses Apache Arrow's in-memory columnar format. It is highly tunable for memory efficiency via pre-buffering settings and is ideal for row-oriented access patterns using columnar data.
    3. Row-Oriented API: A high-level abstraction that iterates over rows sequentially. While convenient and easy to use, it is generally not memory-efficient and is not recommended for large-scale performance-critical workloads.
  4. Use the Row-oriented API for convenient Parquet writing and reading

    master

    The row-oriented API provides a high-level abstraction over Parquet's column-oriented nature, allowing you to work with whole rows (e.g., using tuples or custom structs) instead of individual columns. This results in more readable code but comes with a performance trade-off: all values in a row group must be stored in memory simultaneously, and internal buffers are resized and copied as they grow.

    When to use: Use this API for convenience and readability in non-performance-critical paths. For high-performance requirements, use the lower-level column-oriented API.

    Key Operations:

    • ParquetFile.CreateRowWriter<T>(path, columns): Creates a writer for a specific row type T.
    • rowWriter.WriteRow(row): Writes a single row.
    • rowWriter.StartNewRowGroup(): Manually starts a new row group.
    • rowWriter.Close(): Finalizes the file.
    • ParquetFile.CreateRowReader<T>(path): Creates a reader for a specific row type T.
    • rowReader.ReadRows(rowGroupIndex): Reads all rows within a specific row group.
    var columns = new[] {"Timestamp", "ObjectId", "Value"};
    using var rowWriter = ParquetFile.CreateRowWriter<(DateTime, int, float)>("data.parquet", columns);
    
    // Writing rows
    rowWriter.WriteRow((DateTime.Now, 1, 0.5f));
    
    // Starting a new row group
    rowWriter.StartNewRowGroup();
    rowWriter.WriteRow((DateTime.Now, 2, 0.7f));
    
    rowWriter.Close();
  5. Optimize memory usage with Arrow API Pre-buffering

    master

    When using the Arrow API (FileReader), you can control memory usage through ArrowReaderProperties:

    • Pre-buffering: By default, the Arrow API may prefetch multiple row groups ahead of time to improve performance, which increases memory usage.
    • Disabling Pre-buffering: Setting arrowProps.PreBuffer = false ensures memory consumption stays close to the size of the data currently being processed, significantly reducing peak memory usage.
  6. Understand ParquetSharp compression codecs

    master

    ParquetSharp supports several compression codecs to reduce file size:

    • None: Uncompressed data. Serves as a baseline for size and provides maximum compatibility and fastest reads.
    • Snappy: Provides fast compression with a moderate reduction ratio. It is often the best choice for a balance of write speed and size reduction, especially when paired with ByteStreamSplit.
    • Zstd: Provides a higher compression ratio (smaller files) but results in slower compression times compared to Snappy.
  7. Understand ParquetSharp encoding strategies for floats

    master

    ParquetSharp supports different encoding strategies that impact how floating-point data is stored on disk:

    • Plain (no dictionary): Writes raw float values as-is. This avoids the overhead of dictionary lookups and is preferred for high-cardinality float data.
    • Plain (dictionary enabled): Attempts to build a dictionary of unique float values per row group. This is only beneficial if floats repeat frequently. Note: This is the default behavior.
    • ByteStreamSplit (no dictionary): Rearranges the bytes of each float so that all sign/exponent bytes are grouped together. This improves compressibility by allowing block compressors to find long runs of similar bytes in the sign/exponent planes. This is highly recommended for scientific float data.
  8. How Type Factories work in ParquetSharp

    master

    ParquetSharp uses Type Factories to map .NET types (logical system types) to Parquet physical types and handle the associated conversions. This mechanism allows users to:

    1. Read/Write custom types: By providing a mapping between a user-defined type and a Parquet physical type.
    2. Override default mappings: Change how standard .NET types are handled globally or per-file.

    Core Components

    • @ParquetSharp.LogicalTypeFactory: Manages the mapping between logical and physical types. It can be subclassed for custom logic or configured via properties for simple changes.
    • @ParquetSharp.LogicalReadConverterFactory: Used by ParquetFileReader to tell readers how to convert physical Parquet data into user-defined logical types.
    • @ParquetSharp.LogicalWriteConverterFactory: Used by ParquetFileWriter to tell writers how to convert user-defined logical types into physical Parquet data.

    When to use which factory

    • LogicalTypeFactory is required if you use a custom type in a Column object during writing, or if you access a reader/writer via non-overriding methods (where the factory is needed to establish the full type mapping).
    • Converter Factories (LogicalReadConverterFactory or LogicalWriteConverterFactory) are always required when using a custom type that the default converters do not support.
  9. Handle Int96 Timestamps

    master
    Legacy Parquet files may use the deprecated Int96 primitive type for timestamps. ParquetSharp does not support reading Int96 directly as .NET DateTime because not all Int96 values are representable as DateTime. Instead, you can read them as raw ParquetSharp.Int96 values and interpret them manually.
  10. Optimize memory usage with ReaderProperties

    master

    You can tune the memory footprint of your Parquet reading operations using ReaderProperties:

    • Buffer Size: Controlled via ReaderProperties.BufferSize. Larger buffers improve throughput by reducing disk I/O operations but increase memory usage. Smaller buffers are more memory-efficient.
    • Buffered Stream: Enable buffered streaming to reduce memory consumption. Use readerProps.EnableBufferedStream() to activate this mode.
    • Chunked Reading: Instead of loading entire columns or row groups, process data in smaller chunks (e.g., using ReadBatch in a loop) to spread memory usage over time and reduce peak consumption.
  11. Write encrypted Parquet files using Key Management Tools

    master

    Writing encrypted files involves three main steps:

    1. Configure KMS Connection: Set up a KmsConnectionConfig with your service details.
    2. Configure Encryption: Create an EncryptionConfiguration specifying the footerKeyId. You can enable UniformEncryption (all columns use the same master key) or provide a ColumnKeys dictionary for per-column master keys. You can also toggle DoubleWrapping or set PlaintextFooter = true to allow schema reading without decryption.
    3. Generate Properties: Use a CryptoFactory to generate FileEncryptionProperties, then pass these to a WriterPropertiesBuilder to create WriterProperties for your ParquetFileWriter.

    Note: Use the Key Management Tools API if you require compatibility with PyArrow.

    
    using var kmsConnectionConfig = new KmsConnectionConfig();
    // Set fields required by your IKmsClient implementation
    kmsConnectionConfig.KmsInstanceId = ...;
    
    string footerKeyId = ...;
    using var encryptionConfig = new EncryptionConfiguration(footerKeyId);
    // Optional: encryptionConfig.UniformEncryption = true;
    // Optional: encryptionConfig.DoubleWrapping = false;
    
    using var fileEncryptionProperties = cryptoFactory.GetFileEncryptionProperties(
        kmsConnectionConfig, encryptionConfig);
    
    using var writerPropertiesBuilder = new WriterPropertiesBuilder();
    using var writerProperties = writerPropertiesBuilder
                    .Compression(Compression.Snappy)
                    .Encryption(fileEncryptionProperties)
                    .Build();
    
    Column[] columns = ...;
    using var fileWriter = new ParquetFileWriter(parquetFilePath, columns, writerProperties);
    // Write data with fileWriter