arrow2

repository·main·Indexed 21 days ago

https://github.com/jorgecarleitao/arrow2

An unofficial, 'transmute-free' and safe Rust implementation of the Apache Arrow columnar memory format. It provides high-performance read/write capabilities for Parquet, Avro, IPC, CSV, JSON/NDJSON, and ODBC, and supports the Arrow C Data and Stream interfaces for zero-copy interoperability. The library includes a comprehensive compute API for analytics operations and a flexible array system with both immutable and mutable variants. Note: As of 2024-01-17, this crate is no longer maintained.

Tokens
30.6K
Snippets
94
Records
130
Agent score
76%

What's inside arrow2

  1. What is Apache Arrow and the Arrow2 crate?

    main

    Apache Arrow is a language-independent columnar memory format designed for efficient analytic operations on modern hardware (CPUs/GPUs). It provides a standardized specification for how data types (integers, floats, strings, lists, etc.) are stored in memory.

    The Arrow2 crate is a Rust implementation of these specifications. It provides the structs and implementations necessary to create Arrow arrays that follow the Apache Arrow standard, enabling high-performance data processing in Rust.

    Key Benefits of the Arrow Format:

    • Fast in-memory data access: By using a columnar representation instead of a row-based one, similar data types are stored contiguously in memory. This improves cache locality and enables faster columnar querying and SIMD optimizations.
    • Zero-copy data sharing: Because the memory format is standardized, different processes (even those written in different languages like Python/Pandas or Scala/Spark) can share the same in-memory data without the overhead of serialization, copying, or conversion.
    • Efficient Inter-process Communication (IPC): Data can be sent across networks or between processes as structured packets (Chunks/RecordBatches) that are ready to use immediately upon receipt without decoding.
  2. Overview of Arrow2 features and capabilities

    main

    Arrow2 is a Rust implementation of the Apache Arrow format designed to be 'transmute-free'. It is a feature-complete implementation (second only to the C++ reference implementation) with the following key capabilities:

    Data Interoperability

    • C Data Interface: Supports reading and writing all Arrow types at zero-copy.
    • C Stream Interface: Supports reading and writing all Arrow types.
    • Rust Interop: Full interoperability with Rust's Vec.
    • MutableArray API: Allows working with bitmaps and arrays in-place.
    • Timestamps: Full support for timestamps with timezones, including timezone-aware arithmetic.

    Supported Formats (Read/Write)

    • CSV
    • Apache Arrow IPC (all types)
    • Apache Arrow Flight (all types)
    • Apache Parquet (except deep nested types)
    • Apache Avro (all types)
    • NJSON
    • ODBC (some types)

    Compute Operations

    Includes an extensive suite of operations such as:

    • Aggregations and arithmetics
    • Cast, comparison, and boolean logic (including Kleene logic)
    • Sort and merge-sort
    • Filter and take
    • Hash operations
    • If-then-else and nullif
    • Temporal operations (day, month, week day, hour, etc.)
    • Window functions
  3. Overview of arrow2 capabilities

    main

    arrow2 is a library for efficient in-memory data operations using the Arrow in-memory format. It is designed as a bottom-up rewrite of the official arrow crate, focusing on soundness and type safety.

    Key capabilities include:

    • Array Operations: Creating and manipulating various array types.
    • Computation: High-performance arithmetic and compute operations on arrays.
    • Schema Management: Defining data structures using Schema and Field.
    • I/O Support: Reading and writing data in formats like Parquet, CSV, JSON, and Arrow IPC.
    • Chunking: Managing data in Chunk structures.
    use std::sync::Arc;
    
    use arrow2::array::*
    use arrow2::datatypes::{Field, DataType, Schema};
    use arrow2::compute::arithmetics;
    use arrow2::error::Result;
    use arrow2::io::parquet::write::*;
    use arrow2::chunk::Chunk;
    
    fn main() -> Result<()> {
        // declare arrays
        let a = Int32Array::from(&[Some(1), None, Some(3)]);
        let b = Int32Array::from(&[Some(2), None, Some(6)]);
    
        // compute
        let c = arithmetics::basic::mul_scalar(&a, &2);
        assert_eq!(c, b);
    
        // declare a schema with fields
        let schema = Schema::from(vec![
            Field::new("c1", DataType::Int32, true),
            Field::new("c2", DataType::Int32, true),
        ]);
    
        // declare chunk
        let chunk = Chunk::new(vec![a.arced(), b.arced()]);
    
        // write to parquet
        let options = WriteOptions {
            write_statistics: true,
            compression: CompressionOptions::Snappy,
            version: Version::V1,
            data_pagesize_limit: None,
        };
    
        let row_groups = RowGroupIterator::try_new(
            vec![Ok(chunk)].into_iter(),
            &schema,
            options,
            vec![vec![Encoding::Plain], vec![Encoding::Plain]],
        )?;
    
        let mut file = vec![];
        let mut writer = FileWriter::try_new(file, schema, options)?;
    
        for group in row_groups {
            writer.write(group?)?;
        }
        let _ = writer.end(None)?;
        Ok(())
    }
  4. Overview of Arrow2 APIs

    main

    Arrow2 is a Rust library designed for interoperability with the Arrow format, optimized for CPU and memory-intensive analytics. It supports heterogeneous data structures, null values, and IPC/FFI interfaces across different languages. The library is organized into five primary API categories:

    1. Low-level API: For efficient operations on contiguous memory regions.
    2. High-level API: For operating directly with Arrow arrays.
    3. Metadata API: For declaring and managing logical types and metadata.
    4. Compute API: Provides operators to perform computations over arrays.
    5. IO API: Provides interfaces for reading from and writing to various formats, including:
      • Arrow: IPC files, IPC streams, and memory-mapped files.
      • CSV: Reading and writing CSV files.
      • Parquet: Reading and writing Parquet files.
      • JSON/NDJSON: Reading and writing JSON and NDJSON.
      • Avro: Reading and writing Avro files.
      • ODBC: Reading and writing via ODBC.
  5. Overview of supported Compute operations

    main

    The compute module supports a wide variety of operations including:

    • Arithmetic: Checked and saturating operations.
    • Reductions: sum, min, and max.
    • Transformations: unary, binary, cast, and if-then-else.
    • Comparisons: Comparison operations.
    • Array Manipulation: take, filter, concat, sort, hash, and merge-sort.
    • Null Handling: nullif.
    • String Operations: length and regex.
    • Temporal Operations: hour, year, month, and iso_week for temporal logical types.
    • List Operations: contains for list types.
  6. Understand the Scalar API design

    main

    In arrow2, a Scalar is a trait object used to represent single values. It is designed as a companion to the Array API and follows similar principles:

    • Small Memory Footprint: By using a trait object instead of an enum, Scalar ensures a consistent, small memory footprint regardless of the underlying physical type.
    • Forward Compatibility: Using a trait object allows for new types to be added without breaking backward compatibility (which would happen with an enum).
    • Abstraction: Implementation details are hidden from the user to reduce the public API surface.
    • Physical Type Mapping: There is exactly one implementation per Arrow physical type. This simplifies user code by reducing the number of match arms required and allows for logical type casting without altering the underlying physical representation.
  7. How logical and physical types are separated

    main

    The crate maintains a strict separation between physical and logical types to ensure type safety and clarity:

    • Physical types: These are implemented using Rust generics.
    • Logical types: These are implemented using variables (such as enum values) and are declared and implemented within the datatypes module.
  8. Safety and security considerations in Arrow2

    main

    Arrow2 uses unsafe code strictly for FFI and when the compiler cannot prove specific invariants (such as UTF-8 invariants defined in the Arrow format or using nightly features like TrustedLen).

    Security Notes

    • MIRI: Extensive tests are run under MIRI to validate unsafe blocks.
    • Vulnerability Monitoring: The project monitors Rust advisory databases.
    • Panic Risk: Reading from untrusted Apache Parquet or Apache Avro data currently may cause a panic!. This is a known issue being addressed.
  9. Use parallelism to decouple CSV serialization from I/O

    main

    To improve performance when serialization is CPU-bound and writing is I/O-bound, you can decouple these two processes. By offloading serialization to other threads, you can trade off higher memory usage for increased throughput. This is achieved by performing serialization and writing in a way that allows them to run concurrently rather than synchronously.

    // Note: This example demonstrates how to offload serialization 
    // to other threads to handle CPU-bound workloads.
  10. Understand logical types with `DataType`

    main

    In Arrow2, logical types are defined using the arrow2::datatypes::DataType enum. These types provide semantic meaning to raw data.

    Each DataType maps to a PhysicalType (the in-memory representation). This is a many-to-one relationship: multiple logical types can share the same physical representation but differ in semantics. For example, DataType::Date32 and DataType::Int32 both use PhysicalType::Primitive(PrimitiveType::Int32), but Date32 is interpreted as the number of days since the UNIX epoch.

  11. Design patterns for IO module implementations

    main

    The io module in arrow2 follows a specific architectural pattern to ensure usability and performance. When working with or extending IO functionality, keep these design principles in mind:

    1. Dependency Management

    • Feature Gating: Any directory depending on external dependencies must be feature-gated using a prefix io_ (e.g., io_csv).
    • API Re-exports: To ensure a seamless developer experience, modules must re-export the APIs of external dependencies they use. This allows users to use the crate without adding extra dependencies to their own Cargo.toml.
      • Example: If a module provides write(writer: &mut csv::Writer<W>, ...), it must include pub use csv::Writer;.

    2. Module Structure

    • Each format directory (e.g., csv, json) should contain two sub-directories: read and write.
    • The base module for a format should re-export these sub-modules: pub use read; and pub use write;.

    3. Separation of Concerns

    • Data vs. Metadata: Reading data should be separated from reading metadata. Schema inference or schema reading should be distinct functions. Functions that read actual data should consume a schema that has been pre-read.
    • IO vs. CPU Bounds: To allow consumers to optimize for performance, implementations must separate IO-bound operations from CPU-bound operations:
      1. IO-bound: Functions that consume a Read implementor and output a "raw" struct (e.g., compressed or serialized data).
      2. CPU-bound: Functions that consume a "raw" struct and convert it into Arrow format.

    These functions must be offered as independent public APIs so consumers can decide how to balance IO and CPU workloads.