#![allow(dead_code)]
use crate::thrift_export::{compact_type, ThriftCompactEncoder};
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParquetType {
Boolean,
Int32,
Int64,
Float,
Double,
ByteArray,
}
impl ParquetType {
#[allow(dead_code)]
pub fn type_name(self) -> &'static str {
match self {
Self::Boolean => "BOOLEAN",
Self::Int32 => "INT32",
Self::Int64 => "INT64",
Self::Float => "FLOAT",
Self::Double => "DOUBLE",
Self::ByteArray => "BYTE_ARRAY",
}
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct ParquetColumn {
pub name: String,
pub ptype: ParquetType,
pub num_values: usize,
pub compressed_size: usize,
pub uncompressed_size: usize,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Default)]
pub struct ParquetRowGroup {
pub num_rows: usize,
pub columns: Vec<ParquetColumn>,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Default)]
pub struct ParquetExport {
pub schema_name: String,
pub row_groups: Vec<ParquetRowGroup>,
pub compression: String,
}
#[allow(dead_code)]
pub fn new_parquet_export(schema_name: &str, compression: &str) -> ParquetExport {
ParquetExport {
schema_name: schema_name.to_string(),
row_groups: Vec::new(),
compression: compression.to_string(),
}
}
#[allow(dead_code)]
pub fn add_row_group(doc: &mut ParquetExport, num_rows: usize) {
doc.row_groups.push(ParquetRowGroup {
num_rows,
columns: Vec::new(),
});
}
#[allow(dead_code)]
pub fn add_parquet_column(
doc: &mut ParquetExport,
name: &str,
ptype: ParquetType,
num_values: usize,
compressed_size: usize,
uncompressed_size: usize,
) {
if let Some(rg) = doc.row_groups.last_mut() {
rg.columns.push(ParquetColumn {
name: name.to_string(),
ptype,
num_values,
compressed_size,
uncompressed_size,
});
}
}
#[allow(dead_code)]
pub fn parquet_row_group_count(doc: &ParquetExport) -> usize {
doc.row_groups.len()
}
#[allow(dead_code)]
pub fn parquet_total_rows(doc: &ParquetExport) -> usize {
doc.row_groups.iter().map(|rg| rg.num_rows).sum()
}
#[allow(dead_code)]
pub fn to_parquet_metadata_json(doc: &ParquetExport) -> String {
let rgs: Vec<String> = doc
.row_groups
.iter()
.map(|rg| {
let cols: Vec<String> = rg
.columns
.iter()
.map(|c| {
format!(
"{{\"name\":\"{}\",\"type\":\"{}\",\"num_values\":{},\"compressed\":{},\"uncompressed\":{}}}",
c.name, c.ptype.type_name(), c.num_values, c.compressed_size, c.uncompressed_size
)
})
.collect();
format!("{{\"num_rows\":{},\"columns\":[{}]}}", rg.num_rows, cols.join(","))
})
.collect();
format!(
"{{\"schema\":\"{}\",\"compression\":\"{}\",\"row_groups\":[{}]}}",
doc.schema_name,
doc.compression,
rgs.join(",")
)
}
#[derive(Debug, Clone)]
pub enum ParquetColumnData {
Int32(Vec<i32>),
Int64(Vec<i64>),
Float(Vec<f32>),
Double(Vec<f64>),
ByteArray(Vec<Vec<u8>>),
Boolean(Vec<bool>),
}
#[derive(Debug, Clone)]
pub struct ParquetColumnWithData {
pub name: String,
pub data: ParquetColumnData,
}
impl ParquetColumnWithData {
#[allow(dead_code)]
pub fn new(name: impl Into<String>, data: ParquetColumnData) -> Self {
ParquetColumnWithData {
name: name.into(),
data,
}
}
}
#[repr(i32)]
#[derive(Debug, Clone, Copy)]
enum ParquetPhysType {
Boolean = 0,
Int32 = 1,
Int64 = 2,
Float = 4,
Double = 5,
ByteArray = 6,
}
impl ParquetColumnData {
fn phys_type(&self) -> ParquetPhysType {
match self {
ParquetColumnData::Boolean(_) => ParquetPhysType::Boolean,
ParquetColumnData::Int32(_) => ParquetPhysType::Int32,
ParquetColumnData::Int64(_) => ParquetPhysType::Int64,
ParquetColumnData::Float(_) => ParquetPhysType::Float,
ParquetColumnData::Double(_) => ParquetPhysType::Double,
ParquetColumnData::ByteArray(_) => ParquetPhysType::ByteArray,
}
}
fn len(&self) -> usize {
match self {
ParquetColumnData::Boolean(v) => v.len(),
ParquetColumnData::Int32(v) => v.len(),
ParquetColumnData::Int64(v) => v.len(),
ParquetColumnData::Float(v) => v.len(),
ParquetColumnData::Double(v) => v.len(),
ParquetColumnData::ByteArray(v) => v.len(),
}
}
fn encode_plain(&self) -> Vec<u8> {
match self {
ParquetColumnData::Boolean(vals) => {
vals.iter().map(|&b| if b { 1u8 } else { 0u8 }).collect()
}
ParquetColumnData::Int32(vals) => {
let mut out = Vec::with_capacity(vals.len() * 4);
for &v in vals {
out.extend_from_slice(&v.to_le_bytes());
}
out
}
ParquetColumnData::Int64(vals) => {
let mut out = Vec::with_capacity(vals.len() * 8);
for &v in vals {
out.extend_from_slice(&v.to_le_bytes());
}
out
}
ParquetColumnData::Float(vals) => {
let mut out = Vec::with_capacity(vals.len() * 4);
for &v in vals {
out.extend_from_slice(&v.to_bits().to_le_bytes());
}
out
}
ParquetColumnData::Double(vals) => {
let mut out = Vec::with_capacity(vals.len() * 8);
for &v in vals {
out.extend_from_slice(&v.to_bits().to_le_bytes());
}
out
}
ParquetColumnData::ByteArray(vals) => {
let total: usize = vals.iter().map(|v| 4 + v.len()).sum();
let mut out = Vec::with_capacity(total);
for v in vals {
out.extend_from_slice(&(v.len() as u32).to_le_bytes());
out.extend_from_slice(v);
}
out
}
}
}
}
fn encode_page_header(num_values: i32, data_size: i32) -> Vec<u8> {
let mut enc = ThriftCompactEncoder::new();
enc.begin_struct();
enc.write_i32_field(1, 0); enc.write_i32_field(2, data_size); enc.write_i32_field(3, data_size); enc.write_struct_field_begin(5);
enc.write_i32_field(1, num_values); enc.write_i32_field(2, 0); enc.write_i32_field(3, 0); enc.write_i32_field(4, 0); enc.end_struct(); enc.end_struct(); enc.into_bytes()
}
fn encode_schema_root(num_children: i32) -> Vec<u8> {
let mut enc = ThriftCompactEncoder::new();
enc.begin_struct();
enc.write_i32_field(3, num_children);
enc.write_string_field(5, b"schema");
enc.end_struct();
enc.into_bytes()
}
fn encode_schema_column(col_name: &str, phys_type: ParquetPhysType) -> Vec<u8> {
let mut enc = ThriftCompactEncoder::new();
enc.begin_struct();
enc.write_i32_field(1, phys_type as i32);
enc.write_string_field(5, col_name.as_bytes());
enc.end_struct();
enc.into_bytes()
}
fn encode_column_metadata(
col_name: &str,
phys_type: ParquetPhysType,
num_values: i64,
total_size: i64,
data_page_offset: i64,
) -> Vec<u8> {
let mut enc = ThriftCompactEncoder::new();
enc.begin_struct();
enc.write_i32_field(1, phys_type as i32);
enc.write_list_field_header(2, compact_type::I32, 1);
{
let zz = ThriftCompactEncoder::zigzag32_pub(0i32);
let varint = ThriftCompactEncoder::encode_varint_pub(zz as u64);
enc.write_raw(&varint);
}
enc.write_list_field_header(3, compact_type::BINARY, 1);
{
let name_bytes = col_name.as_bytes();
let len_varint = ThriftCompactEncoder::encode_varint_pub(name_bytes.len() as u64);
enc.write_raw(&len_varint);
enc.write_raw(name_bytes);
}
enc.write_i32_field(4, 0);
enc.write_i64_field(5, num_values);
enc.write_i64_field(6, total_size);
enc.write_i64_field(7, total_size);
enc.write_i64_field(9, data_page_offset);
enc.end_struct();
enc.into_bytes()
}
fn encode_file_metadata(
columns: &[ParquetColumnWithData],
col_offsets: &[(i64, i64)], num_rows: i64,
) -> Vec<u8> {
let mut enc = ThriftCompactEncoder::new();
enc.begin_struct();
enc.write_i32_field(1, 2);
let schema_count = 1 + columns.len();
enc.write_list_field_header(2, compact_type::STRUCT, schema_count);
enc.write_raw(&encode_schema_root(columns.len() as i32));
for col in columns {
enc.write_raw(&encode_schema_column(&col.name, col.data.phys_type()));
}
enc.write_i64_field(3, num_rows);
enc.write_list_field_header(4, compact_type::STRUCT, 1);
{
let mut rg = ThriftCompactEncoder::new();
rg.begin_struct();
rg.write_list_field_header(1, compact_type::STRUCT, columns.len());
for (i, col) in columns.iter().enumerate() {
let (offset, size) = col_offsets[i];
let meta_bytes = encode_column_metadata(
&col.name,
col.data.phys_type(),
col.data.len() as i64,
size,
offset,
);
let mut cc = ThriftCompactEncoder::new();
cc.begin_struct();
cc.write_struct_field_begin(2);
cc.write_raw(&meta_bytes[1..]); cc.end_struct(); rg.write_raw(&cc.into_bytes());
}
let total_size: i64 = col_offsets.iter().map(|(_, s)| s).sum();
rg.write_i64_field(2, total_size);
rg.write_i64_field(3, num_rows);
rg.end_struct();
enc.write_raw(&rg.into_bytes());
}
enc.end_struct();
enc.into_bytes()
}
#[allow(dead_code)]
pub fn to_parquet_bytes(columns: &[ParquetColumnWithData]) -> Vec<u8> {
const MAGIC: &[u8; 4] = b"PAR1";
if columns.is_empty() {
let meta = encode_file_metadata(&[], &[], 0);
let mut out = Vec::with_capacity(8 + meta.len() + 8);
out.extend_from_slice(MAGIC);
out.extend_from_slice(&meta);
out.extend_from_slice(&(meta.len() as u32).to_le_bytes());
out.extend_from_slice(MAGIC);
return out;
}
let mut out: Vec<u8> = Vec::new();
out.extend_from_slice(MAGIC);
let num_rows = columns[0].data.len() as i64;
let mut col_offsets: Vec<(i64, i64)> = Vec::with_capacity(columns.len());
for col in columns {
let plain_bytes = col.data.encode_plain();
let data_size = plain_bytes.len() as i32;
let num_values = col.data.len() as i32;
let page_header = encode_page_header(num_values, data_size);
let data_page_offset = out.len() as i64;
let total_size = (page_header.len() + plain_bytes.len()) as i64;
out.extend_from_slice(&page_header);
out.extend_from_slice(&plain_bytes);
col_offsets.push((data_page_offset, total_size));
}
let meta = encode_file_metadata(columns, &col_offsets, num_rows);
out.extend_from_slice(&meta);
out.extend_from_slice(&(meta.len() as u32).to_le_bytes());
out.extend_from_slice(MAGIC);
out
}
#[allow(dead_code)]
pub fn export_mesh_parquet_meta(vertex_count: usize, index_count: usize) -> String {
let mut doc = new_parquet_export("Mesh", "SNAPPY");
add_row_group(&mut doc, vertex_count);
add_parquet_column(
&mut doc,
"x",
ParquetType::Float,
vertex_count,
vertex_count * 3,
vertex_count * 4,
);
add_parquet_column(
&mut doc,
"y",
ParquetType::Float,
vertex_count,
vertex_count * 3,
vertex_count * 4,
);
add_parquet_column(
&mut doc,
"z",
ParquetType::Float,
vertex_count,
vertex_count * 3,
vertex_count * 4,
);
add_row_group(&mut doc, index_count);
add_parquet_column(
&mut doc,
"index",
ParquetType::Int32,
index_count,
index_count * 3,
index_count * 4,
);
to_parquet_metadata_json(&doc)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_parquet_export_empty() {
let doc = new_parquet_export("Schema", "NONE");
assert_eq!(parquet_row_group_count(&doc), 0);
}
#[test]
fn test_add_row_group() {
let mut doc = new_parquet_export("S", "NONE");
add_row_group(&mut doc, 100);
assert_eq!(parquet_row_group_count(&doc), 1);
}
#[test]
fn test_total_rows() {
let mut doc = new_parquet_export("S", "NONE");
add_row_group(&mut doc, 50);
add_row_group(&mut doc, 75);
assert_eq!(parquet_total_rows(&doc), 125);
}
#[test]
fn test_add_column() {
let mut doc = new_parquet_export("S", "NONE");
add_row_group(&mut doc, 10);
add_parquet_column(&mut doc, "x", ParquetType::Float, 10, 30, 40);
assert_eq!(doc.row_groups[0].columns.len(), 1);
}
#[test]
fn test_type_name_float() {
assert_eq!(ParquetType::Float.type_name(), "FLOAT");
}
#[test]
fn test_type_name_int32() {
assert_eq!(ParquetType::Int32.type_name(), "INT32");
}
#[test]
fn test_to_json_contains_schema() {
let doc = new_parquet_export("MySchema", "GZIP");
let s = to_parquet_metadata_json(&doc);
assert!(s.contains("MySchema"));
}
#[test]
fn test_to_json_contains_compression() {
let doc = new_parquet_export("S", "SNAPPY");
let s = to_parquet_metadata_json(&doc);
assert!(s.contains("SNAPPY"));
}
#[test]
fn test_export_mesh_parquet_meta() {
let s = export_mesh_parquet_meta(100, 300);
assert!(s.contains("Mesh"));
assert!(s.contains("SNAPPY"));
}
#[test]
fn test_export_mesh_parquet_two_row_groups() {
let s = export_mesh_parquet_meta(10, 30);
assert_eq!(s.matches("num_rows").count(), 2);
}
#[test]
fn parquet_bytes_magic_at_start_and_end() {
let cols = vec![ParquetColumnWithData::new(
"x",
ParquetColumnData::Int32(vec![1, 2, 3]),
)];
let bytes = to_parquet_bytes(&cols);
assert_eq!(&bytes[..4], b"PAR1", "leading magic");
assert_eq!(&bytes[bytes.len() - 4..], b"PAR1", "trailing magic");
}
#[test]
fn parquet_bytes_footer_length_consistent() {
let cols = vec![ParquetColumnWithData::new(
"v",
ParquetColumnData::Double(vec![1.0, 2.0]),
)];
let bytes = to_parquet_bytes(&cols);
let len = bytes.len();
let footer_len =
u32::from_le_bytes([bytes[len - 8], bytes[len - 7], bytes[len - 6], bytes[len - 5]])
as usize;
assert!(footer_len > 0, "footer length must be > 0");
assert!(footer_len + 12 <= len, "footer fits within file");
}
#[test]
fn parquet_bytes_int32_plain_encoding() {
let values: Vec<i32> = vec![0x01020304, -1];
let cols = vec![ParquetColumnWithData::new(
"col",
ParquetColumnData::Int32(values.clone()),
)];
let bytes = to_parquet_bytes(&cols);
let needle: [u8; 4] = 0x01020304_i32.to_le_bytes();
let found = bytes.windows(4).any(|w| w == needle);
assert!(found, "PLAIN LE bytes for 0x01020304 must appear in output");
}
#[test]
fn parquet_bytes_bytearray_length_prefix() {
let payload = b"hello";
let cols = vec![ParquetColumnWithData::new(
"s",
ParquetColumnData::ByteArray(vec![payload.to_vec()]),
)];
let bytes = to_parquet_bytes(&cols);
let len_bytes: [u8; 4] = (payload.len() as u32).to_le_bytes();
let found_prefix = bytes.windows(4).any(|w| w == len_bytes);
assert!(found_prefix, "ByteArray length prefix must appear in output");
let found_payload = bytes
.windows(payload.len())
.any(|w| w == payload.as_slice());
assert!(found_payload, "ByteArray payload must appear in output");
}
#[test]
fn parquet_bytes_empty_columns() {
let bytes = to_parquet_bytes(&[]);
assert!(bytes.len() >= 12, "minimum 12 bytes (2*magic + footer_len)");
assert_eq!(&bytes[..4], b"PAR1");
assert_eq!(&bytes[bytes.len() - 4..], b"PAR1");
}
#[test]
fn parquet_bytes_double_round_trip() {
let val: f64 = std::f64::consts::PI;
let cols = vec![ParquetColumnWithData::new(
"pi",
ParquetColumnData::Double(vec![val]),
)];
let bytes = to_parquet_bytes(&cols);
let needle = val.to_bits().to_le_bytes();
let found = bytes.windows(8).any(|w| w == needle);
assert!(found, "PI double LE bits must appear in output");
}
}