packages feed

wireform-parquet-0.1.0.0: scripts/parquet_dictionary_ieee_interop.py

#!/usr/bin/env python3
"""Check dictionary NaNs, signed zero and IEEE payloads with reference readers."""
import argparse
import math
from pathlib import Path
import subprocess
import tempfile

import duckdb
import polars as pl
import pyarrow as pa
import pyarrow.ipc as ipc
import pyarrow.parquet as pq

from parquet_aggregate_interop import TType, column_metadata, footer, groups
from parquet_delta_interop import encoded_struct
from parquet_writer_options_interop import thrift_at


def fixture(bits):
    if bits == 32:
        values = [0x3f800000, 0x7fc01234, 0xffa05678, 0x7fc01234, 0,
                  0x80000000, 0, 0x80000000, 0x7f800000, 0xff800000, 1, 0xffffffff]
        state, multiplier, increment = 0x12345678, 1664525, 1013904223
    else:
        values = [0x3ff0000000000000, 0x7ff8123456789abc, 0xfff056789abcdef0,
                  0x7ff8123456789abc, 0, 0x8000000000000000, 0,
                  0x8000000000000000, 0x7ff0000000000000, 0xfff0000000000000,
                  1, 0xffffffffffffffff]
        state, multiplier, increment = (0x123456789abcdef0,
                                       6364136223846793005, 1442695040888963407)
    for _ in range(129):
        values.append(state)
        state = (state * multiplier + increment) & (2**bits - 1)
    return values + list(reversed(values))


def check_semantics(actual, expected):
    assert len(actual) == len(expected)
    for left, right in zip(actual, expected, strict=True):
        if math.isnan(right):
            assert math.isnan(left)
        else:
            assert left == right
            if right == 0:
                assert math.copysign(1, left) == math.copysign(1, right)


def dictionary_file(seed, path, dictionary, data):
    _, metadata = footer(seed)
    column = column_metadata(metadata)
    column[2] = TType.LIST, (TType.I32, [(TType.I32, 0), (TType.I32, 8)])
    column[6] = column[7] = TType.I64, len(dictionary) + len(data)
    column[9] = TType.I64, 4 + len(dictionary)
    column[11] = TType.I64, 4
    column.pop(13, None)
    group = groups(metadata)[0][1]
    group[2] = group[6] = TType.I64, len(dictionary) + len(data)
    encoded_footer = encoded_struct(metadata[1])
    path.write_bytes(b'PAR1' + dictionary + data + encoded_footer
                     + len(encoded_footer).to_bytes(4, 'little') + b'PAR1')


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--probe', type=Path, required=True)
    parser.add_argument('--reader', type=Path)
    args = parser.parse_args()
    with tempfile.TemporaryDirectory(prefix='wireform-parquet-dict-ieee-') as directory:
        root = Path(directory)
        subprocess.run([str(args.probe.resolve()), '--dictionary-ieee', str(root)], check=True)
        expected_bits = {}
        for bits in (32, 64):
            values = fixture(bits)
            uint_type = getattr(pa, f'uint{bits}')()
            float_type = getattr(pa, f'float{bits}')()
            expected = pa.array(values, uint_type).view(float_type)
            seed = root / 'seed.parquet'
            table = pa.Table.from_arrays([expected], schema=pa.schema([
                pa.field('x', float_type, nullable=False)]))
            pq.write_table(table, seed, compression='NONE',
                           use_dictionary=False, write_statistics=False)
            dictionary = (root / f'f{bits}.dictionary.bin').read_bytes()
            data = (root / f'f{bits}.data.bin').read_bytes()
            header, _ = thrift_at(dictionary, 0)
            assert header[1] == 2 and header[7][2] == 0, 'dictionary page must use PLAIN'
            path = root / f'f{bits}.parquet'
            dictionary_file(seed, path, dictionary, data)
            actual = pq.read_table(path, use_threads=False)['x'].combine_chunks()
            assert actual.type == float_type and actual.view(uint_type).to_pylist() == values
            polars = pl.read_parquet(path)['x'].to_arrow()
            assert polars.type == float_type
            check_semantics(polars.to_pylist(), expected.to_pylist())
            connection = duckdb.connect(config={'threads': 1})
            try:
                result = connection.execute('SELECT x FROM read_parquet(?)', [str(path)]).to_arrow_table()['x'].combine_chunks()
                assert result.type == float_type
                check_semantics(result.to_pylist(), expected.to_pylist())
            finally:
                connection.close()
            expected_bits[path.name] = bits, values
            print(f'PASS f{bits}: PyArrow exact IEEE bits; Polars/DuckDB NaNs, infinities, signed zero and finite values')
        seed.unlink()
        if args.reader:
            decoded = root / 'decoded'
            subprocess.run([str(args.reader.resolve()), str(root), '--arrow-output', str(decoded)], check=True)
            for name, (bits, values) in expected_bits.items():
                with ipc.open_stream(decoded / (name + '.arrows')) as reader:
                    actual = reader.read_all()['x'].combine_chunks()
                assert actual.view(getattr(pa, f'uint{bits}')()).to_pylist() == values
            print('PASS Wireform Arrow output: exact IEEE bits in both dictionary files')


if __name__ == '__main__':
    main()