Skip to content

wireform-arrow

wireform-arrow implements Apache Arrow IPC and the Arrow columnar data model in Haskell. Arrow is the interchange format between analytics engines, dataframe libraries, and columnar storage. Use this package when you need typed record batches, schema-aware encoding, or a shared column vocabulary that Parquet and ORC readers in wireform can target.

  • Schema framing and IPC message encode/decode via Arrow.IPC
  • Record batch encode and decode for in-memory columnar data
  • Typed record API with Template Haskell and Generic support
  • Table projection and subsetting to read only the columns you need
  • Optional compression (Zstd and LZ4 behind Cabal flags)
  • Zero-copy decode: columns are Arrow-native buffers that alias the input; only offsets, UTF-8 and dictionary keys are validated (C kernels)
  • Single-allocation encode, plus lazy encoders whose chunks alias the column buffers
  • Streaming reader for framed IPC streams in Arrow.Stream

Define a record type, build a Table, encode a vector of records to an Arrow IPC stream with Arrow.Stream, and decode it back:

{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE OverloadedStrings #-}
module Trades where
import Arrow.Record
import Arrow.Stream (decodeArrowStream, defaultWriteOptions, encodeArrowStream)
import Data.ByteString (ByteString)
import Data.Int (Int32)
import Data.Text (Text)
import qualified Data.Vector as V
data Trade = Trade
{ tradeSym :: !Text
, tradeQty :: !Int32
, tradeNote :: !(Maybe Text)
}
deriving stock (Show, Eq)
tradeTable :: Table Trade
tradeTable = table enc dec
where
enc =
fieldE "sym" tradeSym utf8E
<> fieldE "qty" tradeQty int32E
<> fieldE "note" tradeNote (nullable utf8E)
dec =
Trade
<$> columnD "sym" utf8D
<*> columnD "qty" int32D
<*> columnD "note" (nullableD utf8D)
-- | One record batch in an IPC stream. 'Left' when the columns do not
-- fit the schema (they always do for a 'Table').
encodeTrades :: V.Vector Trade -> Either String ByteString
encodeTrades trades =
let (schema, columns) = encodeTable tradeTable trades
in encodeArrowStream defaultWriteOptions schema [columns]
-- | Every record of every batch in the stream.
decodeTrades :: ByteString -> Either String (V.Vector Trade)
decodeTrades bytes = do
(schema, batches) <- decodeArrowStream bytes
V.concat <$> traverse (decodeTable tradeTable schema) batches
roundTripTrades :: V.Vector Trade -> Either String (V.Vector Trade)
roundTripTrades trades = encodeTrades trades >>= decodeTrades

encodeArrowFile / decodeArrowFile do the same for the Arrow file format. The writers check every batch against the schema and return Left for a mismatch, or for dictionaries they cannot write (an emit-once dictionary larger than its index type can address).

Project column batches down to a subset of fields when the full schema is larger than what your query needs:

{-# LANGUAGE OverloadedStrings #-}
import Arrow.Column (ColumnArray)
import Arrow.Record (projectTable)
import Arrow.Types (Schema)
import qualified Data.Vector as V
projectSymQty :: Schema -> V.Vector ColumnArray -> Maybe (Schema, V.Vector ColumnArray)
projectSymQty schema cols =
projectTable ["sym", "qty"] schema cols

Arrow.File reads the file format and exposes the raw record batches; Arrow.Stream.openStreamReader reads a stream one batch at a time.

A ColumnArray holds Arrow’s own buffers: storable vectors for fixed-width values and offsets, LSB-first bitmaps for validity and booleans, and ByteString regions for variable-length data. Nullability is per value: every array with a validity slot carries Maybe Validity, Nothing meaning no nulls. The patterns (ColPrim, ColInt64, ColUtf8, …) only match; build with primColumn, the from* conversions, the builders, or the validating mk* constructors:

{-# LANGUAGE OverloadedStrings #-}
module Build where
import Arrow.Column
import Control.Monad.ST (runST)
import qualified Data.Vector as V
import qualified Data.Vector.Storable as VS
-- O(1): the vector becomes the values buffer.
prices :: ColumnArray
prices = primColumn PDouble (VS.fromList [101.5, 99.25, 100])
-- A builder appends into pinned, growable buffers; the validity bitmap
-- is allocated on the first null.
quantities :: ColumnArray
quantities = runST $ do
b <- newPrimBuilder PInt64 3
appendPrim b 10
appendNull b
appendPrim b 30
freezeBuilder b
-- Convenience constructors from boxed vectors (O(n)).
notes :: ColumnArray
notes = fromMaybeTexts (V.fromList [Just "filled", Nothing, Just "late"])
-- Nested columns: build the children, then validate with mk*.
fills :: Either String ColumnArray
fills = mkList Nothing (VS.fromList [0, 2, 2, 3]) (primColumn PInt32 (VS.fromList [5, 7, 9]))

Read through typed views: bind asPrim, asUtf8, asBinary or asBool once per column, then index with primAt, textAt, bytesAt or boolArrayAt (listRange for list and map rows, dictKeyAt for dictionary rows).

For a whole column of Haskell values, the to*Vector conversions return an Arrow.Vector.Vector: a data family (like Data.Vector.Unboxed) used through Data.Vector.Generic with element types such as Maybe Int64 or Maybe Text, stored the way the column is. A Vector (Maybe Int64) is the column’s validity bitmap plus its storable values (8 bytes and one bit per row, no heap object per row), and fused folds over it compile to a loop that allocates nothing per row. toMaybeVector and toBoolVector are O(1) (they alias the column; a column without nulls gets a fresh all-set bitmap); toTextVector, toBytesVector and toListVector are O(rows) (offsets widened to Int), with every row a slice of the column’s data: Text rows are neither copied nor re-validated (data outside the GHC heap, such as an mmapped file, is copied once), views copy their bytes once, and dictionaries convert their values once and share them. The inverses fromMaybeVector and fromBoolVector are O(1) plus a popcount. G.convert gives a boxed Data.Vector; G.force copies the rows into fresh buffers, detaching them from the input.

{-# LANGUAGE BangPatterns #-}
module Read where
import Arrow.Column
import Data.Int (Int64)
import Data.Text (Text)
import qualified Data.Vector as V
import qualified Data.Vector.Generic as G
-- Bind the typed view once, then index; the loop does not allocate.
sumQuantities :: ColumnArray -> Maybe Int64
sumQuantities c = do
p <- asPrim PInt64 c
let n = primArrayLength p
go !acc i
| i >= n = acc
| otherwise = go (acc + maybe 0 id (primAt p i)) (i + 1)
pure (go 0 0)
-- The same sum over the whole column: O(1) conversion, fused fold.
sumQuantities' :: ColumnArray -> Maybe Int64
sumQuantities' c = G.foldl' (\acc m -> maybe acc (+ acc) m) 0 . toMaybeVector <$> asPrim PInt64 c
-- One copy into a Text, no UTF-8 re-validation.
noteAt :: ColumnArray -> Int -> Maybe Text
noteAt c i = asUtf8 c >>= \u -> textAt u i
-- A boxed Data.Vector when an API wants one.
quantityList :: ColumnArray -> Maybe (V.Vector (Maybe Int64))
quantityList c = G.convert . toMaybeVector <$> asPrim PInt64 c

Decoded columns alias the input bytes and keep them alive. Holding a small column from a large message pins the whole message; copyColumn copies only the bytes the column references:

module Lifetime where
import Arrow.Column (ColumnArray, copyColumn)
import Arrow.Stream (decodeArrowStream)
import Data.ByteString (ByteString)
import qualified Data.Vector as V
-- Decoded columns alias 'bytes'. Keeping one small column from a large
-- message would keep the whole message alive; copyColumn detaches it.
firstColumn :: ByteString -> Either String ColumnArray
firstColumn bytes = do
(_, batches) <- decodeArrowStream bytes
case batches of
cols : _ | not (V.null cols) -> Right (copyColumn (V.head cols))
_ -> Left "no columns"

Dictionary keys are an integer column at the wire width of the field’s index type and carry the row validity. Eq is logical: same type, same length, the same validity per row and equal values in valid rows; null slot contents, offsets, slices and dictionary layouts do not matter.

The eager encoders write the whole output into one allocation. The lazy encoders return chunks that alias the column buffers, so their cost follows the buffer count rather than the data size:

module Lazy where
import Arrow.Column (ColumnArray)
import Arrow.Stream (defaultWriteOptions, encodeArrowStreamLazy)
import Arrow.Types (Schema)
import qualified Data.ByteString.Lazy as BL
import qualified Data.Vector as V
import System.IO (Handle)
-- The chunks are the message headers and the column buffers themselves:
-- no body copy, so the cost tracks the buffer count, not the data size.
writeBatches :: Handle -> Schema -> [V.Vector ColumnArray] -> IO (Either String ())
writeBatches h schema batches =
traverse (BL.hPut h) (encodeArrowStreamLazy defaultWriteOptions schema batches)

The boxed constructors and their *Maybe twins (ColInt64Maybe (V.Vector (Maybe Int64)), ColUtf8 (V.Vector Text), ColListMaybe valid offsets child, …) are gone. ColInt64Maybe v becomes fromMaybes PInt64 v; ColUtf8Maybe v becomes fromMaybeTexts v; ColListMaybe valid offs child becomes mkList (validityFromBools valid) offs child; ColDictionary i keys vals becomes mkDictionary i keysColumn vals; matching on ColInt64Maybe v becomes matching ColInt64 validity v or reading through asPrim PInt64; isNullableColumn c becomes nullCount c > 0.

{-# LANGUAGE OverloadedStrings #-}
module Migrate where
import Arrow.Column
import Data.Int (Int32)
import Data.Text (Text)
import qualified Data.Vector as V
import qualified Data.Vector.Storable as VS
-- Before: ColInt64Maybe (V.fromList [Just 1, Nothing])
nullableIds :: ColumnArray
nullableIds = fromMaybes PInt64 (V.fromList [Just 1, Nothing])
-- Before: ColUtf8 (V.fromList ["a", "b"])
names :: ColumnArray
names = fromTexts (V.fromList ["a", "b"])
-- Before: ColDictionary 0 (VP.fromList [0, 1, 0]) (ColUtf8 ...)
-- Keys are an integer column at the wire width and carry the row nulls.
colours :: Either String ColumnArray
colours =
mkDictionary 0 (fromMaybes PInt32 (V.fromList [Just 0, Nothing, Just 1]))
(fromTexts (V.fromList ["red", "green"]))
-- Before: case c of ColInt64Maybe v -> ...; ColInt64 v -> ...
-- After: one pattern; the validity is Nothing when there are no nulls.
describe :: ColumnArray -> String
describe c = case c of
ColInt64 Nothing v -> "dense int64 x" <> show (VS.length v)
ColInt64 (Just m) _ -> show (validityNullCount m) <> " nulls"
_ -> columnTag c
-- Before: ColListMaybe valid offsets child
nullableLists :: Either String ColumnArray
nullableLists =
mkList (validityFromBools (V.fromList [True, False])) (VS.fromList [0, 1, 1])
(primColumn PInt32 (VS.fromList [7 :: Int32]))
-- Equality is logical: layout differences that do not change the rows
-- (slices, offset bases, null slot contents) compare equal.
sameRows :: Bool
sameRows = sliceColumnArray 1 1 names == fromTexts (V.fromList ["b" :: Text])
ModulePurpose
Arrow.TypesSchema, field, and buffer types; schemaFingerprint
Arrow.ColumnColumnArray (Arrow-native buffers, Maybe Validity), patterns, typed accessors, from*/mk* construction, copyColumn, slice/concat/take, logical Eq
Arrow.Column.BuilderGrowable pinned builders in ST/IO (re-exported by Arrow.Column)
Arrow.Column.BufferBitmap, Validity, fixed-width element types, aligned allocation
Arrow.Column.InternalRaw constructors and structural validators, for readers, writers and malformed-input tests
Arrow.VectorVector a data family behind the Data.Vector.Generic API in Arrow layout (storable values, bitmaps, validity plus values for Maybe a, offsets into one store for Text, ByteString and lists); what the to*Vector conversions return
Arrow.Read.ColumnsRecord batch to columns: buffer slicing, validation, zero-copy aliasing
Arrow.RecordTyped Table, Encoder, Decoder, projection helpers
Arrow.Record.Generic / Arrow.Record.THGeneric and TH record derivation
Arrow.DeriveAnnotation-driven deriver
Arrow.IPCIPC message framing encode/decode
Arrow.StreamStream and file encode / decode (with dictionaries, including nested ones), lazy encoders, pull-based streaming reader
Arrow.FileArrow file format readers
Arrow.FlatBufferIPCFlatBuffer-backed IPC metadata path (re-exports .Common, .Read, .Write)
Arrow.WritewriteArrowStream / writeArrowFile, column encoders, validateColumns

Enable Zstd or LZ4 with Cabal flags (+zstd, +lz4). Compressed IPC streams follow the standard Arrow body compression layout; uncompressed IPC remains the default for maximum interoperability.

One record batch per workload through Arrow.Stream, set against arrow-rs 58 building the same batches from the same generators. Four groups per workload: encode (one allocation; arrow-rs StreamWriter into a Vec<u8>), encode lazy (chunks aliasing the column buffers; arrow-rs has no lazy writer, so it is set against StreamWriter), decode (zero copy; arrow-rs StreamReader with its default validation on), and decode + toVector (decode, convert every column with the to*Vector conversions, then G.force each result into fresh buffers so it owns its data; arrow-rs copies into owned Vec<Option<T>>, String and nested Vec values). Dictionary rows share one owned copy of the values instead of one string per row. Inputs are built outside the timed region. The ratio column is wireform-arrow time over arrow-rs time, so 2.00x means wireform-arrow takes twice as long. Run both, sequentially, with python3 scripts/run-benchmarks.py --only arrow --render; the harnesses are wireform-arrow/bench/Bench.hs and interop/arrow-rs/benches/arrow_ipc.rs.

The bench binary runs with a 64 MB nursery (-with-rtsopts=-A64m). Decoding, lazy encoding and the Arrow.Vector conversions barely allocate, but code that builds boxed values (the typed Arrow.Record rows, G.convert to a boxed vector, your own conversions) is dominated by minor collections at GHC’s default 4 MB nursery. Programs that move large batches should do the same: link with -rtsopts and run with +RTS -A64m, or bake it in with -with-rtsopts.

wireform-arrow vs arrow-rs, IPC stream encode + decode (100k rows) wireform-arrow vs arrow-rs, IPC stream encode + decode (100k rows) lower is better · µs · ghc-9.8.4 on darwin-aarch64, criterion 1.6.5; arrow-rs 58.2.0, rustc 1.98.1, criterion.rs 0.7.0 0 1250 2500 3750 5000 39.3 10.7 39.5 10.3 37.8 10.7 45.2 12.9 43.5 12.5 413 61.7 93.9 26.8 63.0 16.4 31.4 6.05 39.3 0.630 39.5 0.610 37.8 0.650 45.2 0.690 43.5 0.690 413 2.07 93.9 0.910 63.0 1.36 31.4 1.12 13.1 0.480 13.1 0.460 13.4 0.750 125 48.0 123 47.8 293 97.4 110 6.08 20.9 1.04 34.1 6.29 159 15.0 171 14.6 160 14.3 1411 241 1458 297 3458 556 3702 189 330 25.7 1338 401 encode int64 encode double encode nullable int64 encode utf8 encode nullable utf8 encode mixed 6-col encode list<int32> encode struct<int32,double,bool> encode dictionary<utf8> encode lazy int64 encode lazy double encode lazy nullable int64 encode lazy utf8 encode lazy nullable utf8 encode lazy mixed 6-col encode lazy list<int32> encode lazy struct<int32,double,bool> encode lazy dictionary<utf8> decode int64 decode double decode nullable int64 decode utf8 decode nullable utf8 decode mixed 6-col decode list<int32> decode struct<int32,double,bool> decode dictionary<utf8> decode + toVector int64 decode + toVector double decode + toVector nullable int64 decode + toVector utf8 decode + toVector nullable utf8 decode + toVector mixed 6-col decode + toVector list<int32> decode + toVector struct<int32,double,bool> decode + toVector dictionary<utf8> arrow-rs wireform-arrow wireform-arrow vs arrow-rs, IPC stream encode + decode (100k rows) lower is better · µs · ghc-9.8.4 on darwin-aarch64, criterion 1.6.5; arrow-rs 58.2.0, rustc 1.98.1, criterion.rs 0.7.0 0 1250 2500 3750 5000 39.3 10.7 39.5 10.3 37.8 10.7 45.2 12.9 43.5 12.5 413 61.7 93.9 26.8 63.0 16.4 31.4 6.05 39.3 0.630 39.5 0.610 37.8 0.650 45.2 0.690 43.5 0.690 413 2.07 93.9 0.910 63.0 1.36 31.4 1.12 13.1 0.480 13.1 0.460 13.4 0.750 125 48.0 123 47.8 293 97.4 110 6.08 20.9 1.04 34.1 6.29 159 15.0 171 14.6 160 14.3 1411 241 1458 297 3458 556 3702 189 330 25.7 1338 401 encode int64 encode double encode nullable int64 encode utf8 encode nullable utf8 encode mixed 6-col encode list<int32> encode struct<int32,double,bool> encode dictionary<utf8> encode lazy int64 encode lazy double encode lazy nullable int64 encode lazy utf8 encode lazy nullable utf8 encode lazy mixed 6-col encode lazy list<int32> encode lazy struct<int32,double,bool> encode lazy dictionary<utf8> decode int64 decode double decode nullable int64 decode utf8 decode nullable utf8 decode mixed 6-col decode list<int32> decode struct<int32,double,bool> decode dictionary<utf8> decode + toVector int64 decode + toVector double decode + toVector nullable int64 decode + toVector utf8 decode + toVector nullable utf8 decode + toVector mixed 6-col decode + toVector list<int32> decode + toVector struct<int32,double,bool> decode + toVector dictionary<utf8> arrow-rs wireform-arrow
Operationarrow-rswireform-arrowratio
encode int6439.3 µs10.7 µs0.27x
encode double39.5 µs10.3 µs0.26x
encode nullable int6437.8 µs10.7 µs0.28x
encode utf845.2 µs12.9 µs0.28x
encode nullable utf843.5 µs12.5 µs0.29x
encode mixed 6-col413 µs61.7 µs0.15x
encode list93.9 µs26.8 µs0.29x
encode struct<int32,double,bool>63.0 µs16.4 µs0.26x
encode dictionary31.4 µs6.05 µs0.19x
encode lazy int6439.3 µs0.63 µs0.02x
encode lazy double39.5 µs0.61 µs0.02x
encode lazy nullable int6437.8 µs0.65 µs0.02x
encode lazy utf845.2 µs0.69 µs0.02x
encode lazy nullable utf843.5 µs0.69 µs0.02x
encode lazy mixed 6-col413 µs2.07 µs0.01x
encode lazy list93.9 µs0.91 µs0.01x
encode lazy struct<int32,double,bool>63.0 µs1.36 µs0.02x
encode lazy dictionary31.4 µs1.12 µs0.04x
decode int6413.1 µs0.48 µs0.04x
decode double13.1 µs0.46 µs0.04x
decode nullable int6413.4 µs0.75 µs0.06x
decode utf8125 µs48.0 µs0.38x
decode nullable utf8123 µs47.8 µs0.39x
decode mixed 6-col293 µs97.4 µs0.33x
decode list110 µs6.08 µs0.06x
decode struct<int32,double,bool>20.9 µs1.04 µs0.05x
decode dictionary34.1 µs6.29 µs0.18x
decode + toVector int64159 µs15.0 µs0.09x
decode + toVector double171 µs14.6 µs0.08x
decode + toVector nullable int64160 µs14.3 µs0.09x
decode + toVector utf81411 µs241 µs0.17x
decode + toVector nullable utf81458 µs297 µs0.20x
decode + toVector mixed 6-col3458 µs556 µs0.16x
decode + toVector list3702 µs189 µs0.05x
decode + toVector struct<int32,double,bool>330 µs25.7 µs0.08x
decode + toVector dictionary1338 µs401 µs0.30x

Last run 2026-10-09 11:04:54 UTC. ghc-9.8.4 on darwin-aarch64, criterion 1.6.5; arrow-rs 58.2.0, rustc 1.98.1, criterion.rs 0.7.0.

wireform-arrow vs arrow-rs, IPC stream encode + decode (100-row batch) wireform-arrow vs arrow-rs, IPC stream encode + decode (100-row batch) lower is better · µs · ghc-9.8.4 on darwin-aarch64, criterion 1.6.5; arrow-rs 58.2.0, rustc 1.98.1, criterion.rs 0.7.0 0 2.50 5.00 7.50 10.0 0.980 0.630 1.00 0.620 0.980 0.640 1.07 0.660 1.06 0.660 2.56 2.11 1.42 0.930 1.94 1.37 2.24 1.09 0.980 0.630 1.00 0.610 0.980 0.650 1.07 0.690 1.06 0.710 2.56 2.08 1.42 0.910 1.94 1.36 2.24 1.12 0.630 0.480 0.620 0.460 0.660 0.500 0.740 0.570 0.760 0.570 2.25 1.54 1.08 0.690 1.29 1.04 1.36 0.880 0.730 0.570 0.720 0.560 0.790 0.580 1.93 1.02 1.94 1.02 5.25 2.76 4.76 1.26 1.55 1.29 2.54 1.56 encode int64 encode double encode nullable int64 encode utf8 encode nullable utf8 encode mixed 6-col encode list<int32> encode struct<int32,double,bool> encode dictionary<utf8> encode lazy int64 encode lazy double encode lazy nullable int64 encode lazy utf8 encode lazy nullable utf8 encode lazy mixed 6-col encode lazy list<int32> encode lazy struct<int32,double,bool> encode lazy dictionary<utf8> decode int64 decode double decode nullable int64 decode utf8 decode nullable utf8 decode mixed 6-col decode list<int32> decode struct<int32,double,bool> decode dictionary<utf8> decode + toVector int64 decode + toVector double decode + toVector nullable int64 decode + toVector utf8 decode + toVector nullable utf8 decode + toVector mixed 6-col decode + toVector list<int32> decode + toVector struct<int32,double,bool> decode + toVector dictionary<utf8> arrow-rs wireform-arrow wireform-arrow vs arrow-rs, IPC stream encode + decode (100-row batch) lower is better · µs · ghc-9.8.4 on darwin-aarch64, criterion 1.6.5; arrow-rs 58.2.0, rustc 1.98.1, criterion.rs 0.7.0 0 2.50 5.00 7.50 10.0 0.980 0.630 1.00 0.620 0.980 0.640 1.07 0.660 1.06 0.660 2.56 2.11 1.42 0.930 1.94 1.37 2.24 1.09 0.980 0.630 1.00 0.610 0.980 0.650 1.07 0.690 1.06 0.710 2.56 2.08 1.42 0.910 1.94 1.36 2.24 1.12 0.630 0.480 0.620 0.460 0.660 0.500 0.740 0.570 0.760 0.570 2.25 1.54 1.08 0.690 1.29 1.04 1.36 0.880 0.730 0.570 0.720 0.560 0.790 0.580 1.93 1.02 1.94 1.02 5.25 2.76 4.76 1.26 1.55 1.29 2.54 1.56 encode int64 encode double encode nullable int64 encode utf8 encode nullable utf8 encode mixed 6-col encode list<int32> encode struct<int32,double,bool> encode dictionary<utf8> encode lazy int64 encode lazy double encode lazy nullable int64 encode lazy utf8 encode lazy nullable utf8 encode lazy mixed 6-col encode lazy list<int32> encode lazy struct<int32,double,bool> encode lazy dictionary<utf8> decode int64 decode double decode nullable int64 decode utf8 decode nullable utf8 decode mixed 6-col decode list<int32> decode struct<int32,double,bool> decode dictionary<utf8> decode + toVector int64 decode + toVector double decode + toVector nullable int64 decode + toVector utf8 decode + toVector nullable utf8 decode + toVector mixed 6-col decode + toVector list<int32> decode + toVector struct<int32,double,bool> decode + toVector dictionary<utf8> arrow-rs wireform-arrow
Operationarrow-rswireform-arrowratio
encode int640.98 µs0.63 µs0.64x
encode double1.00 µs0.62 µs0.62x
encode nullable int640.98 µs0.64 µs0.65x
encode utf81.07 µs0.66 µs0.62x
encode nullable utf81.06 µs0.66 µs0.62x
encode mixed 6-col2.56 µs2.11 µs0.82x
encode list1.42 µs0.93 µs0.65x
encode struct<int32,double,bool>1.94 µs1.37 µs0.71x
encode dictionary2.24 µs1.09 µs0.49x
encode lazy int640.98 µs0.63 µs0.64x
encode lazy double1.00 µs0.61 µs0.61x
encode lazy nullable int640.98 µs0.65 µs0.66x
encode lazy utf81.07 µs0.69 µs0.64x
encode lazy nullable utf81.06 µs0.71 µs0.67x
encode lazy mixed 6-col2.56 µs2.08 µs0.81x
encode lazy list1.42 µs0.91 µs0.64x
encode lazy struct<int32,double,bool>1.94 µs1.36 µs0.70x
encode lazy dictionary2.24 µs1.12 µs0.50x
decode int640.63 µs0.48 µs0.76x
decode double0.62 µs0.46 µs0.74x
decode nullable int640.66 µs0.50 µs0.76x
decode utf80.74 µs0.57 µs0.77x
decode nullable utf80.76 µs0.57 µs0.75x
decode mixed 6-col2.25 µs1.54 µs0.68x
decode list1.08 µs0.69 µs0.64x
decode struct<int32,double,bool>1.29 µs1.04 µs0.81x
decode dictionary1.36 µs0.88 µs0.65x
decode + toVector int640.73 µs0.57 µs0.78x
decode + toVector double0.72 µs0.56 µs0.78x
decode + toVector nullable int640.79 µs0.58 µs0.73x
decode + toVector utf81.93 µs1.02 µs0.53x
decode + toVector nullable utf81.94 µs1.02 µs0.53x
decode + toVector mixed 6-col5.25 µs2.76 µs0.53x
decode + toVector list4.76 µs1.26 µs0.26x
decode + toVector struct<int32,double,bool>1.55 µs1.29 µs0.83x
decode + toVector dictionary2.54 µs1.56 µs0.61x

Last run 2026-10-09 11:04:54 UTC. ghc-9.8.4 on darwin-aarch64, criterion 1.6.5; arrow-rs 58.2.0, rustc 1.98.1, criterion.rs 0.7.0.

wireform-arrow entry points vs arrow-rs, mixed 6-column table (100k rows) wireform-arrow entry points vs arrow-rs, mixed 6-column table (100k rows) lower is better · µs · ghc-9.8.4 on darwin-aarch64, criterion 1.6.5; arrow-rs 58.2.0, rustc 1.98.1, criterion.rs 0.7.0 0 1250 2500 3750 5000 393 61.5 393 2.07 393 61.2 277 96.8 397 64.2 397 3.69 277 97.4 277 97.8 1645 1470 3076 2944 stream encode (Arrow.Stream) stream encode lazy (Arrow.Stream) stream encode (Arrow.Write) stream decode (Arrow.Stream) file encode (Arrow.Stream) file encode lazy (Arrow.Stream) file decode (Arrow.Stream) file read (Arrow.File) typed encode (Arrow.Record) typed decode (Arrow.Record) arrow-rs wireform-arrow wireform-arrow entry points vs arrow-rs, mixed 6-column table (100k rows) lower is better · µs · ghc-9.8.4 on darwin-aarch64, criterion 1.6.5; arrow-rs 58.2.0, rustc 1.98.1, criterion.rs 0.7.0 0 1250 2500 3750 5000 393 61.5 393 2.07 393 61.2 277 96.8 397 64.2 397 3.69 277 97.4 277 97.8 1645 1470 3076 2944 stream encode (Arrow.Stream) stream encode lazy (Arrow.Stream) stream encode (Arrow.Write) stream decode (Arrow.Stream) file encode (Arrow.Stream) file encode lazy (Arrow.Stream) file decode (Arrow.Stream) file read (Arrow.File) typed encode (Arrow.Record) typed decode (Arrow.Record) arrow-rs wireform-arrow
Operationarrow-rswireform-arrowratio
stream encode (Arrow.Stream)393 µs61.5 µs0.16x
stream encode lazy (Arrow.Stream)393 µs2.07 µs0.01x
stream encode (Arrow.Write)393 µs61.2 µs0.16x
stream decode (Arrow.Stream)277 µs96.8 µs0.35x
file encode (Arrow.Stream)397 µs64.2 µs0.16x
file encode lazy (Arrow.Stream)397 µs3.69 µs0.01x
file decode (Arrow.Stream)277 µs97.4 µs0.35x
file read (Arrow.File)277 µs97.8 µs0.35x
typed encode (Arrow.Record)1645 µs1470 µs0.89x
typed decode (Arrow.Record)3076 µs2944 µs0.96x

Last run 2026-10-09 11:04:54 UTC. ghc-9.8.4 on darwin-aarch64, criterion 1.6.5; arrow-rs 58.2.0, rustc 1.98.1, criterion.rs 0.7.0.

The entry-point rows map to the closest arrow-rs path: StreamWriter for every stream writer (eager, lazy, Arrow.Write), FileWriter for both file writers, FileReader for both file readers, and for the typed rows a Vec of row structs turned into one array per field (or read back into owned row structs), since arrow-rs has no record deriving. Comparisons against pyarrow and arrow-cpp are not yet measured.