packages feed

wireform-parquet-0.1.0.0: scripts/parquet_nested_bloom_interop.py

#!/usr/bin/env python3
"""Check generated nested blooms with unmodified Arrow C++ and liblzo2."""
import argparse
import hashlib
import json
from pathlib import Path
import struct
import subprocess
import tempfile

import parquet_lzo_interop as lzo


def values():
    return [
        list(map(lambda x: struct.pack('<i', x).hex(), [-7, 0, 42])),
        list(map(lambda x: struct.pack('<q', x).hex(), [-1099511627776, 1099511627776])),
        list(map(lambda x: struct.pack('<I', x).hex(), [0x7fc00001, 0x80000000, 0, 0x7f800000])),
        list(map(lambda x: struct.pack('<d', x).hex(), [-0.0, 0.0, float('inf'), -3.5])),
        list(map(lambda x: bytes([x % 2]).hex(), range(17))),
        list(map(lambda x: x.encode().hex(), ['', 'alpha', 'λ\0β'])),
        ['', b'abc'.hex(), bytes([0, 255, 4, 0, 0, 0]).hex()],
        [(b'\0' * 9).hex(), (b'\xff' * 9).hex()],
        [bytes(range(16)).hex()],
        [bytes(range(12)).hex()],
        [(b'\xff' * 12).hex()],
        [(b'\xff' * 16).hex(), (b'\0' * 16).hex()],
        [(b'\xff' * 32).hex()],
        [], ['01'], ['00'] * 8,
    ]


def metadata(path):
    data = path.read_bytes()
    length = int.from_bytes(data[-8:-4], 'little')
    start = len(data) - 8 - length
    footer = lzo.decode(data[start:-8]).node
    return {'bytes': data, 'footer': footer, 'footerStart': start}


def bloom_bytes(path):
    record = metadata(path)
    group = record['footer'].value[4].value[0]
    columns = group.value[1].value
    assert len(columns) == 2, path
    cm = columns[0].value[3].value
    offset, size = cm[14].value, cm[15].value
    assert offset >= 4 and size > 0 and offset + size <= record['footerStart'], path
    assert 14 not in columns[1].value[3].value, ('unrequested bloom', path)
    return record['bytes'][offset:offset + size]


def strip_auxiliary(source, target):
    record = metadata(source)
    for group in record['footer'].value[4].value:
        for chunk in group.value[1].value:
            for field in (4, 5, 6, 7):
                chunk.value.pop(field, None)
            column = chunk.value[3].value
            column.pop(14, None)
            column.pop(15, None)
    footer = lzo.encode(record['footer'])
    target.write_bytes(record['bytes'][:record['footerStart']] + footer + struct.pack('<I', len(footer)) + b'PAR1')


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--fixtures', type=Path, required=True)
    parser.add_argument('--reference', type=Path, required=True)
    parser.add_argument('--lzo-reference', type=Path, required=True)
    parser.add_argument('--output', type=Path, required=True)
    args = parser.parse_args()
    files = sorted(args.fixtures.glob('nested-generated-bloom-*.parquet'))
    assert len(files) == 224, ('full seven-codec/two-page/16-model coverage', len(files))
    expected = values()
    records = []
    cpp_queries = 0
    operations = 0
    with tempfile.TemporaryDirectory(prefix='nested-bloom-interop-') as temp:
        scratch = Path(temp)
        for path in files:
            index, codec, version = path.stem.removeprefix('nested-generated-bloom-').split('-')
            case = int(index)
            canonical = args.fixtures / f'nested-generated-bloom-{case}-Uncompressed-{version}.parquet'
            assert bloom_bytes(path) == bloom_bytes(canonical), ('codec-independent bloom bytes', path)
            readable = path
            if codec == 'LZO':
                stripped, readable = scratch / 'stripped.parquet', scratch / 'decoded.parquet'
                strip_auxiliary(path, stripped)
                operations += lzo.transcode(stripped, readable, 0, args.lzo_reference, scratch,
                                            require_source_codec=3, expected_version=1 if version == 'PageV1' else 2)
            proc = subprocess.run([str(args.reference), str(readable), '-', 'plaintext'], capture_output=True, text=True)
            assert proc.returncode == 0, (path, proc.stderr)
            header, *columns = map(json.loads, proc.stdout.splitlines())
            assert header['rowGroups'] == 1 and len(columns) == 2, path
            assert columns[0]['pathParts'] == ['values.with.dot', 'list', 'element'], path
            assert columns[0]['values'] == expected[case], ('physical values', path, columns[0]['values'])
            assert columns[1]['values'] == list(map(lambda x: struct.pack('<i', x).hex(), [1, 2, 3])), path
            assert columns[1]['bloomBytes'] == 0, path
            if codec != 'LZO':
                assert columns[0]['bloomBytes'] > 0, path
                assert columns[0]['bloomValuesChecked'] == len(expected[case]), path
                cpp_queries += len(expected[case])
            else:
                # Unmodified C++ lacks LZO. liblzo2 decodes only compression;
                # C++ then proves exact physical values. Original bloom bytes
                # equal the matching uncompressed fixture checked above.
                assert columns[0]['bloomBytes'] == 0, path
            records.append({'file': path.name, 'physicalValuesMatchIndependentModel': True,
                            'bloomMatchesUncompressedControl': True,
                            'cppDirectBloomProbes': len(expected[case]) if codec != 'LZO' else 0,
                            'lzoValuesViaIndependentLiblzo2': codec == 'LZO'})
    proof = {'files': records, 'fileCount': len(records), 'cppBloomValueQueries': cpp_queries,
             'independentLzoPageOperations': operations,
             'referenceSHA256': hashlib.sha256(args.reference.read_bytes()).hexdigest(),
             'allPassed': True}
    args.output.write_text(json.dumps(proof, indent=2) + '\n')
    print(f'PASS: {len(records)} files, {cpp_queries} C++ membership probes, {operations} liblzo2 page operations')


if __name__ == '__main__':
    main()