#![cfg(target_arch = "wasm32")]
use super::http_range::{ByteRange, RangeClient};
use wasm_bindgen::prelude::*;
use web_sys::console;
const PARQUET_MAGIC: &[u8] = b"PAR1";
const FOOTER_METADATA_SIZE: u64 = 8;
const MAX_FOOTER_SIZE: u64 = 10 * 1024 * 1024;
pub struct StreamingParquetReader {
client: RangeClient,
file_size: u64,
metadata: Option<ParquetMetadata>,
}
impl StreamingParquetReader {
pub async fn new(url: impl Into<String>) -> Result<Self, JsValue> {
let client = RangeClient::new(url);
let file_size = client.get_file_size().await?;
console::log_1(&format!("Parquet file size: {} bytes", file_size).into());
Ok(Self { client, file_size, metadata: None })
}
pub async fn read_metadata(&mut self) -> Result<&ParquetMetadata, JsValue> {
if self.metadata.is_some() {
return Ok(self.metadata.as_ref().expect("metadata checked Some above"));
}
let footer_range =
ByteRange::new(self.file_size - FOOTER_METADATA_SIZE, self.file_size - 1);
let footer_bytes = self.client.fetch_range(footer_range).await?;
if &footer_bytes[4..8] != PARQUET_MAGIC {
return Err(JsValue::from_str("Invalid Parquet file: missing magic number"));
}
let footer_length = u32::from_le_bytes([
footer_bytes[0],
footer_bytes[1],
footer_bytes[2],
footer_bytes[3],
]) as u64;
if footer_length > MAX_FOOTER_SIZE {
return Err(JsValue::from_str(&format!(
"Footer too large: {} bytes (max {})",
footer_length, MAX_FOOTER_SIZE
)));
}
console::log_1(&format!("Reading Parquet footer: {} bytes", footer_length).into());
let footer_start = self.file_size - FOOTER_METADATA_SIZE - footer_length;
let footer_range = ByteRange::new(footer_start, self.file_size - FOOTER_METADATA_SIZE - 1);
let footer_data = self.client.fetch_range(footer_range).await?;
let metadata = self.parse_footer(&footer_data)?;
self.metadata = Some(metadata);
Ok(self.metadata.as_ref().expect("metadata just assigned above"))
}
fn parse_footer(&self, _data: &[u8]) -> Result<ParquetMetadata, JsValue> {
console::log_1(&"Parsing Parquet footer (stub implementation)".into());
Ok(ParquetMetadata { num_rows: 0, row_groups: vec![], schema: ParquetSchema::default() })
}
pub async fn read_row_group(&self, index: usize) -> Result<Vec<u8>, JsValue> {
let metadata = self
.metadata
.as_ref()
.ok_or_else(|| JsValue::from_str("Metadata not loaded. Call read_metadata() first"))?;
if index >= metadata.row_groups.len() {
return Err(JsValue::from_str(&format!(
"Row group index {} out of bounds (total: {})",
index,
metadata.row_groups.len()
)));
}
let row_group = &metadata.row_groups[index];
let range = ByteRange::new(
row_group.file_offset,
row_group.file_offset + row_group.total_byte_size - 1,
);
console::log_1(
&format!(
"Reading row group {}: {} bytes at offset {}",
index, row_group.total_byte_size, row_group.file_offset
)
.into(),
);
self.client.fetch_range(range).await
}
pub async fn read_columns(
&self,
row_group_index: usize,
column_indices: &[usize],
) -> Result<Vec<Vec<u8>>, JsValue> {
let metadata =
self.metadata.as_ref().ok_or_else(|| JsValue::from_str("Metadata not loaded"))?;
let row_group = &metadata.row_groups[row_group_index];
let mut column_data = Vec::new();
for &col_idx in column_indices {
if col_idx >= row_group.columns.len() {
return Err(JsValue::from_str(&format!("Column index {} out of bounds", col_idx)));
}
let column = &row_group.columns[col_idx];
let range = ByteRange::new(
column.file_offset,
column.file_offset + column.total_compressed_size - 1,
);
let bytes = self.client.fetch_range(range).await?;
column_data.push(bytes);
}
Ok(column_data)
}
}
#[derive(Debug, Clone)]
pub struct ParquetMetadata {
pub num_rows: i64,
pub row_groups: Vec<RowGroupMetadata>,
pub schema: ParquetSchema,
}
#[derive(Debug, Clone)]
pub struct RowGroupMetadata {
pub num_rows: i64,
pub file_offset: u64,
pub total_byte_size: u64,
pub columns: Vec<ColumnChunkMetadata>,
}
#[derive(Debug, Clone)]
pub struct ColumnChunkMetadata {
pub name: String,
pub file_offset: u64,
pub total_compressed_size: u64,
pub statistics: Option<ColumnStatistics>,
}
#[derive(Debug, Clone)]
pub struct ColumnStatistics {
pub min: Vec<u8>,
pub max: Vec<u8>,
pub null_count: i64,
}
#[derive(Debug, Clone, Default)]
pub struct ParquetSchema {
pub columns: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parquet_magic_constant() {
assert_eq!(PARQUET_MAGIC, b"PAR1");
assert_eq!(PARQUET_MAGIC.len(), 4);
}
#[test]
fn test_footer_metadata_size() {
assert_eq!(FOOTER_METADATA_SIZE, 8);
}
#[test]
fn test_max_footer_size_reasonable() {
assert_eq!(MAX_FOOTER_SIZE, 10 * 1024 * 1024);
}
}