
Haskell ganha biblioteca para escrever arquivos Parquet
A organização DataHaskell implementou um escritor de arquivos Parquet na biblioteca Dataframe, permitindo que programas em Haskell leiam e escrevam esse formato de forma nativa. O uso é simples: basta passar um dataframe para a função writeParquet, que grava o arquivo com padrões razoáveis de tamanho de row group e de página. Quem precisar de controle mais fino pode usar writeParquetWithOptions.
Por que Parquet em Haskell
Para interoperar com o ecossistema de dados, Haskell precisa entender os formatos padrão usados nele. Até então, as opções de serialização em Haskell se resumiam a CSV, JSON ou despejar dados em um ByteString. CSV e JSON têm seu lugar, mas tornam-se lentos e pesados à medida que o volume de dados cresce, e formatos caseiros tendem a carecer de interoperabilidade com ferramentas de ciência de dados. O Parquet troca simplicidade por armazenamento e consulta eficientes: alta taxa de compressão e possibilidade de ler apenas os dados relevantes a uma consulta. Dada a universalidade do formato na ciência de dados, poder ler e escrever Parquet é útil para armazenamento de longo prazo, transferência pela rede e interoperabilidade.
Estrutura do formato
Um arquivo Parquet é uma série de row groups seguidos de metadados ao final, que permitem ao leitor localizar column chunks e páginas, além de trazer estatísticas e bloom filters que ajudam um query planner a decidir se vale ler determinado row group. Cada row group é uma coleção de column chunks com o mesmo número de linhas; cada column chunk é uma série de data pages. Uma data page contém metadados (codificação, número de valores, estatísticas), definition levels e repetition levels - uma codificação barata de nulidade e estrutura aninhada - e os dados codificados e comprimidos. A codificação é definida por página, e a compressão por column chunk. A implementação atual suporta apenas definition levels até 1, para valores nulos, e esquemas planos.
Opções de escrita e dimensionamento
O tipo ParquetWriteOptions expõe pageSize, rowGroupSize, batchRows, subBatchRows, compressionCodec, strategy e maxRowsPerFile. Como cada column chunk deve conter o mesmo número de linhas, os tamanhos alvo de página e row group são tratados como best effort: os valores reais podem ficar um pouco acima ou abaixo do alvo. As colunas são processadas em lotes de batchRows, e o estado do row group é verificado após cada lote, de modo que cada row group contém um múltiplo inteiro de batchRows; as páginas, por sua vez, contêm múltiplos inteiros de subBatchRows (exceto a última página e o último column chunk). O sub-batching no nível de página reduz a contabilidade em IORef após cada escrita, gerando ganhos significativos de velocidade. Os autores notam que colunas com entradas desproporcionalmente grandes podem fazer o batching estourar os alvos por ordens de magnitude, o que pode exigir um mecanismo de mitigação no futuro.
Memória e loop central
Como é difícil saber de antemão o tamanho dos buffers - especialmente dos buffers de column chunk, que variam bastante entre colunas - a implementação usa buffers de memória expansíveis baseados em MutableByteArray fixado (pinned), permitindo convertê-los em Ptr Word8 na hora de descarregar para outro buffer ou para o arquivo. Ao crescer, em vez de usar a função grow de Data.Primitive (incompatível com memória fixada), aloca-se um novo ByteArray e o antigo é deixado para o garbage collector; a fragmentação esperada é baixa, pois os buffers tendem a ser bem maiores que blocos de 4 KB do GHC, e crescimentos devem se tornar raros após as primeiras páginas e row groups.
O escritor pode ser visto como um fold com efeitos sobre o dataframe: itera-se pelas linhas produzindo escritas em memória e em disco, e o resultado final é o metadado anexado ao fim do arquivo. O loop principal escreve lotes e descarrega o row group para o arquivo quando o buffer atinge o tamanho alvo. Buffers de página são montados com um scratchBuffer reutilizável - que reúne valores, definition levels e metadados da página - antes de ir para o buffer do column chunk. Ao final, flushBufferToFile escreve em blocos de 256 KiB: como as escritas passam pelo page cache do kernel, chunks maiores reduzem syscalls e evitam que o processo seja estrangulado quando há muitas páginas sujas.
Estado atual e próximos passos
O escritor hoje suporta apenas compressão Snappy ou sem compressão, plain encoding e definition levels até 1, e é totalmente single-threaded. Entre os planos futuros estão: suporte ao conjunto completo de compressões e codificações do Parquet; exploração de concorrência ou paralelismo, desde que haja justificativa de desempenho; gravação de metadados opcionais como estatísticas e bloom filters, úteis para leitores otimizarem consultas; e uma estratégia de duas passagens com buffers menores em memória e arquivos temporários em disco, para sistemas com memória restrita, em que manter um row group inteiro na memória pode esgotar os recursos.