wireform-parquet-0.1.0.0: src/Parquet/Compress.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{- | Page-data compression for the Parquet writer.
The reader's 'Parquet.Read.decompressChunk' supports @Uncompressed@,
@GZip@, @Snappy@ (with @-fsnappy@), @ZSTD@ (with @-fzstd@), and
@LZ4_RAW@ (with @-flz4@), raw @LZO@ and optional @Brotli@
(with @-fbrotli@). This module exposes the symmetric writer
side: 'compressPageBytes' takes a 'Compression' codec and a page body
and returns the compressed bytes.
The codec set behind cabal flags is identical to the reader's; if the
user requests a codec that wasn't enabled at build time, the writer
returns @Left@ rather than silently emitting uncompressed data.
-}
module Parquet.Compress (
compressPageBytes,
validateCompression,
) where
import Codec.Compression.GZip qualified as GZip
import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
import Data.ByteString.Lazy qualified as BL
import Columnar.LZO qualified as LZO
import Data.Bifunctor (first)
#ifdef HAVE_SNAPPY
import qualified Codec.Compression.Snappy as Snappy
#endif
#ifdef HAVE_ZSTD
import qualified Codec.Compression.Zstd as Zstd
#endif
#ifdef HAVE_LZ4
import qualified Columnar.LZ4 as LZ4
#endif
#ifdef HAVE_BROTLI
import qualified Parquet.Brotli as Brotli
#endif
import Parquet.Types (Compression (..))
-- | Check availability without running the codec or allocating a page.
validateCompression :: Compression -> Either String ()
validateCompression codec = case codec of
Uncompressed -> Right ()
GZip -> Right ()
LZO -> Right ()
#ifdef HAVE_SNAPPY
Snappy -> Right ()
#endif
#ifdef HAVE_ZSTD
ZSTD -> Right ()
#endif
#ifdef HAVE_LZ4
LZ4Raw -> Right ()
#endif
#ifdef HAVE_BROTLI
Brotli -> Right ()
#endif
_ -> Left ("Parquet.Compress: codec unavailable for writing: " ++ show codec)
{- | Compress a page-body byte string with the given codec.
@Uncompressed@ is always available; @GZip@ uses the built-in @zlib@
dependency; @Snappy@\/@ZSTD@\/@LZ4@ require the matching @-f@ flag at
build time. Returns @Left@ for codecs we cannot encode (e.g. the
deprecated Hadoop-flavoured @LZ4@).
-}
compressPageBytes :: Compression -> ByteString -> Either String ByteString
compressPageBytes Uncompressed bs = Right bs
compressPageBytes GZip bs =
Right (BL.toStrict (GZip.compress (BL.fromStrict bs)))
#ifdef HAVE_SNAPPY
compressPageBytes Snappy bs = Right (Snappy.compress bs)
#else
compressPageBytes Snappy _ =
Left "Parquet.Compress: Snappy requires building wireform with -fsnappy"
#endif
#ifdef HAVE_ZSTD
compressPageBytes ZSTD bs =
Right (Zstd.compress 3 bs) -- level 3 is the parquet-mr default.
#else
compressPageBytes ZSTD _ =
Left "Parquet.Compress: ZSTD requires building wireform with -fzstd"
#endif
#ifdef HAVE_LZ4
-- LZ4_RAW (Parquet codec id 7): the raw LZ4 block format with
-- no framing. 'Columnar.LZ4.compress' is a direct FFI call to
-- liblz4's @LZ4_compress_default@ — output is byte-identical
-- to what arrow-cpp / parquet-mr / pyarrow emit, so any
-- modern Parquet reader accepts it.
-- A page above liblz4's raw-block limit cannot be compressed at all:
-- 'LZ4.compress' would return an empty payload, which would be written
-- as a valid but empty page. Reject it instead (Parquet pages are
-- bounded by the page size, far below the limit).
-- The raw block format represents empty input with a final zero token.
-- Columnar.LZ4 uses an empty ByteString as its failure/empty sentinel, so
-- handle this legal page explicitly before asking for a non-empty bound.
compressPageBytes LZ4Raw bs
| BS.null bs = Right (BS.singleton 0)
| LZ4.compressBound (BS.length bs) < 0 =
Left "Parquet.Compress: LZ4_RAW input exceeds LZ4_MAX_INPUT_SIZE"
| otherwise = Right (LZ4.compress bs)
#else
compressPageBytes LZ4Raw _ =
Left "Parquet.Compress: LZ4_RAW requires building wireform with -flz4"
#endif
compressPageBytes LZ4 _ =
Left $
"Parquet.Compress: deprecated Hadoop LZ4 (codec 5) is not supported; "
++ "use LZ4_RAW (codec 7) with -flz4"
#ifdef HAVE_BROTLI
compressPageBytes Brotli bs =
Brotli.compress bs
#else
compressPageBytes Brotli _ =
Left "Parquet.Compress: Brotli requires building wireform with -fbrotli"
#endif
-- Parquet uses the raw LZO1X stream, without Hadoop/lzop framing.
compressPageBytes LZO bytes =
first (("Parquet.Compress: " ++) . LZO.prettyLZOError) (LZO.compress bytes)