packages feed

wireform-parquet-0.1.0.0: scripts/parquet_large_offset_interop.py

#!/usr/bin/env python3
"""Public LargeUtf8/LargeBinary projections against unmodified Arrow C++."""
import argparse
import json
from pathlib import Path
import subprocess
import tempfile
import pyarrow as pa
import pyarrow.ipc as ipc
import pyarrow.parquet as pq


def widen(field):
    typ = field.type
    if pa.types.is_string(typ):
        typ = pa.large_string()
    elif pa.types.is_binary(typ):
        typ = pa.large_binary()
    elif pa.types.is_struct(typ):
        typ = pa.struct(list(map(widen, typ)))
    elif pa.types.is_list(typ):
        typ = pa.large_list(widen(typ.value_field))
    elif pa.types.is_map(typ):
        typ = pa.map_(widen(typ.key_field), widen(typ.item_field), keys_sorted=typ.keys_sorted)
    return pa.field(field.name, typ, nullable=field.nullable, metadata=field.metadata)


def project(reader, source, schema, output):
    request = output.with_suffix('.schema.arrows')
    with ipc.new_stream(request, schema):
        pass
    return subprocess.run(list(map(str, [reader, '--project', source, request, output])), capture_output=True, text=True, timeout=60)


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--reader', type=Path, required=True)
    parser.add_argument('--output', type=Path, required=True)
    parser.add_argument('--codecs', nargs='+', default=['NONE', 'gzip', 'snappy', 'zstd', 'lz4', 'brotli'])
    args = parser.parse_args()
    cases = 0
    rejected = 0
    with tempfile.TemporaryDirectory(prefix='large-offset-interop-') as directory:
        root = Path(directory)
        required_schema = pa.schema([pa.field('text', pa.string(), nullable=False), pa.field('bytes', pa.binary(), nullable=False)])
        tables = {
            'required': pa.Table.from_arrays([pa.array(['alpha', '', 'κόσμε'] * 9), pa.array([b'\x00\xff', b'', b'bytes'] * 9)], schema=required_schema),
            'nullable': pa.table({'text': pa.array(['alpha', None, '', 'κόσμε'] * 9), 'bytes': pa.array([b'\x00\xff', None, b'', b'bytes'] * 9)}),
            'nested': pa.table({'values': pa.array([None, [], [None, 'alpha', 'κόσμε'], ['']] * 9, pa.list_(pa.string())), 'record': pa.array([None, {'x': b'\xff', 'y': 'κόσμε'}, {'x': None, 'y': None}] * 12, pa.struct([('x', pa.binary()), ('y', pa.string())]))}),
            'map': pa.table({'mapping': pa.array([None, [], [('alpha', [b'\x00\xff', None])], [('κόσμε', []), ('', None)]] * 9, pa.map_(pa.string(), pa.list_(pa.binary())))}),
            'empty': pa.table({'text': pa.array([], pa.string()), 'mapping': pa.array([], pa.map_(pa.binary(), pa.string()))}),
        }
        for codec in args.codecs:
            for version in ['1.0', '2.0']:
                for dictionary in [False, True]:
                    for name, table in tables.items():
                        stem = f'{codec}-{version}-{dictionary}-{name}'
                        source = root / (stem + '.parquet')
                        output = root / (stem + '.arrows')
                        pq.write_table(table, source, compression=codec, data_page_version=version, use_dictionary=dictionary, row_group_size=11, write_batch_size=3, max_rows_per_page=3, write_page_index=True, write_page_checksum=True)
                        target = pa.schema(list(map(widen, table.schema)))
                        result = project(args.reader, source, target, output)
                        assert result.returncode == 0, (stem, result.stdout, result.stderr)
                        with ipc.open_stream(output) as reader:
                            actual = reader.read_all()
                        actual.validate(full=True)
                        expected = table.cast(target)
                        assert actual.equals(expected, check_metadata=False), (stem, actual, expected)
                        cases += 1
        # Offset widening never changes UTF-8 into opaque bytes, or vice versa.
        for source_type, target_type, values in [(pa.string(), pa.large_binary(), ['alpha']), (pa.binary(), pa.large_string(), [b'alpha'])]:
            source = root / f'invalid-{rejected}.parquet'
            pq.write_table(pa.table({'v': pa.array(values, source_type)}), source, compression='NONE')
            result = project(args.reader, source, pa.schema([('v', target_type)]), root / f'invalid-{rejected}.arrows')
            assert result.returncode > 0 and 'IncompatibleType' in result.stdout, result
            rejected += 1
        report = {'pyarrow': pa.__version__, 'cases': cases, 'incompatibleRejected': rejected, 'failures': 0, 'skips': 0}
        args.output.write_text(json.dumps(report, indent=2) + '\n')
        print(json.dumps(report, indent=2))


if __name__ == '__main__':
    main()