packages feed

wireform-parquet-0.1.0.0: scripts/parquet_writer_options_interop.py

#!/usr/bin/env python3
"""Verify writer settings and page indexes with Apache Thrift and three engines."""
import argparse
from pathlib import Path
import subprocess
import tempfile

import duckdb
import polars as pl
import pyarrow as pa
import pyarrow.parquet as pq
from parquet_statistics_interop import read_value, TCompactProtocol, TMemoryBuffer, TType


def thrift_at(raw, offset):
    transport = TMemoryBuffer(raw[offset:])
    value = read_value(TCompactProtocol(transport), TType.STRUCT)
    return value, transport.cstringio_buf.tell()


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--probe', required=True, type=Path)
    args = parser.parse_args()
    with tempfile.TemporaryDirectory(prefix='wireform-parquet-options-') as directory:
        root = Path(directory)
        subprocess.run([str(args.probe.resolve()), '--writer-options', str(root)], check=True)
        files = sorted(root.glob('*.parquet'))
        assert len(files) == 32, len(files)
        for path in files:
            shape, version, codec, index_mode = path.stem.split('-')
            nullable, indexed = shape == 'mixed', index_mode == 'index'
            expected = pa.Table.from_arrays([pa.array([1,2,3] * 2, pa.int32()),
                pa.array(([10,None,30] if nullable else [10,20,30]) * 2, pa.int64())],
                schema=pa.schema([pa.field('r', pa.int32(), nullable=False),
                                  pa.field('o', pa.int64(), nullable=nullable)]))
            assert pq.read_table(path).equals(expected), path.name
            polars = pl.read_parquet(path).to_arrow()
            assert polars.schema.types == expected.schema.types and polars.cast(expected.schema).equals(expected), path.name
            con = duckdb.connect()
            try:
                duck = con.execute('SELECT * FROM read_parquet(?)', [str(path)]).to_arrow_table()
                assert duck.schema.types == expected.schema.types and duck.cast(expected.schema).equals(expected), path.name
            finally:
                con.close()
            raw = path.read_bytes()
            footer_size = int.from_bytes(raw[-8:-4], 'little')
            footer_start = len(raw) - 8 - footer_size
            footer, _ = thrift_at(raw, footer_start)
            metadata = pq.ParquetFile(path).metadata
            assert metadata.num_row_groups == 2
            page_ends = []
            for rg, group in enumerate(footer[4]):
                assert group[2] == sum(chunk[3][6] for chunk in group[1]), (path.name, 'row-group uncompressed size')
                for col, chunk in enumerate(group[1]):
                    assert chunk[2] == 0, (path.name, 'deprecated file_offset must be zero')
                    column = chunk[3]
                    assert column[12][7] is True and column[12][8] is True, (path.name, "exact bounds")
                    reference = metadata.row_group(rg).column(col)
                    assert reference.compression.lower() == codec, (path.name, reference.compression)
                    offset = column[9]
                    header, header_length = thrift_at(raw, offset)
                    assert header[1] == (0 if version == 'v1' else 3), path.name
                    page_end = offset + header_length + header[3]
                    page_ends.append(page_end)
                    assert page_end - offset == column[7], (path.name, 'compressed size')
                    assert header_length + header[2] == column[6], (path.name, 'uncompressed size')
                    assert (14 in column and 15 in column) == (col == 1), (path.name, 'bloom')
                    assert reference.has_offset_index == indexed and reference.has_column_index == indexed, path.name
                    assert all((field in chunk) == indexed for field in (4,5,6,7)), (path.name, chunk)
                    if not indexed:
                        continue
                    # ColumnChunk IDs: offset index=(4,5), column index=(6,7).
                    offset_index, used = thrift_at(raw, chunk[4])
                    assert used == chunk[5] and offset_index[1] == [{1: offset, 2: page_end-offset, 3: 0}], path.name
                    column_index, used = thrift_at(raw, chunk[6])
                    assert used == chunk[7] and column_index[1] == [False], path.name
                    assert column_index[5] == [int(nullable and col == 1)], path.name
                    width = 4 if col == 0 else 8
                    assert column_index[2] == [(1 if col == 0 else 10).to_bytes(width, 'little', signed=True)], path.name
                    assert column_index[3] == [(3 if col == 0 else 30).to_bytes(width, 'little', signed=True)], path.name
                    assert page_end <= chunk[4] < chunk[6] < footer_start, path.name
            assert max(page_ends) <= footer_start
            print('PASS', path.name)
        print('32 files: values verified in PyArrow, DuckDB and Polars; page versions, codecs, blooms and indexes independently checked')


if __name__ == '__main__':
    main()