packages feed

wireform-parquet-0.1.0.0: scripts/parquet_metadata_interop.py

#!/usr/bin/env python3
"""Check file metadata with independent Apache Thrift and PyArrow codecs."""
import argparse
from dataclasses import dataclass
from pathlib import Path
import struct
import subprocess
import tempfile

import pyarrow as pa
import pyarrow.parquet as pq
from thrift.Thrift import TType
from parquet_lzo_interop import Node, decode, encode


@dataclass
class Entry:
    key: bytes
    value: bytes | None


@dataclass
class ParsedFile:
    prefix: bytes
    footer: Node


def parse(path):
    data = path.read_bytes()
    assert data[:4] == data[-4:] == b"PAR1"
    length = struct.unpack("<I", data[-8:-4])[0]
    result = decode(data[-8-length:-8])
    assert result.consumed == length
    return ParsedFile(data[:-8-length], result.node)


def metadata(path):
    footer = parse(path).footer
    if 5 not in footer.value:
        return None
    field = footer.value[5]
    assert field.kind == TType.LIST and field.element_kind == TType.STRUCT
    result = []
    for item in field.value:
        assert item.kind == TType.STRUCT and item.value[1].kind == TType.STRING
        value = item.value.get(2)
        assert value is None or value.kind == TType.STRING
        result.append(Entry(item.value[1].value, value.value if value is not None else None))
    return result


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--probe", required=True, type=Path)
    args = parser.parse_args()
    with tempfile.TemporaryDirectory(prefix="parquet-metadata-") as directory:
        root = Path(directory)
        source, copied = root / "source", root / "copied"
        source.mkdir()
        subprocess.run([str(args.probe), "--metadata", str(source)], check=True)
        expected = [Entry(b"same", None), Entry(b"same", b""), Entry(b"same", b"last"),
                    Entry("κλειδί".encode(), "世界".encode()), Entry(b"opaque", b"\xff\x00\x80")]
        assert metadata(source / "absent.parquet") is None
        assert metadata(source / "empty.parquet") == []
        assert metadata(source / "entries.parquet") == expected
        schema = pa.schema([pa.field("x", pa.int32())], metadata={b"custom": b"\xff\x00", "name".encode(): "世界".encode()})
        table = pa.Table.from_arrays([pa.array([-2**31, 0, 2**31-1], pa.int32())], schema=schema)
        foreign = source / "foreign.parquet"
        pq.write_table(table, foreign, compression="NONE", write_page_index=False, write_page_checksum=True)
        assert any(entry.key == b"ARROW:schema" for entry in metadata(foreign))
        parsed = parse(foreign)
        fields = parsed.footer.value
        fields[5].value.extend([
            Node(TType.STRUCT, {1: Node(TType.STRING, b"duplicate")}),
            Node(TType.STRUCT, {1: Node(TType.STRING, b"duplicate"), 2: Node(TType.STRING, b"")}),
            Node(TType.STRUCT, {1: Node(TType.STRING, b"duplicate"), 2: Node(TType.STRING, b"last")})])
        raw = encode(parsed.footer)
        (source / "foreign-duplicate.parquet").write_bytes(parsed.prefix + raw + struct.pack("<I", len(raw)) + b"PAR1")
        files = sorted(source.glob("*.parquet"))
        assert len(files) == 5
        before = {path.name: metadata(path) for path in files}
        subprocess.run([str(args.probe), "--copy-footers", str(source), str(copied)], check=True)
        assert {p.name for p in copied.glob("*.parquet")} == set(before)
        for path in files:
            assert pq.read_table(path).column("x").to_pylist() == [-2**31, 0, 2**31-1], path.name
            output = copied / path.name
            assert metadata(output) == before[path.name], path.name
            assert pq.read_table(output).column("x").to_pylist() == [-2**31, 0, 2**31-1], path.name
    print("PASS: 3 native metadata states and5 footer copies; Apache Thrift +PyArrow values,order,duplicates,absent/empty/opaquevalues andARROW:schema")


if __name__ == "__main__":
    main()