wireform-parquet-0.1.0.0: scripts/parquet_dictionary_bytes_interop.py
#!/usr/bin/env python3
"""Check dictionary byte keys, slice boundaries and larger payloads independently."""
import argparse
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_dictionary_ieee_interop import dictionary_file
from parquet_writer_options_interop import thrift_at
def fixtures():
def short_values(maximum):
return list(map(lambda size: bytes(size), range(maximum + 1))) + list(map(
lambda size: bytes(range(size)), range(maximum + 1)))
random = []
value = 0x123456789abcdef0
for _ in range(257):
random.append(value)
value = (value * 6364136223846793005 + 1442695040888963407) & (2**64 - 1)
sparse = list(map(lambda word: word.to_bytes(8, 'little'), random[:129]))
wide = []
for index, word in enumerate(random[:129]):
width = (64, 256, 4096)[index % 3]
wide.append(word.to_bytes(8, 'little') + b'a' * (width - 8))
common_ends = list(map(lambda word: b'a' * 24 + word.to_bytes(8, 'little') + b'a' * 32,
random[:129]))
late = list(map(lambda word: word.to_bytes(8, 'little') + b'a' * 56, random[:129]))
late.append(random[0].to_bytes(8, 'little') + b'a' * 16 + b'b' * 8 + b'a' * 32)
late.extend(map(lambda word: word.to_bytes(8, 'little') + b'a' * 56, random[129:]))
late.append(random[0].to_bytes(8, 'little') + b'a' * 32 + b'c' * 8 + b'a' * 16)
cases = {'short': short_values(8), 'tagged-short': short_values(7), 'mixed': short_values(40),
'sparse8': sparse, 'wide': wide, 'common-ends': common_ends,
'late-promotion': late}
for name, cardinality in [('rle-binary', 2), ('rle-mixed', 16), ('rle-wide', 257)]:
values = list(range(cardinality))
for key, count in enumerate((7, 8, 9, 15, 16, 17, 23, 24, 31, 32, 33, 63, 64, 65)):
values.extend([key % cardinality] * count)
values.extend((0, 1, 0))
cases[name] = list(map(lambda value: value.to_bytes(8, 'little'), values))
for name, values in cases.items():
cases[name] = values + list(reversed(values))
return cases
def run_kinds(body):
"""Inspect run framing independently of Wireform and the external engines."""
width, offset, kinds = body[0], 1, set()
assert 0 <= width <= 32
while offset < len(body):
header, shift = 0, 0
while True:
byte = body[offset]
offset += 1
header |= (byte & 127) << shift
if byte < 128:
break
shift += 7
assert shift < 35
assert header > 0
packed = bool(header & 1)
count = header >> 1
assert 0 < count * (8 if packed else 1) <= 2**31 - 1
offset += count * width if packed else (width + 7) // 8
assert offset <= len(body)
kinds.add(packed)
assert offset == len(body)
return kinds
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-bytes-') as directory:
root = Path(directory)
subprocess.run([str(args.probe.resolve()), '--dictionary-bytes', str(root)], check=True)
cases = fixtures()
for name, values in cases.items():
expected = pa.array(values, pa.binary())
table = pa.Table.from_arrays([expected], schema=pa.schema([
pa.field('x', pa.binary(), nullable=False)]))
seed = root / 'seed.parquet'
pq.write_table(table, seed, compression='NONE', use_dictionary=False,
write_statistics=False)
dictionary = (root / f'{name}.dictionary.bin').read_bytes()
data = (root / f'{name}.data.bin').read_bytes()
header, _ = thrift_at(dictionary, 0)
assert header[1] == 2 and header[7][2] == 0
if name.startswith('rle-'):
_, payload_start = thrift_at(data, 0)
assert run_kinds(data[payload_start:]) == {False, True}, name
path = root / f'{name}.parquet'
dictionary_file(seed, path, dictionary, data)
assert pq.read_table(path, use_threads=False)['x'].to_pylist() == values
assert pl.read_parquet(path)['x'].to_list() == values
connection = duckdb.connect(config={'threads': 1})
try:
actual = connection.execute('SELECT x FROM read_parquet(?)', [str(path)]).fetchall()
assert list(map(lambda row: row[0], actual)) == values
finally:
connection.close()
print(f'PASS {name}: exact byte arrays in PyArrow, DuckDB and Polars')
seed.unlink()
if args.reader:
decoded = root / 'decoded'
subprocess.run([str(args.reader.resolve()), str(root), '--arrow-output', str(decoded)], check=True)
for name, values in cases.items():
with ipc.open_stream(decoded / (name + '.parquet.arrows')) as reader:
actual = reader.read_all()['x'].to_pylist()
assert actual == values
print(f'PASS Wireform Arrow output: exact bytes in all {len(cases)} dictionary files')
if __name__ == '__main__':
main()