From 54bc343e29c43c0d8c18d95b2ee796fd2afa5c44 Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Fri, 6 Feb 2026 18:48:08 -0500 Subject: [PATCH 01/50] Add alp to the thrift definitions --- parquet/src/basic.rs | 10 ++++++++++ parquet/src/format.rs | 5 ++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/parquet/src/basic.rs b/parquet/src/basic.rs index ba8ffc2e92c3..b251058bd6e9 100644 --- a/parquet/src/basic.rs +++ b/parquet/src/basic.rs @@ -634,6 +634,10 @@ enum Encoding { /// afterwards. Note that the use of this encoding with FIXED_LEN_BYTE_ARRAY(N) data may /// perform poorly for large values of N. BYTE_STREAM_SPLIT = 9; + /// Adaptive Lossless floating-Point encoding (ALP). + /// + /// Currently specified for FLOAT and DOUBLE. + ALP = 10; } ); @@ -654,6 +658,7 @@ impl FromStr for Encoding { "DELTA_BYTE_ARRAY" | "delta_byte_array" => Ok(Encoding::DELTA_BYTE_ARRAY), "RLE_DICTIONARY" | "rle_dictionary" => Ok(Encoding::RLE_DICTIONARY), "BYTE_STREAM_SPLIT" | "byte_stream_split" => Ok(Encoding::BYTE_STREAM_SPLIT), + "ALP" | "alp" => Ok(Encoding::ALP), _ => Err(general_err!("unknown encoding: {}", s)), } } @@ -791,6 +796,7 @@ fn i32_to_encoding(val: i32) -> Encoding { 7 => Encoding::DELTA_BYTE_ARRAY, 8 => Encoding::RLE_DICTIONARY, 9 => Encoding::BYTE_STREAM_SPLIT, + 10 => Encoding::ALP, _ => panic!("Impossible encoding {val}"), } } @@ -2137,6 +2143,7 @@ mod tests { ); assert_eq!(Encoding::DELTA_BYTE_ARRAY.to_string(), "DELTA_BYTE_ARRAY"); assert_eq!(Encoding::RLE_DICTIONARY.to_string(), "RLE_DICTIONARY"); + assert_eq!(Encoding::ALP.to_string(), "ALP"); } #[test] @@ -2438,6 +2445,8 @@ mod tests { assert_eq!(encoding, Encoding::RLE_DICTIONARY); encoding = "BYTE_STREAM_SPLIT".parse().unwrap(); assert_eq!(encoding, Encoding::BYTE_STREAM_SPLIT); + encoding = "alp".parse().unwrap(); + assert_eq!(encoding, Encoding::ALP); // test lowercase encoding = "byte_stream_split".parse().unwrap(); @@ -2573,6 +2582,7 @@ mod tests { Encoding::PLAIN_DICTIONARY, Encoding::RLE_DICTIONARY, Encoding::BYTE_STREAM_SPLIT, + Encoding::ALP, ]; encodings_roundtrip(encodings.into()); } diff --git a/parquet/src/format.rs b/parquet/src/format.rs index 101799d00350..402f20ddf488 100644 --- a/parquet/src/format.rs +++ b/parquet/src/format.rs @@ -450,6 +450,8 @@ impl Encoding { /// Added in 2.8 for FLOAT and DOUBLE. /// Support for INT32, INT64 and FIXED_LEN_BYTE_ARRAY added in 2.11. pub const BYTE_STREAM_SPLIT: Encoding = Encoding(9); + /// Adaptive Lossless floating-Point encoding (ALP) for FLOAT and DOUBLE. + pub const ALP: Encoding = Encoding(10); pub const ENUM_VALUES: &'static [Self] = &[ Self::PLAIN, Self::PLAIN_DICTIONARY, @@ -460,6 +462,7 @@ impl Encoding { Self::DELTA_BYTE_ARRAY, Self::RLE_DICTIONARY, Self::BYTE_STREAM_SPLIT, + Self::ALP, ]; } @@ -486,6 +489,7 @@ impl From for Encoding { 7 => Encoding::DELTA_BYTE_ARRAY, 8 => Encoding::RLE_DICTIONARY, 9 => Encoding::BYTE_STREAM_SPLIT, + 10 => Encoding::ALP, _ => Encoding(i) } } @@ -6041,4 +6045,3 @@ impl crate::thrift::TSerializable for FileCryptoMetaData { o_prot.write_struct_end() } } - From 622841b3c7e05231755e7398b2cac836f3339365 Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Fri, 6 Feb 2026 18:48:29 -0500 Subject: [PATCH 02/50] Add alp to the encoding enum --- parquet/src/encodings/decoding.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/parquet/src/encodings/decoding.rs b/parquet/src/encodings/decoding.rs index 58430820a9b6..4b3e937df124 100644 --- a/parquet/src/encodings/decoding.rs +++ b/parquet/src/encodings/decoding.rs @@ -67,6 +67,9 @@ pub(crate) mod private { "Encoding {} is not supported for type", encoding )), + Encoding::ALP => Err(nyi_err!( + "Encoding ALP is not implemented" + )), e => Err(nyi_err!("Encoding {} is not supported", e)), } } From bf754b2eb8b53fbbbe943abf52a72c3d0fd27b9c Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Thu, 12 Feb 2026 16:43:49 -0500 Subject: [PATCH 03/50] Add alp header and page layout structs --- parquet-testing | 2 +- parquet/src/encodings/decoding.rs | 12 +- parquet/src/encodings/decoding/alp.rs | 265 ++++++++++++++++++++++++++ parquet/tests/arrow_reader/alp.rs | 41 ++++ parquet/tests/arrow_reader/mod.rs | 1 + 5 files changed, 316 insertions(+), 5 deletions(-) create mode 100644 parquet/src/encodings/decoding/alp.rs create mode 100644 parquet/tests/arrow_reader/alp.rs diff --git a/parquet-testing b/parquet-testing index a3d96a65e11e..abd2478aea41 160000 --- a/parquet-testing +++ b/parquet-testing @@ -1 +1 @@ -Subproject commit a3d96a65e11e2bbca7d22a894e8313ede90a33a3 +Subproject commit abd2478aea41d6cb10352b2277a7dc67ae20a02a diff --git a/parquet/src/encodings/decoding.rs b/parquet/src/encodings/decoding.rs index 4b3e937df124..36c7f6720104 100644 --- a/parquet/src/encodings/decoding.rs +++ b/parquet/src/encodings/decoding.rs @@ -29,10 +29,12 @@ use crate::data_type::*; use crate::encodings::decoding::byte_stream_split_decoder::{ ByteStreamSplitDecoder, VariableWidthByteStreamSplitDecoder, }; +use crate::encodings::decoding::alp::AlpDecoder; use crate::errors::{ParquetError, Result}; use crate::schema::types::ColumnDescPtr; use crate::util::bit_util::{self, BitReader}; +mod alp; mod byte_stream_split_decoder; pub(crate) mod private { @@ -63,13 +65,11 @@ pub(crate) mod private { Encoding::RLE | Encoding::DELTA_BINARY_PACKED | Encoding::DELTA_BYTE_ARRAY - | Encoding::DELTA_LENGTH_BYTE_ARRAY => Err(general_err!( + | Encoding::DELTA_LENGTH_BYTE_ARRAY + | Encoding::ALP => Err(general_err!( "Encoding {} is not supported for type", encoding )), - Encoding::ALP => Err(nyi_err!( - "Encoding ALP is not implemented" - )), e => Err(nyi_err!("Encoding {} is not supported", e)), } } @@ -119,6 +119,7 @@ pub(crate) mod private { ) -> Result>> { match encoding { Encoding::BYTE_STREAM_SPLIT => Ok(Box::new(ByteStreamSplitDecoder::new())), + Encoding::ALP => Ok(Box::new(AlpDecoder::new())), _ => get_decoder_default(descr, encoding), } } @@ -130,6 +131,7 @@ pub(crate) mod private { ) -> Result>> { match encoding { Encoding::BYTE_STREAM_SPLIT => Ok(Box::new(ByteStreamSplitDecoder::new())), + Encoding::ALP => Ok(Box::new(AlpDecoder::new())), _ => get_decoder_default(descr, encoding), } } @@ -1138,6 +1140,8 @@ mod tests { create_and_check_decoder::(Encoding::DELTA_LENGTH_BYTE_ARRAY, None); create_and_check_decoder::(Encoding::DELTA_BYTE_ARRAY, None); create_and_check_decoder::(Encoding::RLE, None); + create_and_check_decoder::(Encoding::ALP, None); + create_and_check_decoder::(Encoding::ALP, None); // error when initializing create_and_check_decoder::( diff --git a/parquet/src/encodings/decoding/alp.rs b/parquet/src/encodings/decoding/alp.rs new file mode 100644 index 000000000000..fa436961fcdb --- /dev/null +++ b/parquet/src/encodings/decoding/alp.rs @@ -0,0 +1,265 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use std::marker::PhantomData; + +use bytes::Bytes; + +use crate::basic::Encoding; +use crate::data_type::DataType; +use crate::encodings::decoding::Decoder; +use crate::errors::{ParquetError, Result}; + +const ALP_HEADER_SIZE: usize = 8; +const ALP_VERSION: u8 = 1; +const ALP_COMPRESSION_MODE: u8 = 0; +const ALP_INTEGER_ENCODING_FOR_BIT_PACK: u8 = 0; +const ALP_MAX_LOG_VECTOR_SIZE: u8 = 16; + +#[derive(Debug, Clone, Copy)] +struct AlpHeader { + version: u8, + compression_mode: u8, + integer_encoding: u8, + log_vector_size: u8, + num_elements: u32, +} + +#[derive(Debug)] +struct AlpPageLayout { + header: AlpHeader, + offsets: Vec, +} + +fn parse_alp_page_layout(data: &[u8]) -> Result { + if data.len() < ALP_HEADER_SIZE { + return Err(general_err!( + "Invalid ALP page: expected at least {} bytes for header, got {}", + ALP_HEADER_SIZE, + data.len() + )); + } + + let header = AlpHeader { + version: data[0], + compression_mode: data[1], + integer_encoding: data[2], + log_vector_size: data[3], + num_elements: u32::from_le_bytes([data[4], data[5], data[6], data[7]]), + }; + + if header.version != ALP_VERSION { + return Err(general_err!( + "Invalid ALP page: unsupported version {}, expected {}", + header.version, + ALP_VERSION + )); + } + + if header.compression_mode != ALP_COMPRESSION_MODE { + return Err(general_err!( + "Invalid ALP page: unsupported compression mode {}", + header.compression_mode + )); + } + + if header.integer_encoding != ALP_INTEGER_ENCODING_FOR_BIT_PACK { + return Err(general_err!( + "Invalid ALP page: unsupported integer encoding {}", + header.integer_encoding + )); + } + + if header.log_vector_size > ALP_MAX_LOG_VECTOR_SIZE { + return Err(general_err!( + "Invalid ALP page: log_vector_size {} exceeds max {}", + header.log_vector_size, + ALP_MAX_LOG_VECTOR_SIZE + )); + } + + let vector_size = 1usize << header.log_vector_size; + let num_vectors = if header.num_elements == 0 { + 0 + } else { + (header.num_elements as usize).div_ceil(vector_size) + }; + + let offsets_len = num_vectors + .checked_mul(std::mem::size_of::()) + .ok_or_else(|| general_err!("Invalid ALP page: offsets length overflow"))?; + let offsets_end = ALP_HEADER_SIZE + .checked_add(offsets_len) + .ok_or_else(|| general_err!("Invalid ALP page: header + offsets length overflow"))?; + + if data.len() < offsets_end { + return Err(general_err!( + "Invalid ALP page: expected at least {} bytes for {} offsets, got {}", + offsets_end, + num_vectors, + data.len() + )); + } + + let body_len = data.len() - ALP_HEADER_SIZE; + let mut offsets = Vec::with_capacity(num_vectors); + for i in 0..num_vectors { + let start = ALP_HEADER_SIZE + i * 4; + let offset = u32::from_le_bytes([ + data[start], + data[start + 1], + data[start + 2], + data[start + 3], + ]); + if offset as usize >= body_len { + return Err(general_err!( + "Invalid ALP page: vector offset {} out of bounds for body length {}", + offset, + body_len + )); + } + offsets.push(offset); + } + + Ok(AlpPageLayout { header, offsets }) +} + +pub(crate) struct AlpDecoder { + num_values: usize, + layout: Option, + _marker: PhantomData, +} + +impl AlpDecoder { + pub(crate) fn new() -> Self { + Self { + num_values: 0, + layout: None, + _marker: PhantomData, + } + } +} + +impl Decoder for AlpDecoder { + fn set_data(&mut self, data: Bytes, num_values: usize) -> Result<()> { + let layout = parse_alp_page_layout(data.as_ref())?; + if layout.header.num_elements as usize != num_values { + return Err(general_err!( + "Invalid ALP page: header num_elements {} does not match page num_values {}", + layout.header.num_elements, + num_values + )); + } + + self.num_values = num_values; + self.layout = Some(layout); + Ok(()) + } + + fn get(&mut self, _buffer: &mut [T::T]) -> Result { + // Parsing succeeds in set_data; value decoding is introduced in a follow-up step. + let num_vectors = self + .layout + .as_ref() + .map(|layout| layout.offsets.len()) + .unwrap_or(0); + Err(nyi_err!( + "Encoding ALP page layout parsed ({} vectors), value decoding is not implemented", + num_vectors + )) + } + + fn values_left(&self) -> usize { + self.num_values + } + + fn encoding(&self) -> Encoding { + Encoding::ALP + } + + fn skip(&mut self, num_values: usize) -> Result { + let skipped = num_values.min(self.num_values); + self.num_values -= skipped; + Ok(skipped) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_alp_page_bytes( + version: u8, + compression_mode: u8, + integer_encoding: u8, + log_vector_size: u8, + num_elements: u32, + offsets: &[u32], + body_tail_len: usize, + ) -> Vec { + let mut out = Vec::with_capacity(ALP_HEADER_SIZE + offsets.len() * 4 + body_tail_len); + out.push(version); + out.push(compression_mode); + out.push(integer_encoding); + out.push(log_vector_size); + out.extend_from_slice(&num_elements.to_le_bytes()); + for offset in offsets { + out.extend_from_slice(&offset.to_le_bytes()); + } + out.extend(std::iter::repeat_n(0u8, body_tail_len)); + out + } + + #[test] + fn test_parse_alp_page_layout_valid() { + // num_elements=4 with vector_size=4 -> one vector, one offset entry. + let data = make_alp_page_bytes(1, 0, 0, 2, 4, &[4], 8); + let parsed = parse_alp_page_layout(&data).unwrap(); + assert_eq!(parsed.header.version, 1); + assert_eq!(parsed.header.num_elements, 4); + assert_eq!(parsed.offsets, vec![4]); + } + + #[test] + fn test_parse_alp_page_layout_short_header() { + let err = parse_alp_page_layout(&[0, 1, 2]).unwrap_err(); + assert!( + err.to_string() + .contains("Invalid ALP page: expected at least 8 bytes for header") + ); + } + + #[test] + fn test_parse_alp_page_layout_invalid_log_vector_size() { + let data = make_alp_page_bytes(1, 0, 0, 17, 1, &[4], 8); + let err = parse_alp_page_layout(&data).unwrap_err(); + assert!( + err.to_string() + .contains("Invalid ALP page: log_vector_size 17 exceeds max 16") + ); + } + + #[test] + fn test_parse_alp_page_layout_invalid_integer_encoding() { + let data = make_alp_page_bytes(1, 0, 1, 2, 1, &[4], 8); + let err = parse_alp_page_layout(&data).unwrap_err(); + assert!( + err.to_string() + .contains("Invalid ALP page: unsupported integer encoding 1") + ); + } +} diff --git a/parquet/tests/arrow_reader/alp.rs b/parquet/tests/arrow_reader/alp.rs new file mode 100644 index 000000000000..e92a1ef7797f --- /dev/null +++ b/parquet/tests/arrow_reader/alp.rs @@ -0,0 +1,41 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use arrow::util::test_util::parquet_test_data; +use parquet::arrow::arrow_reader::ArrowReaderBuilder; + +#[test] +fn test_read_single_f64_alp_page_layout() { + let path = std::path::PathBuf::from(parquet_test_data()).join("single_f64_ALP.parquet"); + if !path.exists() { + eprintln!("Skipping ALP test file not found: {}", path.display()); + return; + } + + let file = std::fs::File::open(path).unwrap(); + let mut reader = ArrowReaderBuilder::try_new(file).unwrap().build().unwrap(); + + let err = loop { + match reader.next() { + Some(Ok(_)) => continue, + Some(Err(err)) => break err, + None => panic!("Expected ALP decode to fail with NYI"), + } + }; + + assert!(err.to_string().contains("Encoding ALP page layout parsed")); +} diff --git a/parquet/tests/arrow_reader/mod.rs b/parquet/tests/arrow_reader/mod.rs index ffc36655b39a..24bfae76e081 100644 --- a/parquet/tests/arrow_reader/mod.rs +++ b/parquet/tests/arrow_reader/mod.rs @@ -38,6 +38,7 @@ use parquet::file::properties::{ use std::sync::Arc; use tempfile::NamedTempFile; +mod alp; mod bad_data; mod bloom_filter; #[cfg(feature = "crc")] From 8337fcd18f030b67b111f219838029f51d3b890e Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Sun, 15 Feb 2026 11:14:51 -0500 Subject: [PATCH 04/50] =?UTF-8?q?Add=20Interleaved=20Vectors=E2=80=8B=20su?= =?UTF-8?q?pport?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- parquet/src/encodings/decoding/alp.rs | 524 ++++++++++++++++++++++++-- 1 file changed, 498 insertions(+), 26 deletions(-) diff --git a/parquet/src/encodings/decoding/alp.rs b/parquet/src/encodings/decoding/alp.rs index fa436961fcdb..da9adb0ba3df 100644 --- a/parquet/src/encodings/decoding/alp.rs +++ b/parquet/src/encodings/decoding/alp.rs @@ -16,6 +16,7 @@ // under the License. use std::marker::PhantomData; +use std::ops::Range; use bytes::Bytes; @@ -30,22 +31,287 @@ const ALP_COMPRESSION_MODE: u8 = 0; const ALP_INTEGER_ENCODING_FOR_BIT_PACK: u8 = 0; const ALP_MAX_LOG_VECTOR_SIZE: u8 = 16; +/// Page-level ALP header (version 1, 8 bytes). +/// +/// Layout in bytes: +/// - `[0]` `version` +/// - `[1]` `compression_mode` +/// - `[2]` `integer_encoding` +/// - `[3]` `log_vector_size` +/// - `[4..8]` `num_elements` (little-endian `i32`) +/// +/// This mirrors the C++ `AlpHeader` in `arrow/util/alp/alp_wrapper.cc`. #[derive(Debug, Clone, Copy)] struct AlpHeader { version: u8, compression_mode: u8, integer_encoding: u8, log_vector_size: u8, - num_elements: u32, + num_elements: i32, } +impl AlpHeader { + fn num_elements_usize(&self) -> usize { + // `num_elements` is serialized as i32, matching Parquet page header `num_values`. + // Parsing rejects negative values, so this conversion is safe. + self.num_elements as usize + } + + /// Returns `2 ^ log_vector_size`. + fn vector_size(&self) -> usize { + 1usize << self.log_vector_size + } + + /// Number of vectors in the page. + /// + /// All vectors are full-sized except possibly the last one. + fn num_vectors(&self) -> usize { + if self.num_elements == 0 { + 0 + } else { + self.num_elements_usize().div_ceil(self.vector_size()) + } + } + + /// Number of logical values in `vector_index`. + fn vector_num_elements(&self, vector_index: usize) -> u16 { + let vector_size = self.vector_size(); + let num_full_vectors = self.num_elements_usize() / vector_size; + let remainder = self.num_elements_usize() % vector_size; + if vector_index < num_full_vectors { + vector_size as u16 + } else if vector_index == num_full_vectors && remainder > 0 { + remainder as u16 + } else { + 0 + } + } +} + +/// Per-vector ALP metadata (4 bytes), equivalent to C++ `AlpEncodedVectorInfo`. +/// +/// Layout in bytes: +/// - `[0]` `exponent` +/// - `[1]` `factor` +/// - `[2..4]` `num_exceptions` (little-endian `u16`) +#[derive(Debug, Clone, Copy)] +struct AlpEncodedVectorInfo { + exponent: u8, + factor: u8, + num_exceptions: u16, +} + +impl AlpEncodedVectorInfo { + const STORED_SIZE: usize = 4; +} + +/// Per-vector FOR metadata for a given exact integer type (`u32` for `f32`, +/// `u64` for `f64`), equivalent to C++ `AlpEncodedForVectorInfo`. +#[derive(Debug, Clone, Copy)] +struct AlpEncodedForVectorInfo { + frame_of_reference: Exact, + bit_width: u8, +} + +impl AlpEncodedForVectorInfo { + /// Serialized size of FOR metadata (`Exact::WIDTH + 1` for `bit_width`). + fn stored_size() -> usize { + Exact::WIDTH + 1 + } + + /// Number of bytes used for bit-packed encoded integers. + fn get_bit_packed_size(&self, num_elements: u16) -> usize { + (self.bit_width as usize * num_elements as usize).div_ceil(8) + } + + /// Data-only size: + /// `[packed_values][exception_positions][exception_values]` + fn get_data_stored_size(&self, num_elements: u16, num_exceptions: u16) -> usize { + let bit_packed_size = self.get_bit_packed_size(num_elements); + bit_packed_size + + num_exceptions as usize * std::mem::size_of::() + + num_exceptions as usize * Exact::WIDTH + } +} + +/// Parsed view of one vector's metadata and data slices. +/// +/// Note: +/// - `packed_values` is expressed as a range into the vector data section +/// - exceptions are copied into owned vectors for safety and simplicity #[derive(Debug)] -struct AlpPageLayout { +struct AlpEncodedVectorView { + num_elements: u16, + alp_info: AlpEncodedVectorInfo, + for_info: AlpEncodedForVectorInfo, + packed_values: Range, + exception_positions: Vec, + exception_values: Vec, +} + +/// Parsed ALP layout for a page with a concrete exact integer type. +/// +/// `Exact = u32` for `f32` pages and `Exact = u64` for `f64` pages. +#[derive(Debug)] +struct AlpPageLayout { header: AlpHeader, offsets: Vec, + vectors: Vec>, +} + +/// Type-erased page layout used by `AlpDecoder` so we can keep one decoder +/// implementation while preserving exact-width FOR storage. +#[derive(Debug)] +enum AlpPageLayoutAny { + F32(AlpPageLayout), + F64(AlpPageLayout), +} + +impl AlpPageLayoutAny { + fn num_vectors(&self) -> usize { + match self { + Self::F32(layout) => layout.vectors.len(), + Self::F64(layout) => layout.vectors.len(), + } + } + + fn num_offsets(&self) -> usize { + match self { + Self::F32(layout) => layout.offsets.len(), + Self::F64(layout) => layout.offsets.len(), + } + } + + fn parsed_values(&self) -> usize { + match self { + Self::F32(layout) => layout.vectors.iter().map(|v| v.num_elements as usize).sum(), + Self::F64(layout) => layout.vectors.iter().map(|v| v.num_elements as usize).sum(), + } + } + + fn total_exceptions(&self) -> usize { + match self { + Self::F32(layout) => layout + .vectors + .iter() + .map(|v| v.alp_info.num_exceptions as usize) + .sum(), + Self::F64(layout) => layout + .vectors + .iter() + .map(|v| v.alp_info.num_exceptions as usize) + .sum(), + } + } + + fn total_packed_bytes(&self) -> usize { + match self { + Self::F32(layout) => layout + .vectors + .iter() + .map(|v| v.packed_values.end - v.packed_values.start) + .sum(), + Self::F64(layout) => layout + .vectors + .iter() + .map(|v| v.packed_values.end - v.packed_values.start) + .sum(), + } + } + + fn total_exception_bytes(&self) -> usize { + match self { + Self::F32(layout) => layout.vectors.iter().map(|v| v.exception_values.len()).sum(), + Self::F64(layout) => layout.vectors.iter().map(|v| v.exception_values.len()).sum(), + } + } + + fn sum_for_xor(&self) -> u64 { + match self { + Self::F32(layout) => layout + .vectors + .iter() + .fold(0u64, |acc, v| acc ^ v.for_info.frame_of_reference.to_u64()), + Self::F64(layout) => layout + .vectors + .iter() + .fold(0u64, |acc, v| acc ^ v.for_info.frame_of_reference.to_u64()), + } + } + + fn sum_positions(&self) -> usize { + match self { + Self::F32(layout) => layout + .vectors + .iter() + .flat_map(|v| v.exception_positions.iter()) + .map(|v| *v as usize) + .sum(), + Self::F64(layout) => layout + .vectors + .iter() + .flat_map(|v| v.exception_positions.iter()) + .map(|v| *v as usize) + .sum(), + } + } +} + +/// Exact integer type used by FOR reconstruction. +/// +/// This mirrors C++: +/// - `float` -> `uint32_t` +/// - `double` -> `uint64_t` +/// +/// Why unsigned (not `i32`/`i64`)? +/// - FOR computes and stores deltas (`value - frame_of_reference`) that are +/// encoded with bitpacking and are naturally treated as non-negative bit +/// patterns. +/// - Using unsigned exact-width integers avoids signed-overflow edge cases in +/// the FOR stage and matches the C++ implementation's arithmetic model. +/// - Signed interpretation is still applied later during ALP decimal +/// reconstruction (after inverse FOR), so we preserve behavior while keeping +/// FOR metadata compact and byte-compatible. +trait AlpExact: Copy + std::fmt::Debug { + const WIDTH: usize; + fn from_le_slice(slice: &[u8]) -> Self; + fn to_u64(self) -> u64; +} + +impl AlpExact for u32 { + const WIDTH: usize = 4; + + fn from_le_slice(slice: &[u8]) -> Self { + u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]]) + } + + fn to_u64(self) -> u64 { + self as u64 + } +} + +impl AlpExact for u64 { + const WIDTH: usize = 8; + + fn from_le_slice(slice: &[u8]) -> Self { + u64::from_le_bytes([ + slice[0], slice[1], slice[2], slice[3], slice[4], slice[5], slice[6], slice[7], + ]) + } + + fn to_u64(self) -> u64 { + self + } } -fn parse_alp_page_layout(data: &[u8]) -> Result { +/// Parse and validate a full ALP page body. +/// +/// The caller provides `Exact` (`u32` or `u64`) based on the physical type. +/// Parsing validates: +/// - header fields and version +/// - offsets section bounds and monotonicity +/// - per-vector metadata and data section sizes +fn parse_alp_page_layout(data: &[u8]) -> Result> { if data.len() < ALP_HEADER_SIZE { return Err(general_err!( "Invalid ALP page: expected at least {} bytes for header, got {}", @@ -59,7 +325,7 @@ fn parse_alp_page_layout(data: &[u8]) -> Result { compression_mode: data[1], integer_encoding: data[2], log_vector_size: data[3], - num_elements: u32::from_le_bytes([data[4], data[5], data[6], data[7]]), + num_elements: i32::from_le_bytes([data[4], data[5], data[6], data[7]]), }; if header.version != ALP_VERSION { @@ -91,13 +357,14 @@ fn parse_alp_page_layout(data: &[u8]) -> Result { ALP_MAX_LOG_VECTOR_SIZE )); } + if header.num_elements < 0 { + return Err(general_err!( + "Invalid ALP page: num_elements {} must be >= 0", + header.num_elements + )); + } - let vector_size = 1usize << header.log_vector_size; - let num_vectors = if header.num_elements == 0 { - 0 - } else { - (header.num_elements as usize).div_ceil(vector_size) - }; + let num_vectors = header.num_vectors(); let offsets_len = num_vectors .checked_mul(std::mem::size_of::()) @@ -116,6 +383,7 @@ fn parse_alp_page_layout(data: &[u8]) -> Result { } let body_len = data.len() - ALP_HEADER_SIZE; + let offsets_section_size = num_vectors * std::mem::size_of::(); let mut offsets = Vec::with_capacity(num_vectors); for i in 0..num_vectors { let start = ALP_HEADER_SIZE + i * 4; @@ -132,15 +400,130 @@ fn parse_alp_page_layout(data: &[u8]) -> Result { body_len )); } + if (offset as usize) < offsets_section_size { + return Err(general_err!( + "Invalid ALP page: vector offset {} points into offsets section {}", + offset, + offsets_section_size + )); + } offsets.push(offset); } - Ok(AlpPageLayout { header, offsets }) + let body = &data[ALP_HEADER_SIZE..]; + let mut vectors = Vec::with_capacity(num_vectors); + for (vector_idx, vector_offset) in offsets.iter().enumerate() { + let vector_start = *vector_offset as usize; + let vector_end = if vector_idx + 1 < offsets.len() { + offsets[vector_idx + 1] as usize + } else { + body_len + }; + + if vector_end < vector_start { + return Err(general_err!( + "Invalid ALP page: vector offsets are not monotonic at index {}", + vector_idx + )); + } + + let vector_num_elements = header.vector_num_elements(vector_idx); + let vector_view = parse_vector_view( + &body[vector_start..vector_end], + vector_num_elements, + )?; + vectors.push(vector_view); + } + + Ok(AlpPageLayout { + header, + offsets, + vectors, + }) +} + +/// Parse one vector: +/// `[AlpEncodedVectorInfo][AlpEncodedForVectorInfo][data-only section]` +/// +/// Data-only section layout: +/// `[packed_values][exception_positions][exception_values]` +fn parse_vector_view( + vector_bytes: &[u8], + num_elements: u16, +) -> Result> { + let metadata_size = AlpEncodedVectorInfo::STORED_SIZE + + AlpEncodedForVectorInfo::::stored_size(); + if vector_bytes.len() < metadata_size { + return Err(general_err!( + "Invalid ALP page: vector metadata too short, expected at least {} bytes, got {}", + metadata_size, + vector_bytes.len() + )); + } + + let alp_info = AlpEncodedVectorInfo { + exponent: vector_bytes[0], + factor: vector_bytes[1], + num_exceptions: u16::from_le_bytes([vector_bytes[2], vector_bytes[3]]), + }; + + let for_start = AlpEncodedVectorInfo::STORED_SIZE; + let for_end = for_start + Exact::WIDTH; + let frame_of_reference = Exact::from_le_slice(&vector_bytes[for_start..for_end]); + let bit_width = vector_bytes[for_end]; + if bit_width as usize > Exact::WIDTH * 8 { + return Err(general_err!( + "Invalid ALP page: bit width {} exceeds {}", + bit_width, + Exact::WIDTH * 8 + )); + } + + let for_info = AlpEncodedForVectorInfo:: { + frame_of_reference, + bit_width, + }; + + let data_size = for_info.get_data_stored_size(num_elements, alp_info.num_exceptions); + if vector_bytes.len() < metadata_size + data_size { + return Err(general_err!( + "Invalid ALP page: vector data too short, expected at least {} bytes, got {}", + metadata_size + data_size, + vector_bytes.len() + )); + } + + let data = &vector_bytes[metadata_size..metadata_size + data_size]; + let packed_size = for_info.get_bit_packed_size(num_elements); + let positions_size = alp_info.num_exceptions as usize * std::mem::size_of::(); + let values_size = alp_info.num_exceptions as usize * Exact::WIDTH; + + let packed_start = 0; + let packed_end = packed_start + packed_size; + let positions_start = packed_end; + let positions_end = positions_start + positions_size; + let values_start = positions_end; + let values_end = values_start + values_size; + + let mut exception_positions = Vec::with_capacity(alp_info.num_exceptions as usize); + for chunk in data[positions_start..positions_end].chunks_exact(2) { + exception_positions.push(u16::from_le_bytes([chunk[0], chunk[1]])); + } + let exception_values = data[values_start..values_end].to_vec(); + + Ok(AlpEncodedVectorView { + num_elements, + alp_info, + for_info, + packed_values: packed_start..packed_end, + exception_positions, + exception_values, + }) } pub(crate) struct AlpDecoder { num_values: usize, - layout: Option, + layout: Option, _marker: PhantomData, } @@ -156,11 +539,25 @@ impl AlpDecoder { impl Decoder for AlpDecoder { fn set_data(&mut self, data: Bytes, num_values: usize) -> Result<()> { - let layout = parse_alp_page_layout(data.as_ref())?; - if layout.header.num_elements as usize != num_values { + // Keep exact-width FOR values per page type, matching C++ layout choices. + let layout = match std::mem::size_of::() { + 4 => AlpPageLayoutAny::F32(parse_alp_page_layout::(data.as_ref())?), + 8 => AlpPageLayoutAny::F64(parse_alp_page_layout::(data.as_ref())?), + type_size => { + return Err(general_err!( + "Invalid ALP page: exact type size {} is unsupported", + type_size + )) + } + }; + let header_num_elements = match &layout { + AlpPageLayoutAny::F32(layout) => layout.header.num_elements, + AlpPageLayoutAny::F64(layout) => layout.header.num_elements, + }; + if header_num_elements as usize != num_values { return Err(general_err!( "Invalid ALP page: header num_elements {} does not match page num_values {}", - layout.header.num_elements, + header_num_elements, num_values )); } @@ -171,15 +568,45 @@ impl Decoder for AlpDecoder { } fn get(&mut self, _buffer: &mut [T::T]) -> Result { - // Parsing succeeds in set_data; value decoding is introduced in a follow-up step. - let num_vectors = self + // Layout parsing succeeds in `set_data`; value decode is implemented next. + let num_vectors = self.layout.as_ref().map(|layout| layout.num_vectors()).unwrap_or(0); + let num_offsets = self.layout.as_ref().map(|layout| layout.num_offsets()).unwrap_or(0); + let parsed_values = self + .layout + .as_ref() + .map(|layout| layout.parsed_values()) + .unwrap_or(0); + let total_exceptions = self + .layout + .as_ref() + .map(|layout| layout.total_exceptions()) + .unwrap_or(0); + let total_packed_bytes = self + .layout + .as_ref() + .map(|layout| layout.total_packed_bytes()) + .unwrap_or(0); + let total_exception_bytes = self + .layout + .as_ref() + .map(|layout| layout.total_exception_bytes()) + .unwrap_or(0); + let sum_for = self.layout.as_ref().map(|layout| layout.sum_for_xor()).unwrap_or(0); + let sum_positions = self .layout .as_ref() - .map(|layout| layout.offsets.len()) + .map(|layout| layout.sum_positions()) .unwrap_or(0); Err(nyi_err!( - "Encoding ALP page layout parsed ({} vectors), value decoding is not implemented", - num_vectors + "Encoding ALP page layout parsed ({} vectors, {} offsets, {} values, {} exceptions, {} packed bytes, {} exception bytes, for-xor {}, pos-sum {}), value decoding is not implemented", + num_vectors, + num_offsets, + parsed_values, + total_exceptions, + total_packed_bytes, + total_exception_bytes, + sum_for, + sum_positions )) } @@ -207,7 +634,7 @@ mod tests { compression_mode: u8, integer_encoding: u8, log_vector_size: u8, - num_elements: u32, + num_elements: i32, offsets: &[u32], body_tail_len: usize, ) -> Vec { @@ -227,8 +654,8 @@ mod tests { #[test] fn test_parse_alp_page_layout_valid() { // num_elements=4 with vector_size=4 -> one vector, one offset entry. - let data = make_alp_page_bytes(1, 0, 0, 2, 4, &[4], 8); - let parsed = parse_alp_page_layout(&data).unwrap(); + let data = make_alp_page_bytes(1, 0, 0, 2, 4, &[4], 13); + let parsed = parse_alp_page_layout::(&data).unwrap(); assert_eq!(parsed.header.version, 1); assert_eq!(parsed.header.num_elements, 4); assert_eq!(parsed.offsets, vec![4]); @@ -236,7 +663,7 @@ mod tests { #[test] fn test_parse_alp_page_layout_short_header() { - let err = parse_alp_page_layout(&[0, 1, 2]).unwrap_err(); + let err = parse_alp_page_layout::(&[0, 1, 2]).unwrap_err(); assert!( err.to_string() .contains("Invalid ALP page: expected at least 8 bytes for header") @@ -246,7 +673,7 @@ mod tests { #[test] fn test_parse_alp_page_layout_invalid_log_vector_size() { let data = make_alp_page_bytes(1, 0, 0, 17, 1, &[4], 8); - let err = parse_alp_page_layout(&data).unwrap_err(); + let err = parse_alp_page_layout::(&data).unwrap_err(); assert!( err.to_string() .contains("Invalid ALP page: log_vector_size 17 exceeds max 16") @@ -256,10 +683,55 @@ mod tests { #[test] fn test_parse_alp_page_layout_invalid_integer_encoding() { let data = make_alp_page_bytes(1, 0, 1, 2, 1, &[4], 8); - let err = parse_alp_page_layout(&data).unwrap_err(); + let err = parse_alp_page_layout::(&data).unwrap_err(); assert!( err.to_string() .contains("Invalid ALP page: unsupported integer encoding 1") ); } + + #[test] + fn test_parse_alp_page_layout_negative_num_elements() { + let data = make_alp_page_bytes(1, 0, 0, 2, -1, &[4], 8); + let err = parse_alp_page_layout::(&data).unwrap_err(); + assert!( + err.to_string() + .contains("Invalid ALP page: num_elements -1 must be >= 0") + ); + } + + #[test] + fn test_parse_alp_page_layout_parses_vector_view_data_only_f64() { + let mut vector = Vec::new(); + + // AlpEncodedVectorInfo: exponent=2, factor=0, num_exceptions=1 + vector.push(2); + vector.push(0); + vector.extend_from_slice(&1u16.to_le_bytes()); + + // AlpEncodedForVectorInfo: frame_of_reference=10, bit_width=0 + vector.extend_from_slice(&10u64.to_le_bytes()); + vector.push(0); + + // Packed values: bit_width=0 and num_elements=1 -> 0 bytes. + // Exception positions (1 * u16) + vector.extend_from_slice(&0u16.to_le_bytes()); + // Exception values (1 * f64 bytes) + vector.extend_from_slice(&42.5_f64.to_le_bytes()); + + let offsets = [4u32]; + let mut page = make_alp_page_bytes(1, 0, 0, 0, 1, &offsets, 0); + page.extend_from_slice(&vector); + + let parsed = parse_alp_page_layout::(&page).unwrap(); + assert_eq!(parsed.vectors.len(), 1); + assert_eq!(parsed.vectors[0].num_elements, 1); + assert_eq!(parsed.vectors[0].alp_info.num_exceptions, 1); + assert_eq!(parsed.vectors[0].for_info.bit_width, 0); + assert_eq!(parsed.vectors[0].exception_positions, vec![0]); + assert_eq!( + parsed.vectors[0].exception_values, + 42.5_f64.to_le_bytes().to_vec() + ); + } } From 148b1d51fd0d295e690bbe939e36becacab2a2bd Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Wed, 18 Feb 2026 17:19:55 -0500 Subject: [PATCH 05/50] Add bit unpacking, reverse FOR and exceptions patching --- parquet/src/encodings/decoding/alp.rs | 361 +++++++++++++++++++------- 1 file changed, 264 insertions(+), 97 deletions(-) diff --git a/parquet/src/encodings/decoding/alp.rs b/parquet/src/encodings/decoding/alp.rs index da9adb0ba3df..e234208a6daf 100644 --- a/parquet/src/encodings/decoding/alp.rs +++ b/parquet/src/encodings/decoding/alp.rs @@ -24,6 +24,7 @@ use crate::basic::Encoding; use crate::data_type::DataType; use crate::encodings::decoding::Decoder; use crate::errors::{ParquetError, Result}; +use crate::util::bit_util::{BitReader, FromBytes}; const ALP_HEADER_SIZE: usize = 8; const ALP_VERSION: u8 = 1; @@ -39,8 +40,6 @@ const ALP_MAX_LOG_VECTOR_SIZE: u8 = 16; /// - `[2]` `integer_encoding` /// - `[3]` `log_vector_size` /// - `[4..8]` `num_elements` (little-endian `i32`) -/// -/// This mirrors the C++ `AlpHeader` in `arrow/util/alp/alp_wrapper.cc`. #[derive(Debug, Clone, Copy)] struct AlpHeader { version: u8, @@ -52,19 +51,13 @@ struct AlpHeader { impl AlpHeader { fn num_elements_usize(&self) -> usize { - // `num_elements` is serialized as i32, matching Parquet page header `num_values`. - // Parsing rejects negative values, so this conversion is safe. self.num_elements as usize } - /// Returns `2 ^ log_vector_size`. fn vector_size(&self) -> usize { 1usize << self.log_vector_size } - /// Number of vectors in the page. - /// - /// All vectors are full-sized except possibly the last one. fn num_vectors(&self) -> usize { if self.num_elements == 0 { 0 @@ -73,7 +66,6 @@ impl AlpHeader { } } - /// Number of logical values in `vector_index`. fn vector_num_elements(&self, vector_index: usize) -> u16 { let vector_size = self.vector_size(); let num_full_vectors = self.num_elements_usize() / vector_size; @@ -89,11 +81,6 @@ impl AlpHeader { } /// Per-vector ALP metadata (4 bytes), equivalent to C++ `AlpEncodedVectorInfo`. -/// -/// Layout in bytes: -/// - `[0]` `exponent` -/// - `[1]` `factor` -/// - `[2..4]` `num_exceptions` (little-endian `u16`) #[derive(Debug, Clone, Copy)] struct AlpEncodedVectorInfo { exponent: u8, @@ -105,8 +92,7 @@ impl AlpEncodedVectorInfo { const STORED_SIZE: usize = 4; } -/// Per-vector FOR metadata for a given exact integer type (`u32` for `f32`, -/// `u64` for `f64`), equivalent to C++ `AlpEncodedForVectorInfo`. +/// Per-vector FOR metadata for exact integer type (`u32` for `f32`, `u64` for `f64`). #[derive(Debug, Clone, Copy)] struct AlpEncodedForVectorInfo { frame_of_reference: Exact, @@ -114,18 +100,14 @@ struct AlpEncodedForVectorInfo { } impl AlpEncodedForVectorInfo { - /// Serialized size of FOR metadata (`Exact::WIDTH + 1` for `bit_width`). fn stored_size() -> usize { Exact::WIDTH + 1 } - /// Number of bytes used for bit-packed encoded integers. fn get_bit_packed_size(&self, num_elements: u16) -> usize { (self.bit_width as usize * num_elements as usize).div_ceil(8) } - /// Data-only size: - /// `[packed_values][exception_positions][exception_values]` fn get_data_stored_size(&self, num_elements: u16, num_exceptions: u16) -> usize { let bit_packed_size = self.get_bit_packed_size(num_elements); bit_packed_size @@ -136,9 +118,12 @@ impl AlpEncodedForVectorInfo { /// Parsed view of one vector's metadata and data slices. /// -/// Note: -/// - `packed_values` is expressed as a range into the vector data section -/// - exceptions are copied into owned vectors for safety and simplicity +/// `packed_values` is a zero-copy range into page body bytes. +/// Exception positions/values are copied for straightforward decode handling. +/// Parsed view of one vector. +/// +/// `packed_values` is a byte range into [`AlpPageLayout::body`] (zero-copy), +/// matching the C++ `LoadViewDataOnly` model for packed bytes. #[derive(Debug)] struct AlpEncodedVectorView { num_elements: u16, @@ -146,21 +131,20 @@ struct AlpEncodedVectorView { for_info: AlpEncodedForVectorInfo, packed_values: Range, exception_positions: Vec, - exception_values: Vec, + exception_values: Vec, } -/// Parsed ALP layout for a page with a concrete exact integer type. -/// -/// `Exact = u32` for `f32` pages and `Exact = u64` for `f64` pages. +/// Parsed ALP page layout for one exact integer width (`u32` for float pages, +/// `u64` for double pages). #[derive(Debug)] struct AlpPageLayout { header: AlpHeader, + body: Bytes, offsets: Vec, vectors: Vec>, } -/// Type-erased page layout used by `AlpDecoder` so we can keep one decoder -/// implementation while preserving exact-width FOR storage. +/// Type-erased wrapper over parsed `f32`/`f64` page layouts. #[derive(Debug)] enum AlpPageLayoutAny { F32(AlpPageLayout), @@ -221,8 +205,16 @@ impl AlpPageLayoutAny { fn total_exception_bytes(&self) -> usize { match self { - Self::F32(layout) => layout.vectors.iter().map(|v| v.exception_values.len()).sum(), - Self::F64(layout) => layout.vectors.iter().map(|v| v.exception_values.len()).sum(), + Self::F32(layout) => layout + .vectors + .iter() + .map(|v| v.exception_values.len() * std::mem::size_of::()) + .sum(), + Self::F64(layout) => layout + .vectors + .iter() + .map(|v| v.exception_values.len() * std::mem::size_of::()) + .sum(), } } @@ -264,18 +256,15 @@ impl AlpPageLayoutAny { /// - `double` -> `uint64_t` /// /// Why unsigned (not `i32`/`i64`)? -/// - FOR computes and stores deltas (`value - frame_of_reference`) that are -/// encoded with bitpacking and are naturally treated as non-negative bit -/// patterns. -/// - Using unsigned exact-width integers avoids signed-overflow edge cases in -/// the FOR stage and matches the C++ implementation's arithmetic model. -/// - Signed interpretation is still applied later during ALP decimal -/// reconstruction (after inverse FOR), so we preserve behavior while keeping -/// FOR metadata compact and byte-compatible. +/// - FOR stores non-negative deltas optimized for bitpacking. +/// - Unsigned arithmetic avoids signed-overflow edge cases in FOR stage. +/// - Signed interpretation is applied later during decimal reconstruction. trait AlpExact: Copy + std::fmt::Debug { const WIDTH: usize; fn from_le_slice(slice: &[u8]) -> Self; fn to_u64(self) -> u64; + fn zero() -> Self; + fn wrapping_add(self, rhs: Self) -> Self; } impl AlpExact for u32 { @@ -288,6 +277,14 @@ impl AlpExact for u32 { fn to_u64(self) -> u64 { self as u64 } + + fn zero() -> Self { + 0 + } + + fn wrapping_add(self, rhs: Self) -> Self { + self.wrapping_add(rhs) + } } impl AlpExact for u64 { @@ -302,30 +299,39 @@ impl AlpExact for u64 { fn to_u64(self) -> u64 { self } + + fn zero() -> Self { + 0 + } + + fn wrapping_add(self, rhs: Self) -> Self { + self.wrapping_add(rhs) + } } -/// Parse and validate a full ALP page body. +/// Parse and validate a full ALP-encoded page body. /// -/// The caller provides `Exact` (`u32` or `u64`) based on the physical type. -/// Parsing validates: -/// - header fields and version -/// - offsets section bounds and monotonicity -/// - per-vector metadata and data section sizes -fn parse_alp_page_layout(data: &[u8]) -> Result> { - if data.len() < ALP_HEADER_SIZE { +/// Validation includes: +/// - header fields/version/encoding +/// - non-negative `num_elements` +/// - offsets bounds + monotonicity +/// - per-vector metadata/data section lengths +fn parse_alp_page_layout(data: Bytes) -> Result> { + let data_ref = data.as_ref(); + if data_ref.len() < ALP_HEADER_SIZE { return Err(general_err!( "Invalid ALP page: expected at least {} bytes for header, got {}", ALP_HEADER_SIZE, - data.len() + data_ref.len() )); } let header = AlpHeader { - version: data[0], - compression_mode: data[1], - integer_encoding: data[2], - log_vector_size: data[3], - num_elements: i32::from_le_bytes([data[4], data[5], data[6], data[7]]), + version: data_ref[0], + compression_mode: data_ref[1], + integer_encoding: data_ref[2], + log_vector_size: data_ref[3], + num_elements: i32::from_le_bytes([data_ref[4], data_ref[5], data_ref[6], data_ref[7]]), }; if header.version != ALP_VERSION { @@ -357,6 +363,7 @@ fn parse_alp_page_layout(data: &[u8]) -> Result= 0", @@ -373,26 +380,30 @@ fn parse_alp_page_layout(data: &[u8]) -> Result(); + let mut offsets = Vec::with_capacity(num_vectors); for i in 0..num_vectors { let start = ALP_HEADER_SIZE + i * 4; let offset = u32::from_le_bytes([ - data[start], - data[start + 1], - data[start + 2], - data[start + 3], + data_ref[start], + data_ref[start + 1], + data_ref[start + 2], + data_ref[start + 3], ]); + if offset as usize >= body_len { return Err(general_err!( "Invalid ALP page: vector offset {} out of bounds for body length {}", @@ -400,6 +411,7 @@ fn parse_alp_page_layout(data: &[u8]) -> Result(data: &[u8]) -> Result(data: &[u8]) -> Result( + body_ref, + vector_start, + vector_end, vector_num_elements, - )?; - vectors.push(vector_view); + )?); } Ok(AlpPageLayout { header, + body, offsets, vectors, }) } -/// Parse one vector: -/// `[AlpEncodedVectorInfo][AlpEncodedForVectorInfo][data-only section]` -/// -/// Data-only section layout: -/// `[packed_values][exception_positions][exception_values]` +/// Parse a single vector section: +/// `[AlpInfo][ForInfo][PackedValues][ExceptionPositions][ExceptionValues]`. fn parse_vector_view( - vector_bytes: &[u8], + body: &[u8], + vector_start: usize, + vector_end: usize, num_elements: u16, ) -> Result> { - let metadata_size = AlpEncodedVectorInfo::STORED_SIZE - + AlpEncodedForVectorInfo::::stored_size(); + let vector_bytes = &body[vector_start..vector_end]; + + let metadata_size = + AlpEncodedVectorInfo::STORED_SIZE + AlpEncodedForVectorInfo::::stored_size(); if vector_bytes.len() < metadata_size { return Err(general_err!( "Invalid ALP page: vector metadata too short, expected at least {} bytes, got {}", @@ -471,6 +486,7 @@ fn parse_vector_view( let for_end = for_start + Exact::WIDTH; let frame_of_reference = Exact::from_le_slice(&vector_bytes[for_start..for_end]); let bit_width = vector_bytes[for_end]; + if bit_width as usize > Exact::WIDTH * 8 { return Err(general_err!( "Invalid ALP page: bit width {} exceeds {}", @@ -509,18 +525,122 @@ fn parse_vector_view( for chunk in data[positions_start..positions_end].chunks_exact(2) { exception_positions.push(u16::from_le_bytes([chunk[0], chunk[1]])); } - let exception_values = data[values_start..values_end].to_vec(); + + let packed_values = + (vector_start + metadata_size + packed_start)..(vector_start + metadata_size + packed_end); + + let mut exception_values = Vec::with_capacity(alp_info.num_exceptions as usize); + for chunk in data[values_start..values_end].chunks_exact(Exact::WIDTH) { + exception_values.push(Exact::from_le_slice(chunk)); + } Ok(AlpEncodedVectorView { num_elements, alp_info, for_info, - packed_values: packed_start..packed_end, + packed_values, exception_positions, exception_values, }) } +/// Decode bit-packed deltas into exact integers. +fn bit_unpack_integers( + packed_values: &[u8], + bit_width: u8, + num_elements: u16, +) -> Result> { + if bit_width as usize > Exact::WIDTH * 8 { + return Err(general_err!( + "Invalid ALP page: bit width {} exceeds {}", + bit_width, + Exact::WIDTH * 8 + )); + } + + if bit_width == 0 { + return Ok(vec![Exact::zero(); num_elements as usize]); + } + + let mut out = vec![Exact::zero(); num_elements as usize]; + let mut reader = BitReader::new(Bytes::copy_from_slice(packed_values)); + let read = reader.get_batch::(&mut out, bit_width as usize); + if read != out.len() { + return Err(general_err!( + "Invalid ALP page: bit unpack read {} values, expected {}", + read, + out.len() + )); + } + + Ok(out) +} + +/// Apply inverse FOR: `decoded = delta + frame_of_reference`. +fn inverse_for(deltas: &mut [Exact], frame_of_reference: Exact) { + for value in deltas { + *value = value.wrapping_add(frame_of_reference); + } +} + +/// Patch exception values at their original positions. +fn patch_exceptions( + decoded: &mut [Exact], + exception_positions: &[u16], + exception_values: &[Exact], +) -> Result<()> { + if exception_positions.len() != exception_values.len() { + return Err(general_err!( + "Invalid ALP page: exception positions ({}) and values ({}) length mismatch", + exception_positions.len(), + exception_values.len() + )); + } + + for (pos, value) in exception_positions.iter().zip(exception_values.iter()) { + let pos = *pos as usize; + if pos >= decoded.len() { + return Err(general_err!( + "Invalid ALP page: exception position {} out of bounds for vector length {}", + pos, + decoded.len() + )); + } + decoded[pos] = *value; + } + + Ok(()) +} + +/// Decode one vector to exact integers: +/// bit-unpack -> inverse FOR -> exception patch. +fn decode_vector_exact( + body: &[u8], + vector: &AlpEncodedVectorView, +) -> Result> { + let mut decoded = bit_unpack_integers( + &body[vector.packed_values.clone()], + vector.for_info.bit_width, + vector.num_elements, + )?; + inverse_for(&mut decoded, vector.for_info.frame_of_reference); + patch_exceptions( + &mut decoded, + &vector.exception_positions, + &vector.exception_values, + )?; + Ok(decoded) +} + +/// Decode all vectors in a page into exact integers (still pre-decimal reconstruction). +fn decode_page_exact(layout: &AlpPageLayout) -> Result> { + let mut out = Vec::with_capacity(layout.header.num_elements_usize()); + for vector in &layout.vectors { + out.extend_from_slice(&decode_vector_exact(layout.body.as_ref(), vector)?); + } + Ok(out) +} + pub(crate) struct AlpDecoder { num_values: usize, layout: Option, @@ -539,10 +659,9 @@ impl AlpDecoder { impl Decoder for AlpDecoder { fn set_data(&mut self, data: Bytes, num_values: usize) -> Result<()> { - // Keep exact-width FOR values per page type, matching C++ layout choices. let layout = match std::mem::size_of::() { - 4 => AlpPageLayoutAny::F32(parse_alp_page_layout::(data.as_ref())?), - 8 => AlpPageLayoutAny::F64(parse_alp_page_layout::(data.as_ref())?), + 4 => AlpPageLayoutAny::F32(parse_alp_page_layout::(data)?), + 8 => AlpPageLayoutAny::F64(parse_alp_page_layout::(data)?), type_size => { return Err(general_err!( "Invalid ALP page: exact type size {} is unsupported", @@ -550,10 +669,12 @@ impl Decoder for AlpDecoder { )) } }; + let header_num_elements = match &layout { AlpPageLayoutAny::F32(layout) => layout.header.num_elements, AlpPageLayoutAny::F64(layout) => layout.header.num_elements, }; + if header_num_elements as usize != num_values { return Err(general_err!( "Invalid ALP page: header num_elements {} does not match page num_values {}", @@ -568,7 +689,6 @@ impl Decoder for AlpDecoder { } fn get(&mut self, _buffer: &mut [T::T]) -> Result { - // Layout parsing succeeds in `set_data`; value decode is implemented next. let num_vectors = self.layout.as_ref().map(|layout| layout.num_vectors()).unwrap_or(0); let num_offsets = self.layout.as_ref().map(|layout| layout.num_offsets()).unwrap_or(0); let parsed_values = self @@ -597,8 +717,19 @@ impl Decoder for AlpDecoder { .as_ref() .map(|layout| layout.sum_positions()) .unwrap_or(0); + + let decoded_checksum = match self.layout.as_ref() { + Some(AlpPageLayoutAny::F32(layout)) => decode_page_exact(layout)? + .into_iter() + .fold(0u64, |acc, v| acc ^ v.to_u64()), + Some(AlpPageLayoutAny::F64(layout)) => decode_page_exact(layout)? + .into_iter() + .fold(0u64, |acc, v| acc ^ v.to_u64()), + None => 0, + }; + Err(nyi_err!( - "Encoding ALP page layout parsed ({} vectors, {} offsets, {} values, {} exceptions, {} packed bytes, {} exception bytes, for-xor {}, pos-sum {}), value decoding is not implemented", + "Encoding ALP page layout parsed ({} vectors, {} offsets, {} values, {} exceptions, {} packed bytes, {} exception bytes, for-xor {}, pos-sum {}, decoded-xor {}), value decoding is not implemented", num_vectors, num_offsets, parsed_values, @@ -606,7 +737,8 @@ impl Decoder for AlpDecoder { total_packed_bytes, total_exception_bytes, sum_for, - sum_positions + sum_positions, + decoded_checksum )) } @@ -653,9 +785,8 @@ mod tests { #[test] fn test_parse_alp_page_layout_valid() { - // num_elements=4 with vector_size=4 -> one vector, one offset entry. let data = make_alp_page_bytes(1, 0, 0, 2, 4, &[4], 13); - let parsed = parse_alp_page_layout::(&data).unwrap(); + let parsed = parse_alp_page_layout::(Bytes::from(data)).unwrap(); assert_eq!(parsed.header.version, 1); assert_eq!(parsed.header.num_elements, 4); assert_eq!(parsed.offsets, vec![4]); @@ -663,7 +794,7 @@ mod tests { #[test] fn test_parse_alp_page_layout_short_header() { - let err = parse_alp_page_layout::(&[0, 1, 2]).unwrap_err(); + let err = parse_alp_page_layout::(Bytes::from_static(&[0, 1, 2])).unwrap_err(); assert!( err.to_string() .contains("Invalid ALP page: expected at least 8 bytes for header") @@ -673,7 +804,7 @@ mod tests { #[test] fn test_parse_alp_page_layout_invalid_log_vector_size() { let data = make_alp_page_bytes(1, 0, 0, 17, 1, &[4], 8); - let err = parse_alp_page_layout::(&data).unwrap_err(); + let err = parse_alp_page_layout::(Bytes::from(data)).unwrap_err(); assert!( err.to_string() .contains("Invalid ALP page: log_vector_size 17 exceeds max 16") @@ -683,7 +814,7 @@ mod tests { #[test] fn test_parse_alp_page_layout_invalid_integer_encoding() { let data = make_alp_page_bytes(1, 0, 1, 2, 1, &[4], 8); - let err = parse_alp_page_layout::(&data).unwrap_err(); + let err = parse_alp_page_layout::(Bytes::from(data)).unwrap_err(); assert!( err.to_string() .contains("Invalid ALP page: unsupported integer encoding 1") @@ -693,7 +824,7 @@ mod tests { #[test] fn test_parse_alp_page_layout_negative_num_elements() { let data = make_alp_page_bytes(1, 0, 0, 2, -1, &[4], 8); - let err = parse_alp_page_layout::(&data).unwrap_err(); + let err = parse_alp_page_layout::(Bytes::from(data)).unwrap_err(); assert!( err.to_string() .contains("Invalid ALP page: num_elements -1 must be >= 0") @@ -704,34 +835,70 @@ mod tests { fn test_parse_alp_page_layout_parses_vector_view_data_only_f64() { let mut vector = Vec::new(); - // AlpEncodedVectorInfo: exponent=2, factor=0, num_exceptions=1 vector.push(2); vector.push(0); vector.extend_from_slice(&1u16.to_le_bytes()); - // AlpEncodedForVectorInfo: frame_of_reference=10, bit_width=0 vector.extend_from_slice(&10u64.to_le_bytes()); vector.push(0); - // Packed values: bit_width=0 and num_elements=1 -> 0 bytes. - // Exception positions (1 * u16) vector.extend_from_slice(&0u16.to_le_bytes()); - // Exception values (1 * f64 bytes) vector.extend_from_slice(&42.5_f64.to_le_bytes()); let offsets = [4u32]; let mut page = make_alp_page_bytes(1, 0, 0, 0, 1, &offsets, 0); page.extend_from_slice(&vector); - let parsed = parse_alp_page_layout::(&page).unwrap(); + let parsed = parse_alp_page_layout::(Bytes::from(page)).unwrap(); assert_eq!(parsed.vectors.len(), 1); assert_eq!(parsed.vectors[0].num_elements, 1); assert_eq!(parsed.vectors[0].alp_info.num_exceptions, 1); assert_eq!(parsed.vectors[0].for_info.bit_width, 0); assert_eq!(parsed.vectors[0].exception_positions, vec![0]); - assert_eq!( - parsed.vectors[0].exception_values, - 42.5_f64.to_le_bytes().to_vec() - ); + assert_eq!(parsed.vectors[0].exception_values, vec![42.5_f64.to_bits()]); + } + + #[test] + fn test_bit_unpack_integers_width_zero() { + let unpacked = bit_unpack_integers::(&[], 0, 3).unwrap(); + assert_eq!(unpacked, vec![0, 0, 0]); + } + + #[test] + fn test_bit_unpack_integers_width_two() { + let unpacked = bit_unpack_integers::(&[0b0010_0111], 2, 3).unwrap(); + assert_eq!(unpacked, vec![3, 1, 2]); + } + + #[test] + fn test_inverse_for_and_patch_exceptions() { + let mut decoded = vec![0u32, 3, 2]; + inverse_for(&mut decoded, 10); + assert_eq!(decoded, vec![10, 13, 12]); + + patch_exceptions(&mut decoded, &[1], &[99]).unwrap(); + assert_eq!(decoded, vec![10, 99, 12]); + } + + #[test] + fn test_decode_vector_exact() { + let vector = AlpEncodedVectorView:: { + num_elements: 3, + alp_info: AlpEncodedVectorInfo { + exponent: 0, + factor: 0, + num_exceptions: 1, + }, + for_info: AlpEncodedForVectorInfo { + frame_of_reference: 10, + bit_width: 2, + }, + packed_values: 0..1, + exception_positions: vec![2], + exception_values: vec![77], + }; + + let decoded = decode_vector_exact(&[0b0010_1100], &vector).unwrap(); + assert_eq!(decoded, vec![10, 13, 77]); } } From 5f0222eac7ac9caffd42643d8b82872a1ab41245 Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Wed, 18 Feb 2026 18:06:46 -0500 Subject: [PATCH 06/50] Decode page works --- parquet/src/encodings/decoding/alp.rs | 407 +++++++++++++------------- parquet/tests/arrow_reader/alp.rs | 25 +- 2 files changed, 226 insertions(+), 206 deletions(-) diff --git a/parquet/src/encodings/decoding/alp.rs b/parquet/src/encodings/decoding/alp.rs index e234208a6daf..8238da8ae929 100644 --- a/parquet/src/encodings/decoding/alp.rs +++ b/parquet/src/encodings/decoding/alp.rs @@ -121,9 +121,6 @@ impl AlpEncodedForVectorInfo { /// `packed_values` is a zero-copy range into page body bytes. /// Exception positions/values are copied for straightforward decode handling. /// Parsed view of one vector. -/// -/// `packed_values` is a byte range into [`AlpPageLayout::body`] (zero-copy), -/// matching the C++ `LoadViewDataOnly` model for packed bytes. #[derive(Debug)] struct AlpEncodedVectorView { num_elements: u16, @@ -140,115 +137,11 @@ struct AlpEncodedVectorView { struct AlpPageLayout { header: AlpHeader, body: Bytes, + #[allow(dead_code)] offsets: Vec, vectors: Vec>, } -/// Type-erased wrapper over parsed `f32`/`f64` page layouts. -#[derive(Debug)] -enum AlpPageLayoutAny { - F32(AlpPageLayout), - F64(AlpPageLayout), -} - -impl AlpPageLayoutAny { - fn num_vectors(&self) -> usize { - match self { - Self::F32(layout) => layout.vectors.len(), - Self::F64(layout) => layout.vectors.len(), - } - } - - fn num_offsets(&self) -> usize { - match self { - Self::F32(layout) => layout.offsets.len(), - Self::F64(layout) => layout.offsets.len(), - } - } - - fn parsed_values(&self) -> usize { - match self { - Self::F32(layout) => layout.vectors.iter().map(|v| v.num_elements as usize).sum(), - Self::F64(layout) => layout.vectors.iter().map(|v| v.num_elements as usize).sum(), - } - } - - fn total_exceptions(&self) -> usize { - match self { - Self::F32(layout) => layout - .vectors - .iter() - .map(|v| v.alp_info.num_exceptions as usize) - .sum(), - Self::F64(layout) => layout - .vectors - .iter() - .map(|v| v.alp_info.num_exceptions as usize) - .sum(), - } - } - - fn total_packed_bytes(&self) -> usize { - match self { - Self::F32(layout) => layout - .vectors - .iter() - .map(|v| v.packed_values.end - v.packed_values.start) - .sum(), - Self::F64(layout) => layout - .vectors - .iter() - .map(|v| v.packed_values.end - v.packed_values.start) - .sum(), - } - } - - fn total_exception_bytes(&self) -> usize { - match self { - Self::F32(layout) => layout - .vectors - .iter() - .map(|v| v.exception_values.len() * std::mem::size_of::()) - .sum(), - Self::F64(layout) => layout - .vectors - .iter() - .map(|v| v.exception_values.len() * std::mem::size_of::()) - .sum(), - } - } - - fn sum_for_xor(&self) -> u64 { - match self { - Self::F32(layout) => layout - .vectors - .iter() - .fold(0u64, |acc, v| acc ^ v.for_info.frame_of_reference.to_u64()), - Self::F64(layout) => layout - .vectors - .iter() - .fold(0u64, |acc, v| acc ^ v.for_info.frame_of_reference.to_u64()), - } - } - - fn sum_positions(&self) -> usize { - match self { - Self::F32(layout) => layout - .vectors - .iter() - .flat_map(|v| v.exception_positions.iter()) - .map(|v| *v as usize) - .sum(), - Self::F64(layout) => layout - .vectors - .iter() - .flat_map(|v| v.exception_positions.iter()) - .map(|v| *v as usize) - .sum(), - } - } -} - /// Exact integer type used by FOR reconstruction. /// /// This mirrors C++: @@ -261,23 +154,21 @@ impl AlpPageLayoutAny { /// - Signed interpretation is applied later during decimal reconstruction. trait AlpExact: Copy + std::fmt::Debug { const WIDTH: usize; + type Signed: Copy; fn from_le_slice(slice: &[u8]) -> Self; - fn to_u64(self) -> u64; fn zero() -> Self; fn wrapping_add(self, rhs: Self) -> Self; + fn reinterpret_as_signed(self) -> Self::Signed; } impl AlpExact for u32 { const WIDTH: usize = 4; + type Signed = i32; fn from_le_slice(slice: &[u8]) -> Self { u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]]) } - fn to_u64(self) -> u64 { - self as u64 - } - fn zero() -> Self { 0 } @@ -285,10 +176,15 @@ impl AlpExact for u32 { fn wrapping_add(self, rhs: Self) -> Self { self.wrapping_add(rhs) } + + fn reinterpret_as_signed(self) -> Self::Signed { + i32::from_ne_bytes(self.to_ne_bytes()) + } } impl AlpExact for u64 { const WIDTH: usize = 8; + type Signed = i64; fn from_le_slice(slice: &[u8]) -> Self { u64::from_le_bytes([ @@ -296,10 +192,6 @@ impl AlpExact for u64 { ]) } - fn to_u64(self) -> u64 { - self - } - fn zero() -> Self { 0 } @@ -307,6 +199,124 @@ impl AlpExact for u64 { fn wrapping_add(self, rhs: Self) -> Self { self.wrapping_add(rhs) } + + fn reinterpret_as_signed(self) -> Self::Signed { + i64::from_ne_bytes(self.to_ne_bytes()) + } +} + +const ALP_I64_POW10: [i64; 19] = [ + 1, + 10, + 100, + 1_000, + 10_000, + 100_000, + 1_000_000, + 10_000_000, + 100_000_000, + 1_000_000_000, + 10_000_000_000, + 100_000_000_000, + 1_000_000_000_000, + 10_000_000_000_000, + 100_000_000_000_000, + 1_000_000_000_000_000, + 10_000_000_000_000_000, + 100_000_000_000_000_000, + 1_000_000_000_000_000_000, +]; + +const ALP_NEG_POW10_F32: [f32; 11] = [ + 1.0, + 0.1, + 0.01, + 0.001, + 0.0001, + 0.00001, + 0.000001, + 0.0000001, + 0.00000001, + 0.000000001, + 0.0000000001, +]; + +const ALP_NEG_POW10_F64: [f64; 19] = [ + 1.0, + 0.1, + 0.01, + 0.001, + 0.0001, + 0.00001, + 0.000001, + 0.0000001, + 0.00000001, + 0.000000001, + 0.0000000001, + 0.00000000001, + 0.000000000001, + 0.0000000000001, + 0.00000000000001, + 0.000000000000001, + 0.0000000000000001, + 0.00000000000000001, + 0.000000000000000001, +]; + +trait AlpFloat: Copy + Default { + type Exact: AlpExact + FromBytes; + + fn decode_value( + signed_encoded: ::Signed, + exponent: u8, + factor: u8, + ) -> Result; + + fn from_exact_bits(bits: Self::Exact) -> Self; +} + +impl AlpFloat for f32 { + type Exact = u32; + + fn decode_value(signed_encoded: i32, exponent: u8, factor: u8) -> Result { + let factor_multiplier = ALP_I64_POW10 + .get(factor as usize) + .ok_or_else(|| general_err!("Invalid ALP page: factor {} exceeds max 18", factor))?; + let exponent_multiplier = ALP_NEG_POW10_F32.get(exponent as usize).ok_or_else(|| { + general_err!( + "Invalid ALP page: exponent {} exceeds max {} for f32", + exponent, + ALP_NEG_POW10_F32.len() - 1 + ) + })?; + Ok((signed_encoded as f32) * (*factor_multiplier as f32) * *exponent_multiplier) + } + + fn from_exact_bits(bits: Self::Exact) -> Self { + f32::from_bits(bits) + } +} + +impl AlpFloat for f64 { + type Exact = u64; + + fn decode_value(signed_encoded: i64, exponent: u8, factor: u8) -> Result { + let factor_multiplier = ALP_I64_POW10 + .get(factor as usize) + .ok_or_else(|| general_err!("Invalid ALP page: factor {} exceeds max 18", factor))?; + let exponent_multiplier = ALP_NEG_POW10_F64.get(exponent as usize).ok_or_else(|| { + general_err!( + "Invalid ALP page: exponent {} exceeds max {} for f64", + exponent, + ALP_NEG_POW10_F64.len() - 1 + ) + })?; + Ok((signed_encoded as f64) * (*factor_multiplier as f64) * *exponent_multiplier) + } + + fn from_exact_bits(bits: Self::Exact) -> Self { + f64::from_bits(bits) + } } /// Parse and validate a full ALP-encoded page body. @@ -584,6 +594,7 @@ fn inverse_for(deltas: &mut [Exact], frame_of_reference: Exact) } /// Patch exception values at their original positions. +#[cfg(test)] fn patch_exceptions( decoded: &mut [Exact], exception_positions: &[u16], @@ -614,6 +625,7 @@ fn patch_exceptions( /// Decode one vector to exact integers: /// bit-unpack -> inverse FOR -> exception patch. +#[cfg(test)] fn decode_vector_exact( body: &[u8], vector: &AlpEncodedVectorView, @@ -632,118 +644,115 @@ fn decode_vector_exact( Ok(decoded) } -/// Decode all vectors in a page into exact integers (still pre-decimal reconstruction). -fn decode_page_exact(layout: &AlpPageLayout) -> Result> { +/// Decode one vector into output floating values: +/// bit-unpack -> inverse FOR -> decimal decode -> patch exceptions. +fn decode_vector_values( + body: &[u8], + vector: &AlpEncodedVectorView, +) -> Result> { + let mut exact_values = bit_unpack_integers( + &body[vector.packed_values.clone()], + vector.for_info.bit_width, + vector.num_elements, + )?; + inverse_for(&mut exact_values, vector.for_info.frame_of_reference); + + let mut out = Vec::with_capacity(vector.num_elements as usize); + for exact_value in exact_values { + let signed_value = exact_value.reinterpret_as_signed(); + out.push(Value::decode_value( + signed_value, + vector.alp_info.exponent, + vector.alp_info.factor, + )?); + } + + if vector.exception_positions.len() != vector.exception_values.len() { + return Err(general_err!( + "Invalid ALP page: exception positions ({}) and values ({}) length mismatch", + vector.exception_positions.len(), + vector.exception_values.len() + )); + } + + for (pos, value_bits) in vector + .exception_positions + .iter() + .zip(vector.exception_values.iter()) + { + let pos = *pos as usize; + if pos >= out.len() { + return Err(general_err!( + "Invalid ALP page: exception position {} out of bounds for vector length {}", + pos, + out.len() + )); + } + out[pos] = Value::from_exact_bits(*value_bits); + } + + Ok(out) +} + +fn decode_page_values(layout: &AlpPageLayout) -> Result> { let mut out = Vec::with_capacity(layout.header.num_elements_usize()); for vector in &layout.vectors { - out.extend_from_slice(&decode_vector_exact(layout.body.as_ref(), vector)?); + out.extend_from_slice(&decode_vector_values::( + layout.body.as_ref(), + vector, + )?); } Ok(out) } pub(crate) struct AlpDecoder { - num_values: usize, - layout: Option, + decoded_values: Vec, + values_decoded: usize, _marker: PhantomData, } impl AlpDecoder { pub(crate) fn new() -> Self { Self { - num_values: 0, - layout: None, + decoded_values: Vec::new(), + values_decoded: 0, _marker: PhantomData, } } } -impl Decoder for AlpDecoder { +impl Decoder for AlpDecoder +where + T::T: AlpFloat, +{ fn set_data(&mut self, data: Bytes, num_values: usize) -> Result<()> { - let layout = match std::mem::size_of::() { - 4 => AlpPageLayoutAny::F32(parse_alp_page_layout::(data)?), - 8 => AlpPageLayoutAny::F64(parse_alp_page_layout::(data)?), - type_size => { - return Err(general_err!( - "Invalid ALP page: exact type size {} is unsupported", - type_size - )) - } - }; + let layout = parse_alp_page_layout::<::Exact>(data)?; - let header_num_elements = match &layout { - AlpPageLayoutAny::F32(layout) => layout.header.num_elements, - AlpPageLayoutAny::F64(layout) => layout.header.num_elements, - }; - - if header_num_elements as usize != num_values { + if layout.header.num_elements_usize() != num_values { return Err(general_err!( "Invalid ALP page: header num_elements {} does not match page num_values {}", - header_num_elements, + layout.header.num_elements, num_values )); } - self.num_values = num_values; - self.layout = Some(layout); + self.decoded_values = decode_page_values::(&layout)?; + self.values_decoded = 0; Ok(()) } - fn get(&mut self, _buffer: &mut [T::T]) -> Result { - let num_vectors = self.layout.as_ref().map(|layout| layout.num_vectors()).unwrap_or(0); - let num_offsets = self.layout.as_ref().map(|layout| layout.num_offsets()).unwrap_or(0); - let parsed_values = self - .layout - .as_ref() - .map(|layout| layout.parsed_values()) - .unwrap_or(0); - let total_exceptions = self - .layout - .as_ref() - .map(|layout| layout.total_exceptions()) - .unwrap_or(0); - let total_packed_bytes = self - .layout - .as_ref() - .map(|layout| layout.total_packed_bytes()) - .unwrap_or(0); - let total_exception_bytes = self - .layout - .as_ref() - .map(|layout| layout.total_exception_bytes()) - .unwrap_or(0); - let sum_for = self.layout.as_ref().map(|layout| layout.sum_for_xor()).unwrap_or(0); - let sum_positions = self - .layout - .as_ref() - .map(|layout| layout.sum_positions()) - .unwrap_or(0); - - let decoded_checksum = match self.layout.as_ref() { - Some(AlpPageLayoutAny::F32(layout)) => decode_page_exact(layout)? - .into_iter() - .fold(0u64, |acc, v| acc ^ v.to_u64()), - Some(AlpPageLayoutAny::F64(layout)) => decode_page_exact(layout)? - .into_iter() - .fold(0u64, |acc, v| acc ^ v.to_u64()), - None => 0, - }; - - Err(nyi_err!( - "Encoding ALP page layout parsed ({} vectors, {} offsets, {} values, {} exceptions, {} packed bytes, {} exception bytes, for-xor {}, pos-sum {}, decoded-xor {}), value decoding is not implemented", - num_vectors, - num_offsets, - parsed_values, - total_exceptions, - total_packed_bytes, - total_exception_bytes, - sum_for, - sum_positions, - decoded_checksum - )) + fn get(&mut self, buffer: &mut [T::T]) -> Result { + let num_values = buffer.len().min(self.values_left()); + let end = self.values_decoded + num_values; + buffer[..num_values].copy_from_slice(&self.decoded_values[self.values_decoded..end]); + self.values_decoded = end; + Ok(num_values) } fn values_left(&self) -> usize { - self.num_values + self.decoded_values + .len() + .saturating_sub(self.values_decoded) } fn encoding(&self) -> Encoding { @@ -751,8 +760,8 @@ impl Decoder for AlpDecoder { } fn skip(&mut self, num_values: usize) -> Result { - let skipped = num_values.min(self.num_values); - self.num_values -= skipped; + let skipped = num_values.min(self.values_left()); + self.values_decoded += skipped; Ok(skipped) } } diff --git a/parquet/tests/arrow_reader/alp.rs b/parquet/tests/arrow_reader/alp.rs index e92a1ef7797f..a74f32693539 100644 --- a/parquet/tests/arrow_reader/alp.rs +++ b/parquet/tests/arrow_reader/alp.rs @@ -16,10 +16,12 @@ // under the License. use arrow::util::test_util::parquet_test_data; +use arrow_array::cast::as_primitive_array; +use arrow_array::types::Float64Type; use parquet::arrow::arrow_reader::ArrowReaderBuilder; #[test] -fn test_read_single_f64_alp_page_layout() { +fn test_read_single_f64_alp() { let path = std::path::PathBuf::from(parquet_test_data()).join("single_f64_ALP.parquet"); if !path.exists() { eprintln!("Skipping ALP test file not found: {}", path.display()); @@ -29,13 +31,22 @@ fn test_read_single_f64_alp_page_layout() { let file = std::fs::File::open(path).unwrap(); let mut reader = ArrowReaderBuilder::try_new(file).unwrap().build().unwrap(); - let err = loop { + let mut total_rows = 0usize; + let mut first_value = None; + loop { match reader.next() { - Some(Ok(_)) => continue, - Some(Err(err)) => break err, - None => panic!("Expected ALP decode to fail with NYI"), + Some(Ok(batch)) => { + total_rows += batch.num_rows(); + if first_value.is_none() && batch.num_rows() > 0 { + let values = as_primitive_array::(batch.column(0).as_ref()); + first_value = Some(values.value(0)); + } + } + Some(Err(err)) => panic!("Unexpected ALP decode error: {err}"), + None => break, } - }; + } - assert!(err.to_string().contains("Encoding ALP page layout parsed")); + assert_eq!(total_rows, 1024); + assert_eq!(first_value, Some(0.125)); } From 027172fc8961a31e9731d6bb2c1a419509e83bc9 Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Wed, 18 Feb 2026 21:17:13 -0500 Subject: [PATCH 07/50] Add more checks and tests --- parquet/src/encodings/decoding/alp.rs | 317 +++++++++++++++++++++++--- 1 file changed, 281 insertions(+), 36 deletions(-) diff --git a/parquet/src/encodings/decoding/alp.rs b/parquet/src/encodings/decoding/alp.rs index 8238da8ae929..66d71245251f 100644 --- a/parquet/src/encodings/decoding/alp.rs +++ b/parquet/src/encodings/decoding/alp.rs @@ -31,6 +31,8 @@ const ALP_VERSION: u8 = 1; const ALP_COMPRESSION_MODE: u8 = 0; const ALP_INTEGER_ENCODING_FOR_BIT_PACK: u8 = 0; const ALP_MAX_LOG_VECTOR_SIZE: u8 = 16; +const ALP_MAX_EXPONENT_F32: u8 = 10; +const ALP_MAX_EXPONENT_F64: u8 = 18; /// Page-level ALP header (version 1, 8 bytes). /// @@ -120,7 +122,6 @@ impl AlpEncodedForVectorInfo { /// /// `packed_values` is a zero-copy range into page body bytes. /// Exception positions/values are copied for straightforward decode handling. -/// Parsed view of one vector. #[derive(Debug)] struct AlpEncodedVectorView { num_elements: u16, @@ -266,11 +267,14 @@ const ALP_NEG_POW10_F64: [f64; 19] = [ trait AlpFloat: Copy + Default { type Exact: AlpExact + FromBytes; - fn decode_value( - signed_encoded: ::Signed, - exponent: u8, - factor: u8, - ) -> Result; + /// Precompute vector-level ALP decimal scale for: + /// `value = encoded * 10^(factor) * 10^(-exponent)`. + /// + /// Preconditions are validated during page parse. + fn decode_scale(exponent: u8, factor: u8) -> Self; + + /// Decode one signed exact integer using a precomputed scale. + fn decode_value(signed_encoded: ::Signed, scale: Self) -> Self; fn from_exact_bits(bits: Self::Exact) -> Self; } @@ -278,18 +282,14 @@ trait AlpFloat: Copy + Default { impl AlpFloat for f32 { type Exact = u32; - fn decode_value(signed_encoded: i32, exponent: u8, factor: u8) -> Result { - let factor_multiplier = ALP_I64_POW10 - .get(factor as usize) - .ok_or_else(|| general_err!("Invalid ALP page: factor {} exceeds max 18", factor))?; - let exponent_multiplier = ALP_NEG_POW10_F32.get(exponent as usize).ok_or_else(|| { - general_err!( - "Invalid ALP page: exponent {} exceeds max {} for f32", - exponent, - ALP_NEG_POW10_F32.len() - 1 - ) - })?; - Ok((signed_encoded as f32) * (*factor_multiplier as f32) * *exponent_multiplier) + fn decode_scale(exponent: u8, factor: u8) -> Self { + debug_assert!(exponent <= ALP_MAX_EXPONENT_F32); + debug_assert!(factor <= exponent); + (ALP_I64_POW10[factor as usize] as f32) * ALP_NEG_POW10_F32[exponent as usize] + } + + fn decode_value(signed_encoded: i32, scale: Self) -> Self { + (signed_encoded as f32) * scale } fn from_exact_bits(bits: Self::Exact) -> Self { @@ -300,18 +300,14 @@ impl AlpFloat for f32 { impl AlpFloat for f64 { type Exact = u64; - fn decode_value(signed_encoded: i64, exponent: u8, factor: u8) -> Result { - let factor_multiplier = ALP_I64_POW10 - .get(factor as usize) - .ok_or_else(|| general_err!("Invalid ALP page: factor {} exceeds max 18", factor))?; - let exponent_multiplier = ALP_NEG_POW10_F64.get(exponent as usize).ok_or_else(|| { - general_err!( - "Invalid ALP page: exponent {} exceeds max {} for f64", - exponent, - ALP_NEG_POW10_F64.len() - 1 - ) - })?; - Ok((signed_encoded as f64) * (*factor_multiplier as f64) * *exponent_multiplier) + fn decode_scale(exponent: u8, factor: u8) -> Self { + debug_assert!(exponent <= ALP_MAX_EXPONENT_F64); + debug_assert!(factor <= exponent); + (ALP_I64_POW10[factor as usize] as f64) * ALP_NEG_POW10_F64[exponent as usize] + } + + fn decode_value(signed_encoded: i64, scale: Self) -> Self { + (signed_encoded as f64) * scale } fn from_exact_bits(bits: Self::Exact) -> Self { @@ -492,6 +488,36 @@ fn parse_vector_view( num_exceptions: u16::from_le_bytes([vector_bytes[2], vector_bytes[3]]), }; + let max_exponent = if Exact::WIDTH == 4 { + ALP_MAX_EXPONENT_F32 + } else { + ALP_MAX_EXPONENT_F64 + }; + + if alp_info.exponent > max_exponent { + return Err(general_err!( + "Invalid ALP page: exponent {} exceeds max {}", + alp_info.exponent, + max_exponent + )); + } + + if alp_info.factor > alp_info.exponent { + return Err(general_err!( + "Invalid ALP page: factor {} exceeds exponent {}", + alp_info.factor, + alp_info.exponent + )); + } + + if alp_info.num_exceptions > num_elements { + return Err(general_err!( + "Invalid ALP page: num_exceptions {} exceeds vector num_elements {}", + alp_info.num_exceptions, + num_elements + )); + } + let for_start = AlpEncodedVectorInfo::STORED_SIZE; let for_end = for_start + Exact::WIDTH; let frame_of_reference = Exact::from_le_slice(&vector_bytes[for_start..for_end]); @@ -533,7 +559,15 @@ fn parse_vector_view( let mut exception_positions = Vec::with_capacity(alp_info.num_exceptions as usize); for chunk in data[positions_start..positions_end].chunks_exact(2) { - exception_positions.push(u16::from_le_bytes([chunk[0], chunk[1]])); + let position = u16::from_le_bytes([chunk[0], chunk[1]]); + if position >= num_elements { + return Err(general_err!( + "Invalid ALP page: exception position {} out of bounds for vector length {}", + position, + num_elements + )); + } + exception_positions.push(position); } let packed_values = @@ -657,14 +691,12 @@ fn decode_vector_values( )?; inverse_for(&mut exact_values, vector.for_info.frame_of_reference); + let scale = Value::decode_scale(vector.alp_info.exponent, vector.alp_info.factor); + let mut out = Vec::with_capacity(vector.num_elements as usize); for exact_value in exact_values { let signed_value = exact_value.reinterpret_as_signed(); - out.push(Value::decode_value( - signed_value, - vector.alp_info.exponent, - vector.alp_info.factor, - )?); + out.push(Value::decode_value(signed_value, scale)); } if vector.exception_positions.len() != vector.exception_values.len() { @@ -705,6 +737,14 @@ fn decode_page_values(layout: &AlpPageLayout) -> Ok(out) } +/// Decoder for ALP-encoded floating-point pages (`f32`/`f64`). +/// +/// Current behavior: +/// - `set_data` parses + validates page layout and eagerly decodes all values. +/// - `get` copies from the decoded buffer into the requested output slice. +/// - `skip` advances the decoded cursor without additional parsing/decoding. +/// +/// This keeps ALP decoding straightforward while functionality is being brought up. pub(crate) struct AlpDecoder { decoded_values: Vec, values_decoded: usize, @@ -792,6 +832,92 @@ mod tests { out } + fn make_vector_u32( + exponent: u8, + factor: u8, + num_exceptions: u16, + frame_of_reference: u32, + bit_width: u8, + packed_values: &[u8], + exception_positions: &[u16], + exception_values: &[u32], + ) -> Vec { + assert_eq!(num_exceptions as usize, exception_positions.len()); + assert_eq!(num_exceptions as usize, exception_values.len()); + + let mut out = Vec::new(); + out.push(exponent); + out.push(factor); + out.extend_from_slice(&num_exceptions.to_le_bytes()); + out.extend_from_slice(&frame_of_reference.to_le_bytes()); + out.push(bit_width); + out.extend_from_slice(packed_values); + for position in exception_positions { + out.extend_from_slice(&position.to_le_bytes()); + } + for value in exception_values { + out.extend_from_slice(&value.to_le_bytes()); + } + out + } + + fn make_vector_u64( + exponent: u8, + factor: u8, + num_exceptions: u16, + frame_of_reference: u64, + bit_width: u8, + packed_values: &[u8], + exception_positions: &[u16], + exception_values: &[u64], + ) -> Vec { + assert_eq!(num_exceptions as usize, exception_positions.len()); + assert_eq!(num_exceptions as usize, exception_values.len()); + + let mut out = Vec::new(); + out.push(exponent); + out.push(factor); + out.extend_from_slice(&num_exceptions.to_le_bytes()); + out.extend_from_slice(&frame_of_reference.to_le_bytes()); + out.push(bit_width); + out.extend_from_slice(packed_values); + for position in exception_positions { + out.extend_from_slice(&position.to_le_bytes()); + } + for value in exception_values { + out.extend_from_slice(&value.to_le_bytes()); + } + out + } + + fn make_page_from_vectors( + log_vector_size: u8, + num_elements: i32, + vectors: &[Vec], + ) -> Vec { + let vector_size = 1usize << log_vector_size; + let expected_num_vectors = if num_elements <= 0 { + 0 + } else { + (num_elements as usize).div_ceil(vector_size) + }; + assert_eq!(vectors.len(), expected_num_vectors); + + let offsets_section_size = vectors.len() * std::mem::size_of::(); + let mut offsets = Vec::with_capacity(vectors.len()); + let mut running_offset = offsets_section_size as u32; + for vector in vectors { + offsets.push(running_offset); + running_offset += vector.len() as u32; + } + + let mut page = make_alp_page_bytes(1, 0, 0, log_vector_size, num_elements, &offsets, 0); + for vector in vectors { + page.extend_from_slice(vector); + } + page + } + #[test] fn test_parse_alp_page_layout_valid() { let data = make_alp_page_bytes(1, 0, 0, 2, 4, &[4], 13); @@ -840,6 +966,88 @@ mod tests { ); } + #[test] + fn test_parse_alp_page_layout_invalid_exponent_f32() { + let vector = make_vector_u32(11, 0, 0, 0, 0, &[], &[], &[]); + let page = make_page_from_vectors(0, 1, &[vector]); + let err = parse_alp_page_layout::(Bytes::from(page)).unwrap_err(); + assert!( + err.to_string() + .contains("Invalid ALP page: exponent 11 exceeds max 10") + ); + } + + #[test] + fn test_parse_alp_page_layout_invalid_factor_f32() { + let vector = make_vector_u32(0, 11, 0, 0, 0, &[], &[], &[]); + let page = make_page_from_vectors(0, 1, &[vector]); + let err = parse_alp_page_layout::(Bytes::from(page)).unwrap_err(); + assert!( + err.to_string() + .contains("Invalid ALP page: factor 11 exceeds exponent 0") + ); + } + + #[test] + fn test_parse_alp_page_layout_factor_exceeds_exponent() { + let vector = make_vector_u32(2, 3, 0, 0, 0, &[], &[], &[]); + let page = make_page_from_vectors(0, 1, &[vector]); + let err = parse_alp_page_layout::(Bytes::from(page)).unwrap_err(); + assert!( + err.to_string() + .contains("Invalid ALP page: factor 3 exceeds exponent 2") + ); + } + + #[test] + fn test_parse_alp_page_layout_invalid_num_exceptions() { + let vector = make_vector_u32(0, 0, 2, 0, 0, &[], &[0, 0], &[0, 0]); + let page = make_page_from_vectors(0, 1, &[vector]); + let err = parse_alp_page_layout::(Bytes::from(page)).unwrap_err(); + assert!( + err.to_string() + .contains("Invalid ALP page: num_exceptions 2 exceeds vector num_elements 1") + ); + } + + #[test] + fn test_parse_alp_page_layout_invalid_exception_position() { + let vector = make_vector_u32(0, 0, 1, 0, 0, &[], &[1], &[123]); + let page = make_page_from_vectors(0, 1, &[vector]); + let err = parse_alp_page_layout::(Bytes::from(page)).unwrap_err(); + assert!( + err.to_string().contains( + "Invalid ALP page: exception position 1 out of bounds for vector length 1" + ) + ); + } + + #[test] + fn test_parse_alp_page_layout_non_monotonic_offsets() { + let data = make_alp_page_bytes(1, 0, 0, 1, 3, &[12, 8], 12); + let err = parse_alp_page_layout::(Bytes::from(data)).unwrap_err(); + assert!( + err.to_string() + .contains("Invalid ALP page: vector offsets are not monotonic at index 0") + ); + } + + #[test] + fn test_parse_alp_page_layout_truncated_vector_data() { + let mut vector = Vec::new(); + vector.push(0); + vector.push(0); + vector.extend_from_slice(&0u16.to_le_bytes()); + vector.extend_from_slice(&10u32.to_le_bytes()); + vector.push(1); + let page = make_page_from_vectors(1, 2, &[vector]); + let err = parse_alp_page_layout::(Bytes::from(page)).unwrap_err(); + assert!( + err.to_string() + .contains("Invalid ALP page: vector data too short") + ); + } + #[test] fn test_parse_alp_page_layout_parses_vector_view_data_only_f64() { let mut vector = Vec::new(); @@ -910,4 +1118,41 @@ mod tests { let decoded = decode_vector_exact(&[0b0010_1100], &vector).unwrap(); assert_eq!(decoded, vec![10, 13, 77]); } + + #[test] + fn test_decode_page_values_f32_no_exceptions() { + let vector = make_vector_u32(0, 0, 0, 10, 2, &[0b1110_0100], &[], &[]); + let page = make_page_from_vectors(2, 4, &[vector]); + let layout = parse_alp_page_layout::(Bytes::from(page)).unwrap(); + let decoded = decode_page_values::(&layout).unwrap(); + assert_eq!(decoded, vec![10.0, 11.0, 12.0, 13.0]); + } + + #[test] + fn test_decode_page_values_f64_multi_vector_with_exceptions() { + let vector0 = make_vector_u64(0, 0, 1, 10, 1, &[0b0000_0010], &[1], &[42.5f64.to_bits()]); + let vector1 = make_vector_u64(0, 0, 0, 7, 0, &[], &[], &[]); + let page = make_page_from_vectors(1, 3, &[vector0, vector1]); + let layout = parse_alp_page_layout::(Bytes::from(page)).unwrap(); + let decoded = decode_page_values::(&layout).unwrap(); + assert_eq!(decoded, vec![10.0, 42.5, 7.0]); + } + + #[test] + fn test_decode_page_values_f32_edge_values_via_exceptions() { + let edge_values = [ + f32::NAN.to_bits(), + (-0.0f32).to_bits(), + f32::INFINITY.to_bits(), + ]; + let vector = make_vector_u32(0, 0, 3, 0, 0, &[], &[0, 1, 2], &edge_values); + let page = make_page_from_vectors(2, 3, &[vector]); + let layout = parse_alp_page_layout::(Bytes::from(page)).unwrap(); + let decoded = decode_page_values::(&layout).unwrap(); + + assert!(decoded[0].is_nan()); + assert_eq!(decoded[1], -0.0); + assert!(decoded[1].is_sign_negative()); + assert_eq!(decoded[2], f32::INFINITY); + } } From 8ee1b7ef61f0a1b0297b3ab1cfce493a7b7c2d92 Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Fri, 20 Feb 2026 15:29:52 -0500 Subject: [PATCH 08/50] Make decode page lazy + some tests --- parquet/src/encodings/decoding/alp.rs | 212 +++++++++++++------------- 1 file changed, 110 insertions(+), 102 deletions(-) diff --git a/parquet/src/encodings/decoding/alp.rs b/parquet/src/encodings/decoding/alp.rs index 66d71245251f..8c55f2b71d3b 100644 --- a/parquet/src/encodings/decoding/alp.rs +++ b/parquet/src/encodings/decoding/alp.rs @@ -138,7 +138,6 @@ struct AlpEncodedVectorView { struct AlpPageLayout { header: AlpHeader, body: Bytes, - #[allow(dead_code)] offsets: Vec, vectors: Vec>, } @@ -153,7 +152,7 @@ struct AlpPageLayout { /// - FOR stores non-negative deltas optimized for bitpacking. /// - Unsigned arithmetic avoids signed-overflow edge cases in FOR stage. /// - Signed interpretation is applied later during decimal reconstruction. -trait AlpExact: Copy + std::fmt::Debug { +pub(super) trait AlpExact: Copy + std::fmt::Debug { const WIDTH: usize; type Signed: Copy; fn from_le_slice(slice: &[u8]) -> Self; @@ -264,7 +263,7 @@ const ALP_NEG_POW10_F64: [f64; 19] = [ 0.000000000000000001, ]; -trait AlpFloat: Copy + Default { +pub(super) trait AlpFloat: Copy + Default { type Exact: AlpExact + FromBytes; /// Precompute vector-level ALP decimal scale for: @@ -627,57 +626,6 @@ fn inverse_for(deltas: &mut [Exact], frame_of_reference: Exact) } } -/// Patch exception values at their original positions. -#[cfg(test)] -fn patch_exceptions( - decoded: &mut [Exact], - exception_positions: &[u16], - exception_values: &[Exact], -) -> Result<()> { - if exception_positions.len() != exception_values.len() { - return Err(general_err!( - "Invalid ALP page: exception positions ({}) and values ({}) length mismatch", - exception_positions.len(), - exception_values.len() - )); - } - - for (pos, value) in exception_positions.iter().zip(exception_values.iter()) { - let pos = *pos as usize; - if pos >= decoded.len() { - return Err(general_err!( - "Invalid ALP page: exception position {} out of bounds for vector length {}", - pos, - decoded.len() - )); - } - decoded[pos] = *value; - } - - Ok(()) -} - -/// Decode one vector to exact integers: -/// bit-unpack -> inverse FOR -> exception patch. -#[cfg(test)] -fn decode_vector_exact( - body: &[u8], - vector: &AlpEncodedVectorView, -) -> Result> { - let mut decoded = bit_unpack_integers( - &body[vector.packed_values.clone()], - vector.for_info.bit_width, - vector.num_elements, - )?; - inverse_for(&mut decoded, vector.for_info.frame_of_reference); - patch_exceptions( - &mut decoded, - &vector.exception_positions, - &vector.exception_values, - )?; - Ok(decoded) -} - /// Decode one vector into output floating values: /// bit-unpack -> inverse FOR -> decimal decode -> patch exceptions. fn decode_vector_values( @@ -740,30 +688,59 @@ fn decode_page_values(layout: &AlpPageLayout) -> /// Decoder for ALP-encoded floating-point pages (`f32`/`f64`). /// /// Current behavior: -/// - `set_data` parses + validates page layout and eagerly decodes all values. -/// - `get` copies from the decoded buffer into the requested output slice. -/// - `skip` advances the decoded cursor without additional parsing/decoding. -/// -/// This keeps ALP decoding straightforward while functionality is being brought up. -pub(crate) struct AlpDecoder { +/// - `set_data` parses + validates page metadata and stores ALP layout state. +/// - `get` lazily decodes the full page once, then copies from the decoded buffer. +/// - `skip` advances the decoded cursor. +pub(crate) struct AlpDecoder +where + T::T: AlpFloat, + ::Exact: Send, +{ + layout: Option::Exact>>, decoded_values: Vec, - values_decoded: usize, + current_offset: usize, + needs_decode: bool, + num_values: usize, _marker: PhantomData, } -impl AlpDecoder { +impl AlpDecoder +where + T::T: AlpFloat, + ::Exact: Send, +{ pub(crate) fn new() -> Self { Self { + layout: None, decoded_values: Vec::new(), - values_decoded: 0, + current_offset: 0, + needs_decode: false, + num_values: 0, _marker: PhantomData, } } + + fn ensure_decoded(&mut self) -> Result<()> { + if !self.needs_decode { + return Ok(()); + } + + let layout = self.layout.take().ok_or_else(|| { + general_err!("Invalid ALP decoder state: set_data must be called before get/skip") + })?; + + debug_assert_eq!(layout.offsets.len(), layout.vectors.len()); + self.decoded_values = decode_page_values::(&layout)?; + self.needs_decode = false; + + Ok(()) + } } impl Decoder for AlpDecoder where T::T: AlpFloat, + ::Exact: Send, { fn set_data(&mut self, data: Bytes, num_values: usize) -> Result<()> { let layout = parse_alp_page_layout::<::Exact>(data)?; @@ -776,23 +753,30 @@ where )); } - self.decoded_values = decode_page_values::(&layout)?; - self.values_decoded = 0; + self.layout = Some(layout); + self.decoded_values.clear(); + self.current_offset = 0; + self.needs_decode = num_values > 0; + self.num_values = num_values; Ok(()) } fn get(&mut self, buffer: &mut [T::T]) -> Result { - let num_values = buffer.len().min(self.values_left()); - let end = self.values_decoded + num_values; - buffer[..num_values].copy_from_slice(&self.decoded_values[self.values_decoded..end]); - self.values_decoded = end; - Ok(num_values) + let target = buffer.len().min(self.num_values); + if target == 0 { + return Ok(0); + } + + self.ensure_decoded()?; + let end = self.current_offset + target; + buffer[..target].copy_from_slice(&self.decoded_values[self.current_offset..end]); + self.current_offset = end; + self.num_values -= target; + Ok(target) } fn values_left(&self) -> usize { - self.decoded_values - .len() - .saturating_sub(self.values_decoded) + self.num_values } fn encoding(&self) -> Encoding { @@ -800,15 +784,22 @@ where } fn skip(&mut self, num_values: usize) -> Result { - let skipped = num_values.min(self.values_left()); - self.values_decoded += skipped; - Ok(skipped) + let to_skip = num_values.min(self.num_values); + if to_skip == 0 { + return Ok(0); + } + + self.ensure_decoded()?; + self.current_offset += to_skip; + self.num_values -= to_skip; + Ok(to_skip) } } #[cfg(test)] mod tests { use super::*; + use crate::data_type::FloatType; fn make_alp_page_bytes( version: u8, @@ -1088,35 +1079,10 @@ mod tests { } #[test] - fn test_inverse_for_and_patch_exceptions() { + fn test_inverse_for() { let mut decoded = vec![0u32, 3, 2]; inverse_for(&mut decoded, 10); assert_eq!(decoded, vec![10, 13, 12]); - - patch_exceptions(&mut decoded, &[1], &[99]).unwrap(); - assert_eq!(decoded, vec![10, 99, 12]); - } - - #[test] - fn test_decode_vector_exact() { - let vector = AlpEncodedVectorView:: { - num_elements: 3, - alp_info: AlpEncodedVectorInfo { - exponent: 0, - factor: 0, - num_exceptions: 1, - }, - for_info: AlpEncodedForVectorInfo { - frame_of_reference: 10, - bit_width: 2, - }, - packed_values: 0..1, - exception_positions: vec![2], - exception_values: vec![77], - }; - - let decoded = decode_vector_exact(&[0b0010_1100], &vector).unwrap(); - assert_eq!(decoded, vec![10, 13, 77]); } #[test] @@ -1155,4 +1121,46 @@ mod tests { assert!(decoded[1].is_sign_negative()); assert_eq!(decoded[2], f32::INFINITY); } + + #[test] + fn test_alp_decoder_get_across_vectors() { + let vector0 = make_vector_u32(0, 0, 0, 10, 1, &[0b0000_0010], &[], &[]); + let vector1 = make_vector_u32(0, 0, 0, 20, 1, &[0b0000_0010], &[], &[]); + let page = make_page_from_vectors(1, 4, &[vector0, vector1]); + + let mut decoder = AlpDecoder::::new(); + decoder.set_data(Bytes::from(page), 4).unwrap(); + + let mut first = [0.0f32; 3]; + let read = decoder.get(&mut first).unwrap(); + assert_eq!(read, 3); + assert_eq!(first, [10.0, 11.0, 20.0]); + assert_eq!(decoder.values_left(), 1); + + let mut second = [0.0f32; 2]; + let read = decoder.get(&mut second).unwrap(); + assert_eq!(read, 1); + assert_eq!(second[0], 21.0); + assert_eq!(decoder.values_left(), 0); + } + + #[test] + fn test_alp_decoder_skip_across_vectors() { + let vector0 = make_vector_u32(0, 0, 0, 10, 1, &[0b0000_0010], &[], &[]); + let vector1 = make_vector_u32(0, 0, 0, 20, 1, &[0b0000_0010], &[], &[]); + let page = make_page_from_vectors(1, 4, &[vector0, vector1]); + + let mut decoder = AlpDecoder::::new(); + decoder.set_data(Bytes::from(page), 4).unwrap(); + + let skipped = decoder.skip(3).unwrap(); + assert_eq!(skipped, 3); + assert_eq!(decoder.values_left(), 1); + + let mut out = [0.0f32; 1]; + let read = decoder.get(&mut out).unwrap(); + assert_eq!(read, 1); + assert_eq!(out[0], 21.0); + assert_eq!(decoder.values_left(), 0); + } } From 18f885e834a647c6fdbcceb7a7c264975179e41a Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Fri, 20 Feb 2026 15:59:10 -0500 Subject: [PATCH 09/50] Find vectors using offsets at decode time, not parse time --- parquet/src/encodings/decoding/alp.rs | 57 ++++++++++++++++----------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/parquet/src/encodings/decoding/alp.rs b/parquet/src/encodings/decoding/alp.rs index 8c55f2b71d3b..189a8aadde04 100644 --- a/parquet/src/encodings/decoding/alp.rs +++ b/parquet/src/encodings/decoding/alp.rs @@ -135,11 +135,10 @@ struct AlpEncodedVectorView { /// Parsed ALP page layout for one exact integer width (`u32` for float pages, /// `u64` for double pages). #[derive(Debug)] -struct AlpPageLayout { +struct AlpPageLayout { header: AlpHeader, body: Bytes, offsets: Vec, - vectors: Vec>, } /// Exact integer type used by FOR reconstruction. @@ -321,7 +320,7 @@ impl AlpFloat for f64 { /// - non-negative `num_elements` /// - offsets bounds + monotonicity /// - per-vector metadata/data section lengths -fn parse_alp_page_layout(data: Bytes) -> Result> { +fn parse_alp_page_layout(data: Bytes) -> Result { let data_ref = data.as_ref(); if data_ref.len() < ALP_HEADER_SIZE { return Err(general_err!( @@ -428,7 +427,6 @@ fn parse_alp_page_layout(data: Bytes) -> Result(data: Bytes) -> Result( - body_ref, - vector_start, - vector_end, - vector_num_elements, - )?); + parse_vector_view::(body_ref, vector_start, vector_end, vector_num_elements)?; } Ok(AlpPageLayout { header, body, offsets, - vectors, }) } @@ -674,12 +666,25 @@ fn decode_vector_values( Ok(out) } -fn decode_page_values(layout: &AlpPageLayout) -> Result> { +fn decode_page_values(layout: &AlpPageLayout) -> Result> { let mut out = Vec::with_capacity(layout.header.num_elements_usize()); - for vector in &layout.vectors { + for (vector_idx, vector_offset) in layout.offsets.iter().enumerate() { + let vector_start = *vector_offset as usize; + let vector_end = if vector_idx + 1 < layout.offsets.len() { + layout.offsets[vector_idx + 1] as usize + } else { + layout.body.len() + }; + let vector_num_elements = layout.header.vector_num_elements(vector_idx); + let vector = parse_vector_view::( + layout.body.as_ref(), + vector_start, + vector_end, + vector_num_elements, + )?; out.extend_from_slice(&decode_vector_values::( layout.body.as_ref(), - vector, + &vector, )?); } Ok(out) @@ -696,7 +701,7 @@ where T::T: AlpFloat, ::Exact: Send, { - layout: Option::Exact>>, + layout: Option, decoded_values: Vec, current_offset: usize, needs_decode: bool, @@ -729,7 +734,6 @@ where general_err!("Invalid ALP decoder state: set_data must be called before get/skip") })?; - debug_assert_eq!(layout.offsets.len(), layout.vectors.len()); self.decoded_values = decode_page_values::(&layout)?; self.needs_decode = false; @@ -1058,12 +1062,21 @@ mod tests { page.extend_from_slice(&vector); let parsed = parse_alp_page_layout::(Bytes::from(page)).unwrap(); - assert_eq!(parsed.vectors.len(), 1); - assert_eq!(parsed.vectors[0].num_elements, 1); - assert_eq!(parsed.vectors[0].alp_info.num_exceptions, 1); - assert_eq!(parsed.vectors[0].for_info.bit_width, 0); - assert_eq!(parsed.vectors[0].exception_positions, vec![0]); - assert_eq!(parsed.vectors[0].exception_values, vec![42.5_f64.to_bits()]); + assert_eq!(parsed.offsets, vec![4]); + let vector_start = parsed.offsets[0] as usize; + let vector_end = parsed.body.len(); + let parsed_vector = parse_vector_view::( + parsed.body.as_ref(), + vector_start, + vector_end, + parsed.header.vector_num_elements(0), + ) + .unwrap(); + assert_eq!(parsed_vector.num_elements, 1); + assert_eq!(parsed_vector.alp_info.num_exceptions, 1); + assert_eq!(parsed_vector.for_info.bit_width, 0); + assert_eq!(parsed_vector.exception_positions, vec![0]); + assert_eq!(parsed_vector.exception_values, vec![42.5_f64.to_bits()]); } #[test] From a9fa0c30e53fc80b07083ad3c056316c78ed1b51 Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Fri, 20 Feb 2026 16:25:18 -0500 Subject: [PATCH 10/50] get decode fast path --- parquet/src/encodings/decoding/alp.rs | 88 ++++++++++++++++++++++++--- 1 file changed, 81 insertions(+), 7 deletions(-) diff --git a/parquet/src/encodings/decoding/alp.rs b/parquet/src/encodings/decoding/alp.rs index 189a8aadde04..87b63a6962ce 100644 --- a/parquet/src/encodings/decoding/alp.rs +++ b/parquet/src/encodings/decoding/alp.rs @@ -667,7 +667,31 @@ fn decode_vector_values( } fn decode_page_values(layout: &AlpPageLayout) -> Result> { - let mut out = Vec::with_capacity(layout.header.num_elements_usize()); + let total = layout.header.num_elements_usize(); + let mut out = vec![Value::default(); total]; + let written = decode_page_values_into::(layout, &mut out)?; + debug_assert_eq!(written, total); + Ok(out) +} + +/// Decode a full ALP page into an existing output slice. +/// +/// This walks vectors using `layout.offsets` (C++-style offset-based decode flow), +/// reparsing each vector view from the page body and decoding it into `out`. +fn decode_page_values_into( + layout: &AlpPageLayout, + out: &mut [Value], +) -> Result { + let total = layout.header.num_elements_usize(); + if out.len() < total { + return Err(general_err!( + "Invalid ALP decode output: output length {} smaller than page values {}", + out.len(), + total + )); + } + + let mut output_offset = 0usize; for (vector_idx, vector_offset) in layout.offsets.iter().enumerate() { let vector_start = *vector_offset as usize; let vector_end = if vector_idx + 1 < layout.offsets.len() { @@ -682,19 +706,20 @@ fn decode_page_values(layout: &AlpPageLayout) -> Result( - layout.body.as_ref(), - &vector, - )?); + let vector_values = decode_vector_values::(layout.body.as_ref(), &vector)?; + let next_offset = output_offset + vector_values.len(); + out[output_offset..next_offset].copy_from_slice(&vector_values); + output_offset = next_offset; } - Ok(out) + Ok(output_offset) } /// Decoder for ALP-encoded floating-point pages (`f32`/`f64`). /// /// Current behavior: /// - `set_data` parses + validates page metadata and stores ALP layout state. -/// - `get` lazily decodes the full page once, then copies from the decoded buffer. +/// - `get` uses a fast path to decode directly to output when all values are requested. +/// - otherwise, `get` lazily decodes the full page once, then copies from decoded buffer. /// - `skip` advances the decoded cursor. pub(crate) struct AlpDecoder where @@ -725,6 +750,9 @@ where } } + /// Decode the stored page into `decoded_values` if it hasn't been decoded yet. + /// + /// Used by partial `get` / `skip` paths that need a stable decoded buffer. fn ensure_decoded(&mut self) -> Result<()> { if !self.needs_decode { return Ok(()); @@ -746,6 +774,9 @@ where T::T: AlpFloat, ::Exact: Send, { + /// Store validated page layout and reset read cursor state. + /// + /// Actual value decoding is deferred until first `get`/`skip`. fn set_data(&mut self, data: Bytes, num_values: usize) -> Result<()> { let layout = parse_alp_page_layout::<::Exact>(data)?; @@ -765,12 +796,32 @@ where Ok(()) } + /// Read up to `buffer.len()` decoded values. + /// + /// Fast path: if caller requests all remaining values from a fresh page, + /// decode directly into `buffer` (matching C++ ALP decoder behavior). + /// Otherwise decode once into internal storage and copy slices from there. fn get(&mut self, buffer: &mut [T::T]) -> Result { let target = buffer.len().min(self.num_values); if target == 0 { return Ok(0); } + // C++ parity fast path: decode directly into caller output when it asks + // for all remaining values from a fresh page. + if self.needs_decode && target == self.num_values { + let layout = self.layout.take().ok_or_else(|| { + general_err!("Invalid ALP decoder state: set_data must be called before get/skip") + })?; + let written = decode_page_values_into::(&layout, &mut buffer[..target])?; + self.needs_decode = false; + self.decoded_values.clear(); + self.current_offset = 0; + self.num_values = 0; + debug_assert_eq!(written, target); + return Ok(written); + } + self.ensure_decoded()?; let end = self.current_offset + target; buffer[..target].copy_from_slice(&self.decoded_values[self.current_offset..end]); @@ -787,6 +838,10 @@ where Encoding::ALP } + /// Skip up to `num_values` decoded values. + /// + /// For parity with C++ partial-read behavior, this may trigger deferred + /// page decoding before advancing the cursor. fn skip(&mut self, num_values: usize) -> Result { let to_skip = num_values.min(self.num_values); if to_skip == 0 { @@ -1176,4 +1231,23 @@ mod tests { assert_eq!(out[0], 21.0); assert_eq!(decoder.values_left(), 0); } + + #[test] + fn test_alp_decoder_get_fast_path_full_read() { + let vector0 = make_vector_u32(0, 0, 0, 10, 1, &[0b0000_0010], &[], &[]); + let vector1 = make_vector_u32(0, 0, 0, 20, 1, &[0b0000_0010], &[], &[]); + let page = make_page_from_vectors(1, 4, &[vector0, vector1]); + + let mut decoder = AlpDecoder::::new(); + decoder.set_data(Bytes::from(page), 4).unwrap(); + + let mut out = [0.0f32; 4]; + let read = decoder.get(&mut out).unwrap(); + assert_eq!(read, 4); + assert_eq!(out, [10.0, 11.0, 20.0, 21.0]); + assert_eq!(decoder.values_left(), 0); + assert!(!decoder.needs_decode); + assert!(decoder.decoded_values.is_empty()); + assert!(decoder.layout.is_none()); + } } From 7754ebca92c46eab5fa2217c156fdf8386181f3d Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Fri, 20 Feb 2026 17:01:57 -0500 Subject: [PATCH 11/50] Simplify decode_page_values --- parquet/src/encodings/decoding/alp.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/parquet/src/encodings/decoding/alp.rs b/parquet/src/encodings/decoding/alp.rs index 87b63a6962ce..be756354941a 100644 --- a/parquet/src/encodings/decoding/alp.rs +++ b/parquet/src/encodings/decoding/alp.rs @@ -669,8 +669,7 @@ fn decode_vector_values( fn decode_page_values(layout: &AlpPageLayout) -> Result> { let total = layout.header.num_elements_usize(); let mut out = vec![Value::default(); total]; - let written = decode_page_values_into::(layout, &mut out)?; - debug_assert_eq!(written, total); + decode_page_values_into::(layout, &mut out)?; Ok(out) } @@ -681,7 +680,7 @@ fn decode_page_values(layout: &AlpPageLayout) -> Result( layout: &AlpPageLayout, out: &mut [Value], -) -> Result { +) -> Result<()> { let total = layout.header.num_elements_usize(); if out.len() < total { return Err(general_err!( @@ -711,7 +710,14 @@ fn decode_page_values_into( out[output_offset..next_offset].copy_from_slice(&vector_values); output_offset = next_offset; } - Ok(output_offset) + if output_offset != total { + return Err(general_err!( + "Invalid ALP decode output: decoded {} values, expected {}", + output_offset, + total + )); + } + Ok(()) } /// Decoder for ALP-encoded floating-point pages (`f32`/`f64`). @@ -813,13 +819,12 @@ where let layout = self.layout.take().ok_or_else(|| { general_err!("Invalid ALP decoder state: set_data must be called before get/skip") })?; - let written = decode_page_values_into::(&layout, &mut buffer[..target])?; + decode_page_values_into::(&layout, &mut buffer[..target])?; self.needs_decode = false; self.decoded_values.clear(); self.current_offset = 0; self.num_values = 0; - debug_assert_eq!(written, target); - return Ok(written); + return Ok(target); } self.ensure_decoded()?; From 6b7e923ede8b3e9742a5488a2a26aea9c5c32535 Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Thu, 12 Mar 2026 14:09:38 -0400 Subject: [PATCH 12/50] remove `version` from page header --- parquet/src/encodings/decoding/alp.rs | 53 ++++++++++----------------- 1 file changed, 19 insertions(+), 34 deletions(-) diff --git a/parquet/src/encodings/decoding/alp.rs b/parquet/src/encodings/decoding/alp.rs index be756354941a..6d8cdb4fdb8c 100644 --- a/parquet/src/encodings/decoding/alp.rs +++ b/parquet/src/encodings/decoding/alp.rs @@ -26,25 +26,22 @@ use crate::encodings::decoding::Decoder; use crate::errors::{ParquetError, Result}; use crate::util::bit_util::{BitReader, FromBytes}; -const ALP_HEADER_SIZE: usize = 8; -const ALP_VERSION: u8 = 1; +const ALP_HEADER_SIZE: usize = 7; const ALP_COMPRESSION_MODE: u8 = 0; const ALP_INTEGER_ENCODING_FOR_BIT_PACK: u8 = 0; const ALP_MAX_LOG_VECTOR_SIZE: u8 = 16; const ALP_MAX_EXPONENT_F32: u8 = 10; const ALP_MAX_EXPONENT_F64: u8 = 18; -/// Page-level ALP header (version 1, 8 bytes). +/// Page-level ALP header (7 bytes). /// /// Layout in bytes: -/// - `[0]` `version` -/// - `[1]` `compression_mode` -/// - `[2]` `integer_encoding` -/// - `[3]` `log_vector_size` -/// - `[4..8]` `num_elements` (little-endian `i32`) +/// - `[0]` `compression_mode` +/// - `[1]` `integer_encoding` +/// - `[2]` `log_vector_size` +/// - `[3..7]` `num_elements` (little-endian `i32`) #[derive(Debug, Clone, Copy)] struct AlpHeader { - version: u8, compression_mode: u8, integer_encoding: u8, log_vector_size: u8, @@ -316,7 +313,7 @@ impl AlpFloat for f64 { /// Parse and validate a full ALP-encoded page body. /// /// Validation includes: -/// - header fields/version/encoding +/// - header fields/encoding /// - non-negative `num_elements` /// - offsets bounds + monotonicity /// - per-vector metadata/data section lengths @@ -331,21 +328,12 @@ fn parse_alp_page_layout(data: Bytes) -> Result } let header = AlpHeader { - version: data_ref[0], - compression_mode: data_ref[1], - integer_encoding: data_ref[2], - log_vector_size: data_ref[3], - num_elements: i32::from_le_bytes([data_ref[4], data_ref[5], data_ref[6], data_ref[7]]), + compression_mode: data_ref[0], + integer_encoding: data_ref[1], + log_vector_size: data_ref[2], + num_elements: i32::from_le_bytes([data_ref[3], data_ref[4], data_ref[5], data_ref[6]]), }; - if header.version != ALP_VERSION { - return Err(general_err!( - "Invalid ALP page: unsupported version {}, expected {}", - header.version, - ALP_VERSION - )); - } - if header.compression_mode != ALP_COMPRESSION_MODE { return Err(general_err!( "Invalid ALP page: unsupported compression mode {}", @@ -866,7 +854,6 @@ mod tests { use crate::data_type::FloatType; fn make_alp_page_bytes( - version: u8, compression_mode: u8, integer_encoding: u8, log_vector_size: u8, @@ -875,7 +862,6 @@ mod tests { body_tail_len: usize, ) -> Vec { let mut out = Vec::with_capacity(ALP_HEADER_SIZE + offsets.len() * 4 + body_tail_len); - out.push(version); out.push(compression_mode); out.push(integer_encoding); out.push(log_vector_size); @@ -966,7 +952,7 @@ mod tests { running_offset += vector.len() as u32; } - let mut page = make_alp_page_bytes(1, 0, 0, log_vector_size, num_elements, &offsets, 0); + let mut page = make_alp_page_bytes(0, 0, log_vector_size, num_elements, &offsets, 0); for vector in vectors { page.extend_from_slice(vector); } @@ -975,9 +961,8 @@ mod tests { #[test] fn test_parse_alp_page_layout_valid() { - let data = make_alp_page_bytes(1, 0, 0, 2, 4, &[4], 13); + let data = make_alp_page_bytes(0, 0, 2, 4, &[4], 13); let parsed = parse_alp_page_layout::(Bytes::from(data)).unwrap(); - assert_eq!(parsed.header.version, 1); assert_eq!(parsed.header.num_elements, 4); assert_eq!(parsed.offsets, vec![4]); } @@ -987,13 +972,13 @@ mod tests { let err = parse_alp_page_layout::(Bytes::from_static(&[0, 1, 2])).unwrap_err(); assert!( err.to_string() - .contains("Invalid ALP page: expected at least 8 bytes for header") + .contains("Invalid ALP page: expected at least 7 bytes for header") ); } #[test] fn test_parse_alp_page_layout_invalid_log_vector_size() { - let data = make_alp_page_bytes(1, 0, 0, 17, 1, &[4], 8); + let data = make_alp_page_bytes(0, 0, 17, 1, &[4], 8); let err = parse_alp_page_layout::(Bytes::from(data)).unwrap_err(); assert!( err.to_string() @@ -1003,7 +988,7 @@ mod tests { #[test] fn test_parse_alp_page_layout_invalid_integer_encoding() { - let data = make_alp_page_bytes(1, 0, 1, 2, 1, &[4], 8); + let data = make_alp_page_bytes(0, 1, 2, 1, &[4], 8); let err = parse_alp_page_layout::(Bytes::from(data)).unwrap_err(); assert!( err.to_string() @@ -1013,7 +998,7 @@ mod tests { #[test] fn test_parse_alp_page_layout_negative_num_elements() { - let data = make_alp_page_bytes(1, 0, 0, 2, -1, &[4], 8); + let data = make_alp_page_bytes(0, 0, 2, -1, &[4], 8); let err = parse_alp_page_layout::(Bytes::from(data)).unwrap_err(); assert!( err.to_string() @@ -1079,7 +1064,7 @@ mod tests { #[test] fn test_parse_alp_page_layout_non_monotonic_offsets() { - let data = make_alp_page_bytes(1, 0, 0, 1, 3, &[12, 8], 12); + let data = make_alp_page_bytes(0, 0, 1, 3, &[12, 8], 12); let err = parse_alp_page_layout::(Bytes::from(data)).unwrap_err(); assert!( err.to_string() @@ -1118,7 +1103,7 @@ mod tests { vector.extend_from_slice(&42.5_f64.to_le_bytes()); let offsets = [4u32]; - let mut page = make_alp_page_bytes(1, 0, 0, 0, 1, &offsets, 0); + let mut page = make_alp_page_bytes(0, 0, 0, 1, &offsets, 0); page.extend_from_slice(&vector); let parsed = parse_alp_page_layout::(Bytes::from(page)).unwrap(); From a13f24425646025d2e6f86714d106b00108f6111 Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Thu, 12 Mar 2026 14:34:56 -0400 Subject: [PATCH 13/50] change `log_vetctor_size` range consts --- parquet/src/encodings/decoding/alp.rs | 108 ++++++++++++++++---------- 1 file changed, 69 insertions(+), 39 deletions(-) diff --git a/parquet/src/encodings/decoding/alp.rs b/parquet/src/encodings/decoding/alp.rs index 6d8cdb4fdb8c..94b53ca03474 100644 --- a/parquet/src/encodings/decoding/alp.rs +++ b/parquet/src/encodings/decoding/alp.rs @@ -29,7 +29,8 @@ use crate::util::bit_util::{BitReader, FromBytes}; const ALP_HEADER_SIZE: usize = 7; const ALP_COMPRESSION_MODE: u8 = 0; const ALP_INTEGER_ENCODING_FOR_BIT_PACK: u8 = 0; -const ALP_MAX_LOG_VECTOR_SIZE: u8 = 16; +const ALP_MIN_LOG_VECTOR_SIZE: u8 = 3; +const ALP_MAX_LOG_VECTOR_SIZE: u8 = 15; const ALP_MAX_EXPONENT_F32: u8 = 10; const ALP_MAX_EXPONENT_F64: u8 = 18; @@ -348,6 +349,14 @@ fn parse_alp_page_layout(data: Bytes) -> Result )); } + if header.log_vector_size < ALP_MIN_LOG_VECTOR_SIZE { + return Err(general_err!( + "Invalid ALP page: log_vector_size {} below min {}", + header.log_vector_size, + ALP_MIN_LOG_VECTOR_SIZE + )); + } + if header.log_vector_size > ALP_MAX_LOG_VECTOR_SIZE { return Err(general_err!( "Invalid ALP page: log_vector_size {} exceeds max {}", @@ -961,7 +970,7 @@ mod tests { #[test] fn test_parse_alp_page_layout_valid() { - let data = make_alp_page_bytes(0, 0, 2, 4, &[4], 13); + let data = make_alp_page_bytes(0, 0, 3, 4, &[4], 13); let parsed = parse_alp_page_layout::(Bytes::from(data)).unwrap(); assert_eq!(parsed.header.num_elements, 4); assert_eq!(parsed.offsets, vec![4]); @@ -977,18 +986,28 @@ mod tests { } #[test] - fn test_parse_alp_page_layout_invalid_log_vector_size() { - let data = make_alp_page_bytes(0, 0, 17, 1, &[4], 8); + fn test_parse_alp_page_layout_too_small_log_vector_size() { + let data = make_alp_page_bytes(0, 0, 2, 1, &[4], 8); let err = parse_alp_page_layout::(Bytes::from(data)).unwrap_err(); assert!( err.to_string() - .contains("Invalid ALP page: log_vector_size 17 exceeds max 16") + .contains("Invalid ALP page: log_vector_size 2 below min 3") + ); + } + + #[test] + fn test_parse_alp_page_layout_too_big_log_vector_size() { + let data = make_alp_page_bytes(0, 0, 16, 1, &[4], 8); + let err = parse_alp_page_layout::(Bytes::from(data)).unwrap_err(); + assert!( + err.to_string() + .contains("Invalid ALP page: log_vector_size 16 exceeds max 15") ); } #[test] fn test_parse_alp_page_layout_invalid_integer_encoding() { - let data = make_alp_page_bytes(0, 1, 2, 1, &[4], 8); + let data = make_alp_page_bytes(0, 1, 3, 1, &[4], 8); let err = parse_alp_page_layout::(Bytes::from(data)).unwrap_err(); assert!( err.to_string() @@ -998,7 +1017,7 @@ mod tests { #[test] fn test_parse_alp_page_layout_negative_num_elements() { - let data = make_alp_page_bytes(0, 0, 2, -1, &[4], 8); + let data = make_alp_page_bytes(0, 0, 3, -1, &[4], 8); let err = parse_alp_page_layout::(Bytes::from(data)).unwrap_err(); assert!( err.to_string() @@ -1009,7 +1028,7 @@ mod tests { #[test] fn test_parse_alp_page_layout_invalid_exponent_f32() { let vector = make_vector_u32(11, 0, 0, 0, 0, &[], &[], &[]); - let page = make_page_from_vectors(0, 1, &[vector]); + let page = make_page_from_vectors(3, 1, &[vector]); let err = parse_alp_page_layout::(Bytes::from(page)).unwrap_err(); assert!( err.to_string() @@ -1020,7 +1039,7 @@ mod tests { #[test] fn test_parse_alp_page_layout_invalid_factor_f32() { let vector = make_vector_u32(0, 11, 0, 0, 0, &[], &[], &[]); - let page = make_page_from_vectors(0, 1, &[vector]); + let page = make_page_from_vectors(3, 1, &[vector]); let err = parse_alp_page_layout::(Bytes::from(page)).unwrap_err(); assert!( err.to_string() @@ -1031,7 +1050,7 @@ mod tests { #[test] fn test_parse_alp_page_layout_factor_exceeds_exponent() { let vector = make_vector_u32(2, 3, 0, 0, 0, &[], &[], &[]); - let page = make_page_from_vectors(0, 1, &[vector]); + let page = make_page_from_vectors(3, 1, &[vector]); let err = parse_alp_page_layout::(Bytes::from(page)).unwrap_err(); assert!( err.to_string() @@ -1042,7 +1061,7 @@ mod tests { #[test] fn test_parse_alp_page_layout_invalid_num_exceptions() { let vector = make_vector_u32(0, 0, 2, 0, 0, &[], &[0, 0], &[0, 0]); - let page = make_page_from_vectors(0, 1, &[vector]); + let page = make_page_from_vectors(3, 1, &[vector]); let err = parse_alp_page_layout::(Bytes::from(page)).unwrap_err(); assert!( err.to_string() @@ -1053,7 +1072,7 @@ mod tests { #[test] fn test_parse_alp_page_layout_invalid_exception_position() { let vector = make_vector_u32(0, 0, 1, 0, 0, &[], &[1], &[123]); - let page = make_page_from_vectors(0, 1, &[vector]); + let page = make_page_from_vectors(3, 1, &[vector]); let err = parse_alp_page_layout::(Bytes::from(page)).unwrap_err(); assert!( err.to_string().contains( @@ -1064,7 +1083,7 @@ mod tests { #[test] fn test_parse_alp_page_layout_non_monotonic_offsets() { - let data = make_alp_page_bytes(0, 0, 1, 3, &[12, 8], 12); + let data = make_alp_page_bytes(0, 0, 3, 9, &[12, 8], 12); let err = parse_alp_page_layout::(Bytes::from(data)).unwrap_err(); assert!( err.to_string() @@ -1080,7 +1099,7 @@ mod tests { vector.extend_from_slice(&0u16.to_le_bytes()); vector.extend_from_slice(&10u32.to_le_bytes()); vector.push(1); - let page = make_page_from_vectors(1, 2, &[vector]); + let page = make_page_from_vectors(3, 2, &[vector]); let err = parse_alp_page_layout::(Bytes::from(page)).unwrap_err(); assert!( err.to_string() @@ -1103,7 +1122,7 @@ mod tests { vector.extend_from_slice(&42.5_f64.to_le_bytes()); let offsets = [4u32]; - let mut page = make_alp_page_bytes(0, 0, 0, 1, &offsets, 0); + let mut page = make_alp_page_bytes(0, 0, 3, 1, &offsets, 0); page.extend_from_slice(&vector); let parsed = parse_alp_page_layout::(Bytes::from(page)).unwrap(); @@ -1146,7 +1165,7 @@ mod tests { #[test] fn test_decode_page_values_f32_no_exceptions() { let vector = make_vector_u32(0, 0, 0, 10, 2, &[0b1110_0100], &[], &[]); - let page = make_page_from_vectors(2, 4, &[vector]); + let page = make_page_from_vectors(3, 4, &[vector]); let layout = parse_alp_page_layout::(Bytes::from(page)).unwrap(); let decoded = decode_page_values::(&layout).unwrap(); assert_eq!(decoded, vec![10.0, 11.0, 12.0, 13.0]); @@ -1156,10 +1175,13 @@ mod tests { fn test_decode_page_values_f64_multi_vector_with_exceptions() { let vector0 = make_vector_u64(0, 0, 1, 10, 1, &[0b0000_0010], &[1], &[42.5f64.to_bits()]); let vector1 = make_vector_u64(0, 0, 0, 7, 0, &[], &[], &[]); - let page = make_page_from_vectors(1, 3, &[vector0, vector1]); + let page = make_page_from_vectors(3, 9, &[vector0, vector1]); let layout = parse_alp_page_layout::(Bytes::from(page)).unwrap(); let decoded = decode_page_values::(&layout).unwrap(); - assert_eq!(decoded, vec![10.0, 42.5, 7.0]); + assert_eq!( + decoded, + vec![10.0, 42.5, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 7.0] + ); } #[test] @@ -1170,7 +1192,7 @@ mod tests { f32::INFINITY.to_bits(), ]; let vector = make_vector_u32(0, 0, 3, 0, 0, &[], &[0, 1, 2], &edge_values); - let page = make_page_from_vectors(2, 3, &[vector]); + let page = make_page_from_vectors(3, 3, &[vector]); let layout = parse_alp_page_layout::(Bytes::from(page)).unwrap(); let decoded = decode_page_values::(&layout).unwrap(); @@ -1184,21 +1206,24 @@ mod tests { fn test_alp_decoder_get_across_vectors() { let vector0 = make_vector_u32(0, 0, 0, 10, 1, &[0b0000_0010], &[], &[]); let vector1 = make_vector_u32(0, 0, 0, 20, 1, &[0b0000_0010], &[], &[]); - let page = make_page_from_vectors(1, 4, &[vector0, vector1]); + let page = make_page_from_vectors(3, 12, &[vector0, vector1]); let mut decoder = AlpDecoder::::new(); - decoder.set_data(Bytes::from(page), 4).unwrap(); + decoder.set_data(Bytes::from(page), 12).unwrap(); - let mut first = [0.0f32; 3]; + let mut first = [0.0f32; 9]; let read = decoder.get(&mut first).unwrap(); - assert_eq!(read, 3); - assert_eq!(first, [10.0, 11.0, 20.0]); - assert_eq!(decoder.values_left(), 1); + assert_eq!(read, 9); + assert_eq!( + first, + [10.0, 11.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 20.0] + ); + assert_eq!(decoder.values_left(), 3); - let mut second = [0.0f32; 2]; + let mut second = [0.0f32; 3]; let read = decoder.get(&mut second).unwrap(); - assert_eq!(read, 1); - assert_eq!(second[0], 21.0); + assert_eq!(read, 3); + assert_eq!(second, [21.0, 20.0, 20.0]); assert_eq!(decoder.values_left(), 0); } @@ -1206,35 +1231,40 @@ mod tests { fn test_alp_decoder_skip_across_vectors() { let vector0 = make_vector_u32(0, 0, 0, 10, 1, &[0b0000_0010], &[], &[]); let vector1 = make_vector_u32(0, 0, 0, 20, 1, &[0b0000_0010], &[], &[]); - let page = make_page_from_vectors(1, 4, &[vector0, vector1]); + let page = make_page_from_vectors(3, 12, &[vector0, vector1]); let mut decoder = AlpDecoder::::new(); - decoder.set_data(Bytes::from(page), 4).unwrap(); + decoder.set_data(Bytes::from(page), 12).unwrap(); - let skipped = decoder.skip(3).unwrap(); - assert_eq!(skipped, 3); - assert_eq!(decoder.values_left(), 1); + let skipped = decoder.skip(9).unwrap(); + assert_eq!(skipped, 9); + assert_eq!(decoder.values_left(), 3); let mut out = [0.0f32; 1]; let read = decoder.get(&mut out).unwrap(); assert_eq!(read, 1); assert_eq!(out[0], 21.0); - assert_eq!(decoder.values_left(), 0); + assert_eq!(decoder.values_left(), 2); } #[test] fn test_alp_decoder_get_fast_path_full_read() { let vector0 = make_vector_u32(0, 0, 0, 10, 1, &[0b0000_0010], &[], &[]); let vector1 = make_vector_u32(0, 0, 0, 20, 1, &[0b0000_0010], &[], &[]); - let page = make_page_from_vectors(1, 4, &[vector0, vector1]); + let page = make_page_from_vectors(3, 12, &[vector0, vector1]); let mut decoder = AlpDecoder::::new(); - decoder.set_data(Bytes::from(page), 4).unwrap(); + decoder.set_data(Bytes::from(page), 12).unwrap(); - let mut out = [0.0f32; 4]; + let mut out = [0.0f32; 12]; let read = decoder.get(&mut out).unwrap(); - assert_eq!(read, 4); - assert_eq!(out, [10.0, 11.0, 20.0, 21.0]); + assert_eq!(read, 12); + assert_eq!( + out, + [ + 10.0, 11.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 20.0, 21.0, 20.0, 20.0 + ] + ); assert_eq!(decoder.values_left(), 0); assert!(!decoder.needs_decode); assert!(decoder.decoded_values.is_empty()); From 6e86d6f2574b49ee25d0a84e8d0a15b32bed9e47 Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Thu, 12 Mar 2026 16:08:26 -0400 Subject: [PATCH 14/50] redo the integration test using a test file from the c++ pr --- parquet-testing | 2 +- parquet/tests/arrow_reader/alp.rs | 129 +++++++++++++++++++++++++----- 2 files changed, 108 insertions(+), 23 deletions(-) diff --git a/parquet-testing b/parquet-testing index abd2478aea41..b49107d5e742 160000 --- a/parquet-testing +++ b/parquet-testing @@ -1 +1 @@ -Subproject commit abd2478aea41d6cb10352b2277a7dc67ae20a02a +Subproject commit b49107d5e7426ccd657965dbf85fcac430812d64 diff --git a/parquet/tests/arrow_reader/alp.rs b/parquet/tests/arrow_reader/alp.rs index a74f32693539..93f36c1aaffb 100644 --- a/parquet/tests/arrow_reader/alp.rs +++ b/parquet/tests/arrow_reader/alp.rs @@ -15,38 +15,123 @@ // specific language governing permissions and limitations // under the License. +use arrow::compute::concat_batches; use arrow::util::test_util::parquet_test_data; use arrow_array::cast::as_primitive_array; -use arrow_array::types::Float64Type; +use arrow_array::types::Float32Type; +use arrow_array::{Array, ArrayRef, Float32Array, RecordBatch}; +use arrow_schema::{DataType, Field, Schema}; use parquet::arrow::arrow_reader::ArrowReaderBuilder; +use std::fs::File; +use std::io::{BufRead, BufReader}; +use std::path::PathBuf; +use std::sync::Arc; #[test] -fn test_read_single_f64_alp() { - let path = std::path::PathBuf::from(parquet_test_data()).join("single_f64_ALP.parquet"); - if !path.exists() { - eprintln!("Skipping ALP test file not found: {}", path.display()); +fn test_read_f32_alp() { + let data_dir = PathBuf::from(parquet_test_data()); + let parquet_path = data_dir.join("alp_float_arade.parquet"); + let expected_csv_path = data_dir.join("alp_arade_expect.csv"); + if !parquet_path.exists() || !expected_csv_path.exists() { + eprintln!("Skipping ALP test files not found"); return; } - let file = std::fs::File::open(path).unwrap(); - let mut reader = ArrowReaderBuilder::try_new(file).unwrap().build().unwrap(); - - let mut total_rows = 0usize; - let mut first_value = None; - loop { - match reader.next() { - Some(Ok(batch)) => { - total_rows += batch.num_rows(); - if first_value.is_none() && batch.num_rows() > 0 { - let values = as_primitive_array::(batch.column(0).as_ref()); - first_value = Some(values.value(0)); - } + let expected = read_expected_csv_batch(&expected_csv_path); + let actual = read_parquet_batch(&parquet_path); + + assert_eq!(actual.schema(), expected.schema(), "schema mismatch"); + assert_eq!( + actual.num_columns(), + expected.num_columns(), + "column mismatch" + ); + assert_eq!(actual.num_rows(), expected.num_rows(), "row count mismatch"); + + for col_idx in 0..actual.num_columns() { + let col_name = actual.schema().field(col_idx).name().clone(); + let actual_col = as_primitive_array::(actual.column(col_idx).as_ref()); + let expected_col = as_primitive_array::(expected.column(col_idx).as_ref()); + + for row_idx in 0..actual.num_rows() { + assert_eq!( + actual_col.is_valid(row_idx), + expected_col.is_valid(row_idx), + "null mismatch at column {col_name} row {row_idx}" + ); + if actual_col.is_valid(row_idx) { + let actual_value = actual_col.value(row_idx); + let expected_value = expected_col.value(row_idx); + assert!( + actual_value.to_bits() == expected_value.to_bits(), + "bit mismatch at column {col_name} row {row_idx}: expected={expected_value} actual={actual_value}" + ); } - Some(Err(err)) => panic!("Unexpected ALP decode error: {err}"), - None => break, } } +} + +fn alp_schema() -> Arc { + Arc::new(Schema::new(vec![ + Field::new("value1", DataType::Float32, true), + Field::new("value2", DataType::Float32, true), + Field::new("value3", DataType::Float32, true), + Field::new("value4", DataType::Float32, true), + ])) +} + +fn read_parquet_batch(path: &PathBuf) -> RecordBatch { + let file = File::open(path).unwrap(); + let reader = ArrowReaderBuilder::try_new(file).unwrap().build().unwrap(); + let mut batches = Vec::new(); + for batch in reader { + batches.push(batch.unwrap()); + } + assert!(!batches.is_empty(), "expected non-empty parquet batch set"); + concat_batches(batches[0].schema_ref(), &batches).unwrap() +} + +fn read_expected_csv_batch(path: &PathBuf) -> RecordBatch { + let file = File::open(path).unwrap(); + let mut lines = BufReader::new(file).lines(); + + let header = lines.next().expect("expected csv header line").unwrap(); + assert_eq!( + header.trim(), + "value1,value2,value3,value4", + "unexpected csv header" + ); + + let mut c0 = Vec::new(); + let mut c1 = Vec::new(); + let mut c2 = Vec::new(); + let mut c3 = Vec::new(); + for (line_idx, line) in lines.enumerate() { + let line = line.unwrap(); + if line.trim().is_empty() { + continue; + } + let values: Vec<_> = line.split(',').map(str::trim).collect(); + assert_eq!( + values.len(), + 4, + "wrong csv column count at line {}", + line_idx + 2 + ); + c0.push(values[0].parse::().unwrap()); + c1.push(values[1].parse::().unwrap()); + c2.push(values[2].parse::().unwrap()); + c3.push(values[3].parse::().unwrap()); + } - assert_eq!(total_rows, 1024); - assert_eq!(first_value, Some(0.125)); + RecordBatch::try_new( + alp_schema(), + vec![ + Arc::new(Float32Array::from(c0)) as ArrayRef, + Arc::new(Float32Array::from(c1)) as ArrayRef, + Arc::new(Float32Array::from(c2)) as ArrayRef, + Arc::new(Float32Array::from(c3)) as ArrayRef, + ], + ) + .unwrap() } From ac56cc28c4c5df2989f09dbe7ff936e576f7fcc0 Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Thu, 12 Mar 2026 16:23:16 -0400 Subject: [PATCH 15/50] cache parsed vectors --- parquet/src/encodings/decoding/alp.rs | 54 +++++++++------------------ 1 file changed, 18 insertions(+), 36 deletions(-) diff --git a/parquet/src/encodings/decoding/alp.rs b/parquet/src/encodings/decoding/alp.rs index 94b53ca03474..d69496688fda 100644 --- a/parquet/src/encodings/decoding/alp.rs +++ b/parquet/src/encodings/decoding/alp.rs @@ -133,10 +133,10 @@ struct AlpEncodedVectorView { /// Parsed ALP page layout for one exact integer width (`u32` for float pages, /// `u64` for double pages). #[derive(Debug)] -struct AlpPageLayout { +struct AlpPageLayout { header: AlpHeader, body: Bytes, - offsets: Vec, + vectors: Vec>, } /// Exact integer type used by FOR reconstruction. @@ -318,7 +318,7 @@ impl AlpFloat for f64 { /// - non-negative `num_elements` /// - offsets bounds + monotonicity /// - per-vector metadata/data section lengths -fn parse_alp_page_layout(data: Bytes) -> Result { +fn parse_alp_page_layout(data: Bytes) -> Result> { let data_ref = data.as_ref(); if data_ref.len() < ALP_HEADER_SIZE { return Err(general_err!( @@ -424,6 +424,7 @@ fn parse_alp_page_layout(data: Bytes) -> Result offsets.push(offset); } + let mut vectors = Vec::with_capacity(num_vectors); for (vector_idx, vector_offset) in offsets.iter().enumerate() { let vector_start = *vector_offset as usize; let vector_end = if vector_idx + 1 < offsets.len() { @@ -440,13 +441,15 @@ fn parse_alp_page_layout(data: Bytes) -> Result } let vector_num_elements = header.vector_num_elements(vector_idx); - parse_vector_view::(body_ref, vector_start, vector_end, vector_num_elements)?; + let vector = + parse_vector_view::(body_ref, vector_start, vector_end, vector_num_elements)?; + vectors.push(vector); } Ok(AlpPageLayout { header, body, - offsets, + vectors, }) } @@ -663,7 +666,7 @@ fn decode_vector_values( Ok(out) } -fn decode_page_values(layout: &AlpPageLayout) -> Result> { +fn decode_page_values(layout: &AlpPageLayout) -> Result> { let total = layout.header.num_elements_usize(); let mut out = vec![Value::default(); total]; decode_page_values_into::(layout, &mut out)?; @@ -672,10 +675,9 @@ fn decode_page_values(layout: &AlpPageLayout) -> Result( - layout: &AlpPageLayout, + layout: &AlpPageLayout, out: &mut [Value], ) -> Result<()> { let total = layout.header.num_elements_usize(); @@ -688,21 +690,8 @@ fn decode_page_values_into( } let mut output_offset = 0usize; - for (vector_idx, vector_offset) in layout.offsets.iter().enumerate() { - let vector_start = *vector_offset as usize; - let vector_end = if vector_idx + 1 < layout.offsets.len() { - layout.offsets[vector_idx + 1] as usize - } else { - layout.body.len() - }; - let vector_num_elements = layout.header.vector_num_elements(vector_idx); - let vector = parse_vector_view::( - layout.body.as_ref(), - vector_start, - vector_end, - vector_num_elements, - )?; - let vector_values = decode_vector_values::(layout.body.as_ref(), &vector)?; + for vector in &layout.vectors { + let vector_values = decode_vector_values::(layout.body.as_ref(), vector)?; let next_offset = output_offset + vector_values.len(); out[output_offset..next_offset].copy_from_slice(&vector_values); output_offset = next_offset; @@ -729,7 +718,7 @@ where T::T: AlpFloat, ::Exact: Send, { - layout: Option, + layout: Option::Exact>>, decoded_values: Vec, current_offset: usize, needs_decode: bool, @@ -973,7 +962,8 @@ mod tests { let data = make_alp_page_bytes(0, 0, 3, 4, &[4], 13); let parsed = parse_alp_page_layout::(Bytes::from(data)).unwrap(); assert_eq!(parsed.header.num_elements, 4); - assert_eq!(parsed.offsets, vec![4]); + assert_eq!(parsed.vectors.len(), 1); + assert_eq!(parsed.vectors[0].num_elements, 4); } #[test] @@ -1126,16 +1116,8 @@ mod tests { page.extend_from_slice(&vector); let parsed = parse_alp_page_layout::(Bytes::from(page)).unwrap(); - assert_eq!(parsed.offsets, vec![4]); - let vector_start = parsed.offsets[0] as usize; - let vector_end = parsed.body.len(); - let parsed_vector = parse_vector_view::( - parsed.body.as_ref(), - vector_start, - vector_end, - parsed.header.vector_num_elements(0), - ) - .unwrap(); + assert_eq!(parsed.vectors.len(), 1); + let parsed_vector = &parsed.vectors[0]; assert_eq!(parsed_vector.num_elements, 1); assert_eq!(parsed_vector.alp_info.num_exceptions, 1); assert_eq!(parsed_vector.for_info.bit_width, 0); From 07ca6f06471c95a5198e4bf3be06d47fa2b88a4a Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Thu, 12 Mar 2026 16:29:58 -0400 Subject: [PATCH 16/50] use `Bytes` for `packed_values` --- parquet/src/encodings/decoding/alp.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/parquet/src/encodings/decoding/alp.rs b/parquet/src/encodings/decoding/alp.rs index d69496688fda..c56c393696bf 100644 --- a/parquet/src/encodings/decoding/alp.rs +++ b/parquet/src/encodings/decoding/alp.rs @@ -581,7 +581,7 @@ fn parse_vector_view( /// Decode bit-packed deltas into exact integers. fn bit_unpack_integers( - packed_values: &[u8], + packed_values: Bytes, bit_width: u8, num_elements: u16, ) -> Result> { @@ -598,7 +598,7 @@ fn bit_unpack_integers( } let mut out = vec![Exact::zero(); num_elements as usize]; - let mut reader = BitReader::new(Bytes::copy_from_slice(packed_values)); + let mut reader = BitReader::new(packed_values); let read = reader.get_batch::(&mut out, bit_width as usize); if read != out.len() { return Err(general_err!( @@ -621,11 +621,11 @@ fn inverse_for(deltas: &mut [Exact], frame_of_reference: Exact) /// Decode one vector into output floating values: /// bit-unpack -> inverse FOR -> decimal decode -> patch exceptions. fn decode_vector_values( - body: &[u8], + body: &Bytes, vector: &AlpEncodedVectorView, ) -> Result> { let mut exact_values = bit_unpack_integers( - &body[vector.packed_values.clone()], + body.slice(vector.packed_values.clone()), vector.for_info.bit_width, vector.num_elements, )?; @@ -691,7 +691,7 @@ fn decode_page_values_into( let mut output_offset = 0usize; for vector in &layout.vectors { - let vector_values = decode_vector_values::(layout.body.as_ref(), vector)?; + let vector_values = decode_vector_values::(&layout.body, vector)?; let next_offset = output_offset + vector_values.len(); out[output_offset..next_offset].copy_from_slice(&vector_values); output_offset = next_offset; @@ -1127,13 +1127,13 @@ mod tests { #[test] fn test_bit_unpack_integers_width_zero() { - let unpacked = bit_unpack_integers::(&[], 0, 3).unwrap(); + let unpacked = bit_unpack_integers::(Bytes::new(), 0, 3).unwrap(); assert_eq!(unpacked, vec![0, 0, 0]); } #[test] fn test_bit_unpack_integers_width_two() { - let unpacked = bit_unpack_integers::(&[0b0010_0111], 2, 3).unwrap(); + let unpacked = bit_unpack_integers::(Bytes::from_static(&[0b0010_0111]), 2, 3).unwrap(); assert_eq!(unpacked, vec![3, 1, 2]); } From def5c095866d3d687708817e97333985bfd34dba Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Thu, 12 Mar 2026 16:33:52 -0400 Subject: [PATCH 17/50] clippy too many args --- parquet/src/encodings/decoding/alp.rs | 223 +++++++++++++++++++------- 1 file changed, 166 insertions(+), 57 deletions(-) diff --git a/parquet/src/encodings/decoding/alp.rs b/parquet/src/encodings/decoding/alp.rs index c56c393696bf..6740d34aed2a 100644 --- a/parquet/src/encodings/decoding/alp.rs +++ b/parquet/src/encodings/decoding/alp.rs @@ -871,60 +871,49 @@ mod tests { out } - fn make_vector_u32( - exponent: u8, - factor: u8, - num_exceptions: u16, - frame_of_reference: u32, - bit_width: u8, - packed_values: &[u8], - exception_positions: &[u16], - exception_values: &[u32], - ) -> Vec { - assert_eq!(num_exceptions as usize, exception_positions.len()); - assert_eq!(num_exceptions as usize, exception_values.len()); + trait AppendLeBytes { + fn append_le_bytes(self, out: &mut Vec); + } - let mut out = Vec::new(); - out.push(exponent); - out.push(factor); - out.extend_from_slice(&num_exceptions.to_le_bytes()); - out.extend_from_slice(&frame_of_reference.to_le_bytes()); - out.push(bit_width); - out.extend_from_slice(packed_values); - for position in exception_positions { - out.extend_from_slice(&position.to_le_bytes()); + impl AppendLeBytes for u32 { + fn append_le_bytes(self, out: &mut Vec) { + out.extend_from_slice(&self.to_le_bytes()); } - for value in exception_values { - out.extend_from_slice(&value.to_le_bytes()); + } + + impl AppendLeBytes for u64 { + fn append_le_bytes(self, out: &mut Vec) { + out.extend_from_slice(&self.to_le_bytes()); } - out } - fn make_vector_u64( + struct VectorSpec<'a, Exact> { exponent: u8, factor: u8, - num_exceptions: u16, - frame_of_reference: u64, + frame_of_reference: Exact, bit_width: u8, - packed_values: &[u8], - exception_positions: &[u16], - exception_values: &[u64], - ) -> Vec { - assert_eq!(num_exceptions as usize, exception_positions.len()); - assert_eq!(num_exceptions as usize, exception_values.len()); + packed_values: &'a [u8], + exception_positions: &'a [u16], + exception_values: &'a [Exact], + } + + fn make_vector(spec: VectorSpec<'_, Exact>) -> Vec { + let num_exceptions = spec.exception_positions.len(); + assert_eq!(num_exceptions, spec.exception_values.len()); + assert!(u16::try_from(num_exceptions).is_ok()); let mut out = Vec::new(); - out.push(exponent); - out.push(factor); - out.extend_from_slice(&num_exceptions.to_le_bytes()); - out.extend_from_slice(&frame_of_reference.to_le_bytes()); - out.push(bit_width); - out.extend_from_slice(packed_values); - for position in exception_positions { + out.push(spec.exponent); + out.push(spec.factor); + out.extend_from_slice(&(num_exceptions as u16).to_le_bytes()); + spec.frame_of_reference.append_le_bytes(&mut out); + out.push(spec.bit_width); + out.extend_from_slice(spec.packed_values); + for position in spec.exception_positions { out.extend_from_slice(&position.to_le_bytes()); } - for value in exception_values { - out.extend_from_slice(&value.to_le_bytes()); + for value in spec.exception_values { + value.append_le_bytes(&mut out); } out } @@ -1017,7 +1006,15 @@ mod tests { #[test] fn test_parse_alp_page_layout_invalid_exponent_f32() { - let vector = make_vector_u32(11, 0, 0, 0, 0, &[], &[], &[]); + let vector = make_vector(VectorSpec { + exponent: 11, + factor: 0, + frame_of_reference: 0u32, + bit_width: 0, + packed_values: &[], + exception_positions: &[], + exception_values: &[], + }); let page = make_page_from_vectors(3, 1, &[vector]); let err = parse_alp_page_layout::(Bytes::from(page)).unwrap_err(); assert!( @@ -1028,7 +1025,15 @@ mod tests { #[test] fn test_parse_alp_page_layout_invalid_factor_f32() { - let vector = make_vector_u32(0, 11, 0, 0, 0, &[], &[], &[]); + let vector = make_vector(VectorSpec { + exponent: 0, + factor: 11, + frame_of_reference: 0u32, + bit_width: 0, + packed_values: &[], + exception_positions: &[], + exception_values: &[], + }); let page = make_page_from_vectors(3, 1, &[vector]); let err = parse_alp_page_layout::(Bytes::from(page)).unwrap_err(); assert!( @@ -1039,7 +1044,15 @@ mod tests { #[test] fn test_parse_alp_page_layout_factor_exceeds_exponent() { - let vector = make_vector_u32(2, 3, 0, 0, 0, &[], &[], &[]); + let vector = make_vector(VectorSpec { + exponent: 2, + factor: 3, + frame_of_reference: 0u32, + bit_width: 0, + packed_values: &[], + exception_positions: &[], + exception_values: &[], + }); let page = make_page_from_vectors(3, 1, &[vector]); let err = parse_alp_page_layout::(Bytes::from(page)).unwrap_err(); assert!( @@ -1050,7 +1063,15 @@ mod tests { #[test] fn test_parse_alp_page_layout_invalid_num_exceptions() { - let vector = make_vector_u32(0, 0, 2, 0, 0, &[], &[0, 0], &[0, 0]); + let vector = make_vector(VectorSpec { + exponent: 0, + factor: 0, + frame_of_reference: 0u32, + bit_width: 0, + packed_values: &[], + exception_positions: &[0, 0], + exception_values: &[0, 0], + }); let page = make_page_from_vectors(3, 1, &[vector]); let err = parse_alp_page_layout::(Bytes::from(page)).unwrap_err(); assert!( @@ -1061,7 +1082,15 @@ mod tests { #[test] fn test_parse_alp_page_layout_invalid_exception_position() { - let vector = make_vector_u32(0, 0, 1, 0, 0, &[], &[1], &[123]); + let vector = make_vector(VectorSpec { + exponent: 0, + factor: 0, + frame_of_reference: 0u32, + bit_width: 0, + packed_values: &[], + exception_positions: &[1], + exception_values: &[123], + }); let page = make_page_from_vectors(3, 1, &[vector]); let err = parse_alp_page_layout::(Bytes::from(page)).unwrap_err(); assert!( @@ -1146,7 +1175,15 @@ mod tests { #[test] fn test_decode_page_values_f32_no_exceptions() { - let vector = make_vector_u32(0, 0, 0, 10, 2, &[0b1110_0100], &[], &[]); + let vector = make_vector(VectorSpec { + exponent: 0, + factor: 0, + frame_of_reference: 10u32, + bit_width: 2, + packed_values: &[0b1110_0100], + exception_positions: &[], + exception_values: &[], + }); let page = make_page_from_vectors(3, 4, &[vector]); let layout = parse_alp_page_layout::(Bytes::from(page)).unwrap(); let decoded = decode_page_values::(&layout).unwrap(); @@ -1155,8 +1192,24 @@ mod tests { #[test] fn test_decode_page_values_f64_multi_vector_with_exceptions() { - let vector0 = make_vector_u64(0, 0, 1, 10, 1, &[0b0000_0010], &[1], &[42.5f64.to_bits()]); - let vector1 = make_vector_u64(0, 0, 0, 7, 0, &[], &[], &[]); + let vector0 = make_vector(VectorSpec { + exponent: 0, + factor: 0, + frame_of_reference: 10u64, + bit_width: 1, + packed_values: &[0b0000_0010], + exception_positions: &[1], + exception_values: &[42.5f64.to_bits()], + }); + let vector1 = make_vector(VectorSpec { + exponent: 0, + factor: 0, + frame_of_reference: 7u64, + bit_width: 0, + packed_values: &[], + exception_positions: &[], + exception_values: &[], + }); let page = make_page_from_vectors(3, 9, &[vector0, vector1]); let layout = parse_alp_page_layout::(Bytes::from(page)).unwrap(); let decoded = decode_page_values::(&layout).unwrap(); @@ -1173,7 +1226,15 @@ mod tests { (-0.0f32).to_bits(), f32::INFINITY.to_bits(), ]; - let vector = make_vector_u32(0, 0, 3, 0, 0, &[], &[0, 1, 2], &edge_values); + let vector = make_vector(VectorSpec { + exponent: 0, + factor: 0, + frame_of_reference: 0u32, + bit_width: 0, + packed_values: &[], + exception_positions: &[0, 1, 2], + exception_values: &edge_values, + }); let page = make_page_from_vectors(3, 3, &[vector]); let layout = parse_alp_page_layout::(Bytes::from(page)).unwrap(); let decoded = decode_page_values::(&layout).unwrap(); @@ -1186,8 +1247,24 @@ mod tests { #[test] fn test_alp_decoder_get_across_vectors() { - let vector0 = make_vector_u32(0, 0, 0, 10, 1, &[0b0000_0010], &[], &[]); - let vector1 = make_vector_u32(0, 0, 0, 20, 1, &[0b0000_0010], &[], &[]); + let vector0 = make_vector(VectorSpec { + exponent: 0, + factor: 0, + frame_of_reference: 10u32, + bit_width: 1, + packed_values: &[0b0000_0010], + exception_positions: &[], + exception_values: &[], + }); + let vector1 = make_vector(VectorSpec { + exponent: 0, + factor: 0, + frame_of_reference: 20u32, + bit_width: 1, + packed_values: &[0b0000_0010], + exception_positions: &[], + exception_values: &[], + }); let page = make_page_from_vectors(3, 12, &[vector0, vector1]); let mut decoder = AlpDecoder::::new(); @@ -1211,8 +1288,24 @@ mod tests { #[test] fn test_alp_decoder_skip_across_vectors() { - let vector0 = make_vector_u32(0, 0, 0, 10, 1, &[0b0000_0010], &[], &[]); - let vector1 = make_vector_u32(0, 0, 0, 20, 1, &[0b0000_0010], &[], &[]); + let vector0 = make_vector(VectorSpec { + exponent: 0, + factor: 0, + frame_of_reference: 10u32, + bit_width: 1, + packed_values: &[0b0000_0010], + exception_positions: &[], + exception_values: &[], + }); + let vector1 = make_vector(VectorSpec { + exponent: 0, + factor: 0, + frame_of_reference: 20u32, + bit_width: 1, + packed_values: &[0b0000_0010], + exception_positions: &[], + exception_values: &[], + }); let page = make_page_from_vectors(3, 12, &[vector0, vector1]); let mut decoder = AlpDecoder::::new(); @@ -1231,8 +1324,24 @@ mod tests { #[test] fn test_alp_decoder_get_fast_path_full_read() { - let vector0 = make_vector_u32(0, 0, 0, 10, 1, &[0b0000_0010], &[], &[]); - let vector1 = make_vector_u32(0, 0, 0, 20, 1, &[0b0000_0010], &[], &[]); + let vector0 = make_vector(VectorSpec { + exponent: 0, + factor: 0, + frame_of_reference: 10u32, + bit_width: 1, + packed_values: &[0b0000_0010], + exception_positions: &[], + exception_values: &[], + }); + let vector1 = make_vector(VectorSpec { + exponent: 0, + factor: 0, + frame_of_reference: 20u32, + bit_width: 1, + packed_values: &[0b0000_0010], + exception_positions: &[], + exception_values: &[], + }); let page = make_page_from_vectors(3, 12, &[vector0, vector1]); let mut decoder = AlpDecoder::::new(); From 5e5c8d242eb4b801cec96e60a6a543c5a54427b8 Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Thu, 12 Mar 2026 19:25:11 -0400 Subject: [PATCH 18/50] cargo fmt --- parquet/src/encodings/decoding.rs | 2 +- parquet/src/encodings/decoding/alp.rs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/parquet/src/encodings/decoding.rs b/parquet/src/encodings/decoding.rs index 36c7f6720104..dce631930975 100644 --- a/parquet/src/encodings/decoding.rs +++ b/parquet/src/encodings/decoding.rs @@ -26,10 +26,10 @@ use super::rle::RleDecoder; use crate::basic::*; use crate::data_type::private::ParquetValueType; use crate::data_type::*; +use crate::encodings::decoding::alp::AlpDecoder; use crate::encodings::decoding::byte_stream_split_decoder::{ ByteStreamSplitDecoder, VariableWidthByteStreamSplitDecoder, }; -use crate::encodings::decoding::alp::AlpDecoder; use crate::errors::{ParquetError, Result}; use crate::schema::types::ColumnDescPtr; use crate::util::bit_util::{self, BitReader}; diff --git a/parquet/src/encodings/decoding/alp.rs b/parquet/src/encodings/decoding/alp.rs index 6740d34aed2a..b5f507e341f9 100644 --- a/parquet/src/encodings/decoding/alp.rs +++ b/parquet/src/encodings/decoding/alp.rs @@ -1162,7 +1162,8 @@ mod tests { #[test] fn test_bit_unpack_integers_width_two() { - let unpacked = bit_unpack_integers::(Bytes::from_static(&[0b0010_0111]), 2, 3).unwrap(); + let unpacked = + bit_unpack_integers::(Bytes::from_static(&[0b0010_0111]), 2, 3).unwrap(); assert_eq!(unpacked, vec![3, 1, 2]); } From 5ed70ae96a4e2126dda1104450bca1cdd9932cd6 Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Mon, 16 Mar 2026 09:30:13 -0400 Subject: [PATCH 19/50] fix decode to two-step multiplication --- parquet/src/encodings/decoding/alp.rs | 134 +++++++++++++++++++------- 1 file changed, 101 insertions(+), 33 deletions(-) diff --git a/parquet/src/encodings/decoding/alp.rs b/parquet/src/encodings/decoding/alp.rs index b5f507e341f9..f444d47611cb 100644 --- a/parquet/src/encodings/decoding/alp.rs +++ b/parquet/src/encodings/decoding/alp.rs @@ -202,26 +202,40 @@ impl AlpExact for u64 { } } -const ALP_I64_POW10: [i64; 19] = [ - 1, - 10, - 100, - 1_000, - 10_000, - 100_000, - 1_000_000, - 10_000_000, - 100_000_000, - 1_000_000_000, - 10_000_000_000, - 100_000_000_000, - 1_000_000_000_000, - 10_000_000_000_000, - 100_000_000_000_000, - 1_000_000_000_000_000, - 10_000_000_000_000_000, - 100_000_000_000_000_000, - 1_000_000_000_000_000_000, +const ALP_POW10_F32: [f32; 11] = [ + 1.0, + 10.0, + 100.0, + 1000.0, + 10000.0, + 100000.0, + 1000000.0, + 10000000.0, + 100000000.0, + 1000000000.0, + 10000000000.0, +]; + +const ALP_POW10_F64: [f64; 19] = [ + 1.0, + 10.0, + 100.0, + 1000.0, + 10000.0, + 100000.0, + 1000000.0, + 10000000.0, + 100000000.0, + 1000000000.0, + 10000000000.0, + 100000000000.0, + 1000000000000.0, + 10000000000000.0, + 100000000000000.0, + 1000000000000000.0, + 10000000000000000.0, + 100000000000000000.0, + 1000000000000000000.0, ]; const ALP_NEG_POW10_F32: [f32; 11] = [ @@ -262,30 +276,35 @@ const ALP_NEG_POW10_F64: [f64; 19] = [ pub(super) trait AlpFloat: Copy + Default { type Exact: AlpExact + FromBytes; + type Scale: Copy; - /// Precompute vector-level ALP decimal scale for: - /// `value = encoded * 10^(factor) * 10^(-exponent)`. + /// Precompute vector-level ALP decimal scale constants for: + /// `value = (encoded * 10^(factor)) * 10^(-exponent)`. /// /// Preconditions are validated during page parse. - fn decode_scale(exponent: u8, factor: u8) -> Self; + fn decode_scale(exponent: u8, factor: u8) -> Self::Scale; - /// Decode one signed exact integer using a precomputed scale. - fn decode_value(signed_encoded: ::Signed, scale: Self) -> Self; + /// Decode one signed exact integer using a precomputed two-step scale. + fn decode_value( + signed_encoded: ::Signed, + scale: Self::Scale, + ) -> Self; fn from_exact_bits(bits: Self::Exact) -> Self; } impl AlpFloat for f32 { type Exact = u32; + type Scale = (f32, f32); - fn decode_scale(exponent: u8, factor: u8) -> Self { + fn decode_scale(exponent: u8, factor: u8) -> Self::Scale { debug_assert!(exponent <= ALP_MAX_EXPONENT_F32); debug_assert!(factor <= exponent); - (ALP_I64_POW10[factor as usize] as f32) * ALP_NEG_POW10_F32[exponent as usize] + (ALP_POW10_F32[factor as usize], ALP_NEG_POW10_F32[exponent as usize]) } - fn decode_value(signed_encoded: i32, scale: Self) -> Self { - (signed_encoded as f32) * scale + fn decode_value(signed_encoded: i32, scale: Self::Scale) -> Self { + ((signed_encoded as f32) * scale.0) * scale.1 } fn from_exact_bits(bits: Self::Exact) -> Self { @@ -295,15 +314,16 @@ impl AlpFloat for f32 { impl AlpFloat for f64 { type Exact = u64; + type Scale = (f64, f64); - fn decode_scale(exponent: u8, factor: u8) -> Self { + fn decode_scale(exponent: u8, factor: u8) -> Self::Scale { debug_assert!(exponent <= ALP_MAX_EXPONENT_F64); debug_assert!(factor <= exponent); - (ALP_I64_POW10[factor as usize] as f64) * ALP_NEG_POW10_F64[exponent as usize] + (ALP_POW10_F64[factor as usize], ALP_NEG_POW10_F64[exponent as usize]) } - fn decode_value(signed_encoded: i64, scale: Self) -> Self { - (signed_encoded as f64) * scale + fn decode_value(signed_encoded: i64, scale: Self::Scale) -> Self { + ((signed_encoded as f64) * scale.0) * scale.1 } fn from_exact_bits(bits: Self::Exact) -> Self { @@ -1246,6 +1266,54 @@ mod tests { assert_eq!(decoded[2], f32::INFINITY); } + #[test] + fn test_decode_page_values_f32_two_step_decimal_multiply() { + let encoded = 1_970_570_984_i32; + let vector = make_vector(VectorSpec { + exponent: 1, + factor: 1, + frame_of_reference: encoded as u32, + bit_width: 0, + packed_values: &[], + exception_positions: &[], + exception_values: &[], + }); + let page = make_page_from_vectors(3, 1, &[vector]); + let layout = parse_alp_page_layout::(Bytes::from(page)).unwrap(); + let decoded = decode_page_values::(&layout).unwrap(); + + let expected_two_step = ((encoded as f32) * ALP_POW10_F32[1]) * ALP_NEG_POW10_F32[1]; + let one_step_scale = ALP_POW10_F32[1] * ALP_NEG_POW10_F32[1]; + let expected_one_step = (encoded as f32) * one_step_scale; + + assert_eq!(decoded[0].to_bits(), expected_two_step.to_bits()); + assert_ne!(decoded[0].to_bits(), expected_one_step.to_bits()); + } + + #[test] + fn test_decode_page_values_f64_two_step_decimal_multiply() { + let encoded = -3_900_047_474_048_127_703_i64; + let vector = make_vector(VectorSpec { + exponent: 1, + factor: 1, + frame_of_reference: encoded as u64, + bit_width: 0, + packed_values: &[], + exception_positions: &[], + exception_values: &[], + }); + let page = make_page_from_vectors(3, 1, &[vector]); + let layout = parse_alp_page_layout::(Bytes::from(page)).unwrap(); + let decoded = decode_page_values::(&layout).unwrap(); + + let expected_two_step = ((encoded as f64) * ALP_POW10_F64[1]) * ALP_NEG_POW10_F64[1]; + let one_step_scale = ALP_POW10_F64[1] * ALP_NEG_POW10_F64[1]; + let expected_one_step = (encoded as f64) * one_step_scale; + + assert_eq!(decoded[0].to_bits(), expected_two_step.to_bits()); + assert_ne!(decoded[0].to_bits(), expected_one_step.to_bits()); + } + #[test] fn test_alp_decoder_get_across_vectors() { let vector0 = make_vector(VectorSpec { From e328d7a8e903e4bc5321b9d74ee9194a83e956c9 Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Mon, 16 Mar 2026 09:47:33 -0400 Subject: [PATCH 20/50] make vector byte count more strict --- parquet/src/encodings/decoding/alp.rs | 86 ++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 3 deletions(-) diff --git a/parquet/src/encodings/decoding/alp.rs b/parquet/src/encodings/decoding/alp.rs index f444d47611cb..b34840b29db7 100644 --- a/parquet/src/encodings/decoding/alp.rs +++ b/parquet/src/encodings/decoding/alp.rs @@ -130,6 +130,16 @@ struct AlpEncodedVectorView { exception_values: Vec, } +impl AlpEncodedVectorView { + fn expected_stored_size(&self) -> usize { + AlpEncodedVectorInfo::STORED_SIZE + + AlpEncodedForVectorInfo::::stored_size() + + self + .for_info + .get_data_stored_size(self.num_elements, self.alp_info.num_exceptions) + } +} + /// Parsed ALP page layout for one exact integer width (`u32` for float pages, /// `u64` for double pages). #[derive(Debug)] @@ -445,6 +455,7 @@ fn parse_alp_page_layout(data: Bytes) -> Result(data: Bytes) -> Result(body_ref, vector_start, vector_end, vector_num_elements)?; + expected_vectors_size = expected_vectors_size + .checked_add(vector.expected_stored_size()) + .ok_or_else(|| general_err!("Invalid ALP page: expected vectors size overflow"))?; vectors.push(vector); } + let expected_body_len = offsets_section_size + .checked_add(expected_vectors_size) + .ok_or_else(|| general_err!("Invalid ALP page: expected body size overflow"))?; + if body_len != expected_body_len { + return Err(general_err!( + "Invalid ALP page: body size {} does not match expected {} (offsets + vectors)", + body_len, + expected_body_len + )); + } + Ok(AlpPageLayout { header, body, @@ -548,15 +573,23 @@ fn parse_vector_view( }; let data_size = for_info.get_data_stored_size(num_elements, alp_info.num_exceptions); - if vector_bytes.len() < metadata_size + data_size { + let expected_size = metadata_size + data_size; + if vector_bytes.len() < expected_size { return Err(general_err!( "Invalid ALP page: vector data too short, expected at least {} bytes, got {}", - metadata_size + data_size, + expected_size, + vector_bytes.len() + )); + } + if vector_bytes.len() > expected_size { + return Err(general_err!( + "Invalid ALP page: vector data too long, expected {} bytes, got {}", + expected_size, vector_bytes.len() )); } - let data = &vector_bytes[metadata_size..metadata_size + data_size]; + let data = &vector_bytes[metadata_size..expected_size]; let packed_size = for_info.get_bit_packed_size(num_elements); let positions_size = alp_info.num_exceptions as usize * std::mem::size_of::(); let values_size = alp_info.num_exceptions as usize * Exact::WIDTH; @@ -1146,6 +1179,53 @@ mod tests { ); } + #[test] + fn test_parse_alp_page_layout_vector_data_too_long() { + let mut vector = make_vector(VectorSpec { + exponent: 0, + factor: 0, + frame_of_reference: 0u32, + bit_width: 0, + packed_values: &[], + exception_positions: &[], + exception_values: &[], + }); + vector.push(0xAB); + + let page = make_page_from_vectors(3, 1, &[vector]); + let err = parse_alp_page_layout::(Bytes::from(page)).unwrap_err(); + assert!( + err.to_string() + .contains("Invalid ALP page: vector data too long, expected 9 bytes, got 10") + ); + } + + #[test] + fn test_parse_alp_page_layout_body_size_mismatch_unclaimed_bytes() { + let vector = make_vector(VectorSpec { + exponent: 0, + factor: 0, + frame_of_reference: 0u32, + bit_width: 0, + packed_values: &[], + exception_positions: &[], + exception_values: &[], + }); + + // offsets section is 4 bytes for one vector, so offset=5 leaves one + // unclaimed byte between offsets and vector data. + let mut page = make_alp_page_bytes(0, 0, 3, 1, &[5], 0); + page.push(0); + page.extend_from_slice(&vector); + + let err = parse_alp_page_layout::(Bytes::from(page)).unwrap_err(); + assert!( + err.to_string().contains( + "Invalid ALP page: body size 14 does not match expected 13 (offsets + vectors)" + ) + ); + } + #[test] fn test_parse_alp_page_layout_parses_vector_view_data_only_f64() { let mut vector = Vec::new(); From 1807d32ea0627cde358af742cd07436e830683aa Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Mon, 16 Mar 2026 09:56:14 -0400 Subject: [PATCH 21/50] add stricter offset validation --- parquet/src/encodings/decoding/alp.rs | 29 ++++++++++++++++----------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/parquet/src/encodings/decoding/alp.rs b/parquet/src/encodings/decoding/alp.rs index b34840b29db7..5c1332629a3b 100644 --- a/parquet/src/encodings/decoding/alp.rs +++ b/parquet/src/encodings/decoding/alp.rs @@ -455,9 +455,18 @@ fn parse_alp_page_layout(data: Bytes) -> Result(data: Bytes) -> Result(body_ref, vector_start, vector_end, vector_num_elements)?; - expected_vectors_size = expected_vectors_size + expected_next_offset = vector_start .checked_add(vector.expected_stored_size()) - .ok_or_else(|| general_err!("Invalid ALP page: expected vectors size overflow"))?; + .ok_or_else(|| general_err!("Invalid ALP page: expected next vector offset overflow"))?; vectors.push(vector); } - let expected_body_len = offsets_section_size - .checked_add(expected_vectors_size) - .ok_or_else(|| general_err!("Invalid ALP page: expected body size overflow"))?; - if body_len != expected_body_len { + if expected_next_offset != body_len { return Err(general_err!( "Invalid ALP page: body size {} does not match expected {} (offsets + vectors)", body_len, - expected_body_len + expected_next_offset )); } @@ -1159,7 +1165,7 @@ mod tests { let err = parse_alp_page_layout::(Bytes::from(data)).unwrap_err(); assert!( err.to_string() - .contains("Invalid ALP page: vector offsets are not monotonic at index 0") + .contains("Invalid ALP page: vector offset 12 at index 0 does not match expected 8") ); } @@ -1220,9 +1226,8 @@ mod tests { let err = parse_alp_page_layout::(Bytes::from(page)).unwrap_err(); assert!( - err.to_string().contains( - "Invalid ALP page: body size 14 does not match expected 13 (offsets + vectors)" - ) + err.to_string() + .contains("Invalid ALP page: vector offset 5 at index 0 does not match expected 4") ); } From 11c79057a5c69e9235fb8769578fa71c95c1ea5e Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Mon, 16 Mar 2026 10:18:25 -0400 Subject: [PATCH 22/50] fmt --- parquet/src/encodings/decoding/alp.rs | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/parquet/src/encodings/decoding/alp.rs b/parquet/src/encodings/decoding/alp.rs index 5c1332629a3b..eb76a27536ce 100644 --- a/parquet/src/encodings/decoding/alp.rs +++ b/parquet/src/encodings/decoding/alp.rs @@ -295,10 +295,7 @@ pub(super) trait AlpFloat: Copy + Default { fn decode_scale(exponent: u8, factor: u8) -> Self::Scale; /// Decode one signed exact integer using a precomputed two-step scale. - fn decode_value( - signed_encoded: ::Signed, - scale: Self::Scale, - ) -> Self; + fn decode_value(signed_encoded: ::Signed, scale: Self::Scale) -> Self; fn from_exact_bits(bits: Self::Exact) -> Self; } @@ -310,7 +307,10 @@ impl AlpFloat for f32 { fn decode_scale(exponent: u8, factor: u8) -> Self::Scale { debug_assert!(exponent <= ALP_MAX_EXPONENT_F32); debug_assert!(factor <= exponent); - (ALP_POW10_F32[factor as usize], ALP_NEG_POW10_F32[exponent as usize]) + ( + ALP_POW10_F32[factor as usize], + ALP_NEG_POW10_F32[exponent as usize], + ) } fn decode_value(signed_encoded: i32, scale: Self::Scale) -> Self { @@ -329,7 +329,10 @@ impl AlpFloat for f64 { fn decode_scale(exponent: u8, factor: u8) -> Self::Scale { debug_assert!(exponent <= ALP_MAX_EXPONENT_F64); debug_assert!(factor <= exponent); - (ALP_POW10_F64[factor as usize], ALP_NEG_POW10_F64[exponent as usize]) + ( + ALP_POW10_F64[factor as usize], + ALP_NEG_POW10_F64[exponent as usize], + ) } fn decode_value(signed_encoded: i64, scale: Self::Scale) -> Self { @@ -485,7 +488,9 @@ fn parse_alp_page_layout(data: Bytes) -> Result(body_ref, vector_start, vector_end, vector_num_elements)?; expected_next_offset = vector_start .checked_add(vector.expected_stored_size()) - .ok_or_else(|| general_err!("Invalid ALP page: expected next vector offset overflow"))?; + .ok_or_else(|| { + general_err!("Invalid ALP page: expected next vector offset overflow") + })?; vectors.push(vector); } @@ -1164,8 +1169,9 @@ mod tests { let data = make_alp_page_bytes(0, 0, 3, 9, &[12, 8], 12); let err = parse_alp_page_layout::(Bytes::from(data)).unwrap_err(); assert!( - err.to_string() - .contains("Invalid ALP page: vector offset 12 at index 0 does not match expected 8") + err.to_string().contains( + "Invalid ALP page: vector offset 12 at index 0 does not match expected 8" + ) ); } From cacacf4393f143eff5faf7b987452ba12a0b574b Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Fri, 8 May 2026 08:52:46 -0400 Subject: [PATCH 23/50] Fix up compilation --- parquet/src/encodings/decoding/alp.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/parquet/src/encodings/decoding/alp.rs b/parquet/src/encodings/decoding/alp.rs index eb76a27536ce..d319e5ada2ea 100644 --- a/parquet/src/encodings/decoding/alp.rs +++ b/parquet/src/encodings/decoding/alp.rs @@ -24,7 +24,7 @@ use crate::basic::Encoding; use crate::data_type::DataType; use crate::encodings::decoding::Decoder; use crate::errors::{ParquetError, Result}; -use crate::util::bit_util::{BitReader, FromBytes}; +use crate::util::bit_util::{BitReader, FromBitpacked, FromBytes}; const ALP_HEADER_SIZE: usize = 7; const ALP_COMPRESSION_MODE: u8 = 0; @@ -159,7 +159,7 @@ struct AlpPageLayout { /// - FOR stores non-negative deltas optimized for bitpacking. /// - Unsigned arithmetic avoids signed-overflow edge cases in FOR stage. /// - Signed interpretation is applied later during decimal reconstruction. -pub(super) trait AlpExact: Copy + std::fmt::Debug { +pub(super) trait AlpExact: Copy + std::fmt::Debug + FromBitpacked { const WIDTH: usize; type Signed: Copy; fn from_le_slice(slice: &[u8]) -> Self; @@ -644,7 +644,7 @@ fn parse_vector_view( } /// Decode bit-packed deltas into exact integers. -fn bit_unpack_integers( +fn bit_unpack_integers( packed_values: Bytes, bit_width: u8, num_elements: u16, From 578481c489aff35538ca9f3c7b09763defc6786d Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Fri, 8 May 2026 16:12:34 -0400 Subject: [PATCH 24/50] Refactor: Pull common header structures into common module --- parquet/src/encodings/alp.rs | 337 ++++++++++++++++++ parquet/src/encodings/decoding.rs | 4 +- .../decoding/{alp.rs => alp_decoder.rs} | 286 +-------------- parquet/src/encodings/mod.rs | 2 + 4 files changed, 343 insertions(+), 286 deletions(-) create mode 100644 parquet/src/encodings/alp.rs rename parquet/src/encodings/decoding/{alp.rs => alp_decoder.rs} (85%) diff --git a/parquet/src/encodings/alp.rs b/parquet/src/encodings/alp.rs new file mode 100644 index 000000000000..bd6bd8f21418 --- /dev/null +++ b/parquet/src/encodings/alp.rs @@ -0,0 +1,337 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//! ALP (Adaptive Lossless floating-Point) Encoding +//! +//! Based on the draft Parquet spec: + +use crate::util::bit_util::{FromBitpacked, FromBytes}; + +pub(crate) const ALP_HEADER_SIZE: usize = 7; +pub(crate) const ALP_COMPRESSION_MODE: u8 = 0; +pub(crate) const ALP_INTEGER_ENCODING_FOR_BIT_PACK: u8 = 0; +pub(crate) const ALP_MIN_LOG_VECTOR_SIZE: u8 = 3; +pub(crate) const ALP_MAX_LOG_VECTOR_SIZE: u8 = 15; +pub(crate) const ALP_MAX_EXPONENT_F32: u8 = 10; +pub(crate) const ALP_MAX_EXPONENT_F64: u8 = 18; + + +/// Page-level ALP header (7 bytes). +/// +/// ```text +/// Byte: 0 1 2 3 4 5 6 +/// +----------------+---------------+--------------+----+----+----+----+ +/// | compression | integer | log_vector | num_elements | +/// | _mode | _encoding | _size | (int32 LE) | +/// +----------------+---------------+--------------+----+----+----+----+ +/// ``` +/// +/// Layout in bytes: +/// - `[0]` `compression_mode` +/// - `[1]` `integer_encoding` +/// - `[2]` `log_vector_size` +/// - `[3..7]` `num_elements` (little-endian `i32`) +#[derive(Debug, Clone, Copy)] +pub(crate) struct AlpHeader { + pub(crate) compression_mode: u8, + pub(crate) integer_encoding: u8, + pub(crate) log_vector_size: u8, + pub(crate) num_elements: i32, +} + +impl AlpHeader { + pub(crate) fn num_elements_usize(&self) -> usize { + self.num_elements as usize + } + + fn vector_size(&self) -> usize { + 1usize << self.log_vector_size + } + + pub(crate) fn num_vectors(&self) -> usize { + if self.num_elements == 0 { + 0 + } else { + self.num_elements_usize().div_ceil(self.vector_size()) + } + } + + pub(crate) fn vector_num_elements(&self, vector_index: usize) -> u16 { + let vector_size = self.vector_size(); + let num_full_vectors = self.num_elements_usize() / vector_size; + let remainder = self.num_elements_usize() % vector_size; + if vector_index < num_full_vectors { + vector_size as u16 + } else if vector_index == num_full_vectors && remainder > 0 { + remainder as u16 + } else { + 0 + } + } +} + + +/// Per-vector ALP metadata (4 bytes), equivalent to C++ `AlpEncodedVectorInfo`. +/// +/// ##### AlpInfo (4 bytes, both types) +/// +/// ``` +/// Byte: 0 1 2 3 +/// +----------+----------+---------+---------+ +/// | exponent | factor | num_exceptions | +/// | (uint8) | (uint8) | (uint16 LE) | +/// +----------+----------+---------+---------+ +/// ``` +#[derive(Debug, Clone, Copy)] +pub(crate) struct AlpEncodedVectorInfo { + pub(crate) exponent: u8, + pub(crate) factor: u8, + pub(crate) num_exceptions: u16, +} + +impl AlpEncodedVectorInfo { + pub(crate) const STORED_SIZE: usize = 4; +} + +/// Per-vector FOR metadata for exact integer type (`u32` for `f32`, `u64` for `f64`). +/// +/// Frame of reference (FOR) encoding +/// +/// ```text +/// Byte: 0 1 2 3 4 +/// +----+----+----+----+-----------+ +/// | frame_of_reference | bit_width | +/// | (int32 LE) | (uint8) | +/// +----+----+----+----+-----------+ +/// ``` +#[derive(Debug, Clone, Copy)] +pub(crate) struct AlpEncodedForVectorInfo { + pub(crate) frame_of_reference: Exact, + pub(crate) bit_width: u8, +} + +impl AlpEncodedForVectorInfo { + pub(crate) fn stored_size() -> usize { + Exact::WIDTH + 1 + } + + pub(crate) fn get_bit_packed_size(&self, num_elements: u16) -> usize { + (self.bit_width as usize * num_elements as usize).div_ceil(8) + } + + pub(crate) fn get_data_stored_size(&self, num_elements: u16, num_exceptions: u16) -> usize { + let bit_packed_size = self.get_bit_packed_size(num_elements); + bit_packed_size + + num_exceptions as usize * std::mem::size_of::() + + num_exceptions as usize * Exact::WIDTH + } +} + + +/// Exact integer type used by FOR reconstruction. +/// +/// This mirrors C++: +/// - `float` -> `uint32_t` +/// - `double` -> `uint64_t` +/// +/// Why unsigned (not `i32`/`i64`)? +/// - FOR stores non-negative deltas optimized for bitpacking. +/// - Unsigned arithmetic avoids signed-overflow edge cases in FOR stage. +/// - Signed interpretation is applied later during decimal reconstruction. +pub(crate) trait AlpExact: Copy + std::fmt::Debug + FromBitpacked { + const WIDTH: usize; + type Signed: Copy; + fn from_le_slice(slice: &[u8]) -> Self; + fn zero() -> Self; + fn wrapping_add(self, rhs: Self) -> Self; + fn reinterpret_as_signed(self) -> Self::Signed; +} + +impl AlpExact for u32 { + const WIDTH: usize = 4; + type Signed = i32; + + fn from_le_slice(slice: &[u8]) -> Self { + u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]]) + } + + fn zero() -> Self { + 0 + } + + fn wrapping_add(self, rhs: Self) -> Self { + self.wrapping_add(rhs) + } + + fn reinterpret_as_signed(self) -> Self::Signed { + i32::from_ne_bytes(self.to_ne_bytes()) + } +} + +impl AlpExact for u64 { + const WIDTH: usize = 8; + type Signed = i64; + + fn from_le_slice(slice: &[u8]) -> Self { + u64::from_le_bytes([ + slice[0], slice[1], slice[2], slice[3], slice[4], slice[5], slice[6], slice[7], + ]) + } + + fn zero() -> Self { + 0 + } + + fn wrapping_add(self, rhs: Self) -> Self { + self.wrapping_add(rhs) + } + + fn reinterpret_as_signed(self) -> Self::Signed { + i64::from_ne_bytes(self.to_ne_bytes()) + } +} +pub(crate) const ALP_POW10_F32: [f32; 11] = [ + 1.0, + 10.0, + 100.0, + 1000.0, + 10000.0, + 100000.0, + 1000000.0, + 10000000.0, + 100000000.0, + 1000000000.0, + 10000000000.0, +]; + +pub(crate) const ALP_POW10_F64: [f64; 19] = [ + 1.0, + 10.0, + 100.0, + 1000.0, + 10000.0, + 100000.0, + 1000000.0, + 10000000.0, + 100000000.0, + 1000000000.0, + 10000000000.0, + 100000000000.0, + 1000000000000.0, + 10000000000000.0, + 100000000000000.0, + 1000000000000000.0, + 10000000000000000.0, + 100000000000000000.0, + 1000000000000000000.0, +]; + +pub(crate) const ALP_NEG_POW10_F32: [f32; 11] = [ + 1.0, + 0.1, + 0.01, + 0.001, + 0.0001, + 0.00001, + 0.000001, + 0.0000001, + 0.00000001, + 0.000000001, + 0.0000000001, +]; + +pub(crate) const ALP_NEG_POW10_F64: [f64; 19] = [ + 1.0, + 0.1, + 0.01, + 0.001, + 0.0001, + 0.00001, + 0.000001, + 0.0000001, + 0.00000001, + 0.000000001, + 0.0000000001, + 0.00000000001, + 0.000000000001, + 0.0000000000001, + 0.00000000000001, + 0.000000000000001, + 0.0000000000000001, + 0.00000000000000001, + 0.000000000000000001, +]; + +pub(crate) trait AlpFloat: Copy + Default { + type Exact: AlpExact + FromBytes; + type Scale: Copy; + + /// Precompute vector-level ALP decimal scale constants for: + /// `value = (encoded * 10^(factor)) * 10^(-exponent)`. + /// + /// Preconditions are validated during page parse. + fn decode_scale(exponent: u8, factor: u8) -> Self::Scale; + + /// Decode one signed exact integer using a precomputed two-step scale. + fn decode_value(signed_encoded: ::Signed, scale: Self::Scale) -> Self; + + fn from_exact_bits(bits: Self::Exact) -> Self; +} + +impl AlpFloat for f32 { + type Exact = u32; + type Scale = (f32, f32); + + fn decode_scale(exponent: u8, factor: u8) -> Self::Scale { + debug_assert!(exponent <= ALP_MAX_EXPONENT_F32); + debug_assert!(factor <= exponent); + ( + ALP_POW10_F32[factor as usize], + ALP_NEG_POW10_F32[exponent as usize], + ) + } + + fn decode_value(signed_encoded: i32, scale: Self::Scale) -> Self { + ((signed_encoded as f32) * scale.0) * scale.1 + } + + fn from_exact_bits(bits: Self::Exact) -> Self { + f32::from_bits(bits) + } +} + +impl AlpFloat for f64 { + type Exact = u64; + type Scale = (f64, f64); + + fn decode_scale(exponent: u8, factor: u8) -> Self::Scale { + debug_assert!(exponent <= ALP_MAX_EXPONENT_F64); + debug_assert!(factor <= exponent); + ( + ALP_POW10_F64[factor as usize], + ALP_NEG_POW10_F64[exponent as usize], + ) + } + + fn decode_value(signed_encoded: i64, scale: Self::Scale) -> Self { + ((signed_encoded as f64) * scale.0) * scale.1 + } + + fn from_exact_bits(bits: Self::Exact) -> Self { + f64::from_bits(bits) + } +} \ No newline at end of file diff --git a/parquet/src/encodings/decoding.rs b/parquet/src/encodings/decoding.rs index 1a7489f6031f..6b06442347a6 100644 --- a/parquet/src/encodings/decoding.rs +++ b/parquet/src/encodings/decoding.rs @@ -26,7 +26,7 @@ use super::rle::RleDecoder; use crate::basic::*; use crate::data_type::private::ParquetValueType; use crate::data_type::*; -use crate::encodings::decoding::alp::AlpDecoder; +use crate::encodings::decoding::alp_decoder::AlpDecoder; use crate::encodings::decoding::byte_stream_split_decoder::{ ByteStreamSplitDecoder, VariableWidthByteStreamSplitDecoder, }; @@ -34,7 +34,7 @@ use crate::errors::{ParquetError, Result}; use crate::schema::types::ColumnDescPtr; use crate::util::bit_util::{self, BitReader, FromBitpacked}; -mod alp; +mod alp_decoder; mod byte_stream_split_decoder; pub(crate) mod private { diff --git a/parquet/src/encodings/decoding/alp.rs b/parquet/src/encodings/decoding/alp_decoder.rs similarity index 85% rename from parquet/src/encodings/decoding/alp.rs rename to parquet/src/encodings/decoding/alp_decoder.rs index d319e5ada2ea..9840acfb7912 100644 --- a/parquet/src/encodings/decoding/alp.rs +++ b/parquet/src/encodings/decoding/alp_decoder.rs @@ -22,100 +22,11 @@ use bytes::Bytes; use crate::basic::Encoding; use crate::data_type::DataType; +use crate::encodings::alp::{AlpEncodedForVectorInfo, AlpEncodedVectorInfo, AlpExact, AlpFloat, AlpHeader, ALP_COMPRESSION_MODE, ALP_HEADER_SIZE, ALP_INTEGER_ENCODING_FOR_BIT_PACK, ALP_MAX_EXPONENT_F32, ALP_MAX_EXPONENT_F64, ALP_MAX_LOG_VECTOR_SIZE, ALP_MIN_LOG_VECTOR_SIZE}; use crate::encodings::decoding::Decoder; use crate::errors::{ParquetError, Result}; use crate::util::bit_util::{BitReader, FromBitpacked, FromBytes}; -const ALP_HEADER_SIZE: usize = 7; -const ALP_COMPRESSION_MODE: u8 = 0; -const ALP_INTEGER_ENCODING_FOR_BIT_PACK: u8 = 0; -const ALP_MIN_LOG_VECTOR_SIZE: u8 = 3; -const ALP_MAX_LOG_VECTOR_SIZE: u8 = 15; -const ALP_MAX_EXPONENT_F32: u8 = 10; -const ALP_MAX_EXPONENT_F64: u8 = 18; - -/// Page-level ALP header (7 bytes). -/// -/// Layout in bytes: -/// - `[0]` `compression_mode` -/// - `[1]` `integer_encoding` -/// - `[2]` `log_vector_size` -/// - `[3..7]` `num_elements` (little-endian `i32`) -#[derive(Debug, Clone, Copy)] -struct AlpHeader { - compression_mode: u8, - integer_encoding: u8, - log_vector_size: u8, - num_elements: i32, -} - -impl AlpHeader { - fn num_elements_usize(&self) -> usize { - self.num_elements as usize - } - - fn vector_size(&self) -> usize { - 1usize << self.log_vector_size - } - - fn num_vectors(&self) -> usize { - if self.num_elements == 0 { - 0 - } else { - self.num_elements_usize().div_ceil(self.vector_size()) - } - } - - fn vector_num_elements(&self, vector_index: usize) -> u16 { - let vector_size = self.vector_size(); - let num_full_vectors = self.num_elements_usize() / vector_size; - let remainder = self.num_elements_usize() % vector_size; - if vector_index < num_full_vectors { - vector_size as u16 - } else if vector_index == num_full_vectors && remainder > 0 { - remainder as u16 - } else { - 0 - } - } -} - -/// Per-vector ALP metadata (4 bytes), equivalent to C++ `AlpEncodedVectorInfo`. -#[derive(Debug, Clone, Copy)] -struct AlpEncodedVectorInfo { - exponent: u8, - factor: u8, - num_exceptions: u16, -} - -impl AlpEncodedVectorInfo { - const STORED_SIZE: usize = 4; -} - -/// Per-vector FOR metadata for exact integer type (`u32` for `f32`, `u64` for `f64`). -#[derive(Debug, Clone, Copy)] -struct AlpEncodedForVectorInfo { - frame_of_reference: Exact, - bit_width: u8, -} - -impl AlpEncodedForVectorInfo { - fn stored_size() -> usize { - Exact::WIDTH + 1 - } - - fn get_bit_packed_size(&self, num_elements: u16) -> usize { - (self.bit_width as usize * num_elements as usize).div_ceil(8) - } - - fn get_data_stored_size(&self, num_elements: u16, num_exceptions: u16) -> usize { - let bit_packed_size = self.get_bit_packed_size(num_elements); - bit_packed_size - + num_exceptions as usize * std::mem::size_of::() - + num_exceptions as usize * Exact::WIDTH - } -} - /// Parsed view of one vector's metadata and data slices. /// /// `packed_values` is a zero-copy range into page body bytes. @@ -149,200 +60,6 @@ struct AlpPageLayout { vectors: Vec>, } -/// Exact integer type used by FOR reconstruction. -/// -/// This mirrors C++: -/// - `float` -> `uint32_t` -/// - `double` -> `uint64_t` -/// -/// Why unsigned (not `i32`/`i64`)? -/// - FOR stores non-negative deltas optimized for bitpacking. -/// - Unsigned arithmetic avoids signed-overflow edge cases in FOR stage. -/// - Signed interpretation is applied later during decimal reconstruction. -pub(super) trait AlpExact: Copy + std::fmt::Debug + FromBitpacked { - const WIDTH: usize; - type Signed: Copy; - fn from_le_slice(slice: &[u8]) -> Self; - fn zero() -> Self; - fn wrapping_add(self, rhs: Self) -> Self; - fn reinterpret_as_signed(self) -> Self::Signed; -} - -impl AlpExact for u32 { - const WIDTH: usize = 4; - type Signed = i32; - - fn from_le_slice(slice: &[u8]) -> Self { - u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]]) - } - - fn zero() -> Self { - 0 - } - - fn wrapping_add(self, rhs: Self) -> Self { - self.wrapping_add(rhs) - } - - fn reinterpret_as_signed(self) -> Self::Signed { - i32::from_ne_bytes(self.to_ne_bytes()) - } -} - -impl AlpExact for u64 { - const WIDTH: usize = 8; - type Signed = i64; - - fn from_le_slice(slice: &[u8]) -> Self { - u64::from_le_bytes([ - slice[0], slice[1], slice[2], slice[3], slice[4], slice[5], slice[6], slice[7], - ]) - } - - fn zero() -> Self { - 0 - } - - fn wrapping_add(self, rhs: Self) -> Self { - self.wrapping_add(rhs) - } - - fn reinterpret_as_signed(self) -> Self::Signed { - i64::from_ne_bytes(self.to_ne_bytes()) - } -} - -const ALP_POW10_F32: [f32; 11] = [ - 1.0, - 10.0, - 100.0, - 1000.0, - 10000.0, - 100000.0, - 1000000.0, - 10000000.0, - 100000000.0, - 1000000000.0, - 10000000000.0, -]; - -const ALP_POW10_F64: [f64; 19] = [ - 1.0, - 10.0, - 100.0, - 1000.0, - 10000.0, - 100000.0, - 1000000.0, - 10000000.0, - 100000000.0, - 1000000000.0, - 10000000000.0, - 100000000000.0, - 1000000000000.0, - 10000000000000.0, - 100000000000000.0, - 1000000000000000.0, - 10000000000000000.0, - 100000000000000000.0, - 1000000000000000000.0, -]; - -const ALP_NEG_POW10_F32: [f32; 11] = [ - 1.0, - 0.1, - 0.01, - 0.001, - 0.0001, - 0.00001, - 0.000001, - 0.0000001, - 0.00000001, - 0.000000001, - 0.0000000001, -]; - -const ALP_NEG_POW10_F64: [f64; 19] = [ - 1.0, - 0.1, - 0.01, - 0.001, - 0.0001, - 0.00001, - 0.000001, - 0.0000001, - 0.00000001, - 0.000000001, - 0.0000000001, - 0.00000000001, - 0.000000000001, - 0.0000000000001, - 0.00000000000001, - 0.000000000000001, - 0.0000000000000001, - 0.00000000000000001, - 0.000000000000000001, -]; - -pub(super) trait AlpFloat: Copy + Default { - type Exact: AlpExact + FromBytes; - type Scale: Copy; - - /// Precompute vector-level ALP decimal scale constants for: - /// `value = (encoded * 10^(factor)) * 10^(-exponent)`. - /// - /// Preconditions are validated during page parse. - fn decode_scale(exponent: u8, factor: u8) -> Self::Scale; - - /// Decode one signed exact integer using a precomputed two-step scale. - fn decode_value(signed_encoded: ::Signed, scale: Self::Scale) -> Self; - - fn from_exact_bits(bits: Self::Exact) -> Self; -} - -impl AlpFloat for f32 { - type Exact = u32; - type Scale = (f32, f32); - - fn decode_scale(exponent: u8, factor: u8) -> Self::Scale { - debug_assert!(exponent <= ALP_MAX_EXPONENT_F32); - debug_assert!(factor <= exponent); - ( - ALP_POW10_F32[factor as usize], - ALP_NEG_POW10_F32[exponent as usize], - ) - } - - fn decode_value(signed_encoded: i32, scale: Self::Scale) -> Self { - ((signed_encoded as f32) * scale.0) * scale.1 - } - - fn from_exact_bits(bits: Self::Exact) -> Self { - f32::from_bits(bits) - } -} - -impl AlpFloat for f64 { - type Exact = u64; - type Scale = (f64, f64); - - fn decode_scale(exponent: u8, factor: u8) -> Self::Scale { - debug_assert!(exponent <= ALP_MAX_EXPONENT_F64); - debug_assert!(factor <= exponent); - ( - ALP_POW10_F64[factor as usize], - ALP_NEG_POW10_F64[exponent as usize], - ) - } - - fn decode_value(signed_encoded: i64, scale: Self::Scale) -> Self { - ((signed_encoded as f64) * scale.0) * scale.1 - } - - fn from_exact_bits(bits: Self::Exact) -> Self { - f64::from_bits(bits) - } -} /// Parse and validate a full ALP-encoded page body. /// @@ -914,6 +631,7 @@ where mod tests { use super::*; use crate::data_type::FloatType; + use crate::encodings::alp::{ALP_NEG_POW10_F32, ALP_NEG_POW10_F64, ALP_POW10_F32, ALP_POW10_F64}; fn make_alp_page_bytes( compression_mode: u8, diff --git a/parquet/src/encodings/mod.rs b/parquet/src/encodings/mod.rs index 894c4fb961ee..1b190a96317d 100644 --- a/parquet/src/encodings/mod.rs +++ b/parquet/src/encodings/mod.rs @@ -18,4 +18,6 @@ pub mod decoding; pub mod encoding; pub mod levels; +mod alp; + experimental!(pub(crate) mod rle); From f10279cb4e5ea0f85603fa2b87b7bf1141a15656 Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Fri, 8 May 2026 22:36:52 -0400 Subject: [PATCH 25/50] fmt --- parquet/src/encodings/alp.rs | 9 +++------ parquet/src/encodings/decoding/alp_decoder.rs | 11 ++++++++--- parquet/src/encodings/mod.rs | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/parquet/src/encodings/alp.rs b/parquet/src/encodings/alp.rs index bd6bd8f21418..5af911a2d2fe 100644 --- a/parquet/src/encodings/alp.rs +++ b/parquet/src/encodings/alp.rs @@ -29,7 +29,6 @@ pub(crate) const ALP_MAX_LOG_VECTOR_SIZE: u8 = 15; pub(crate) const ALP_MAX_EXPONENT_F32: u8 = 10; pub(crate) const ALP_MAX_EXPONENT_F64: u8 = 18; - /// Page-level ALP header (7 bytes). /// /// ```text @@ -49,8 +48,8 @@ pub(crate) const ALP_MAX_EXPONENT_F64: u8 = 18; pub(crate) struct AlpHeader { pub(crate) compression_mode: u8, pub(crate) integer_encoding: u8, - pub(crate) log_vector_size: u8, - pub(crate) num_elements: i32, + pub(crate) log_vector_size: u8, + pub(crate) num_elements: i32, } impl AlpHeader { @@ -84,7 +83,6 @@ impl AlpHeader { } } - /// Per-vector ALP metadata (4 bytes), equivalent to C++ `AlpEncodedVectorInfo`. /// /// ##### AlpInfo (4 bytes, both types) @@ -141,7 +139,6 @@ impl AlpEncodedForVectorInfo { } } - /// Exact integer type used by FOR reconstruction. /// /// This mirrors C++: @@ -334,4 +331,4 @@ impl AlpFloat for f64 { fn from_exact_bits(bits: Self::Exact) -> Self { f64::from_bits(bits) } -} \ No newline at end of file +} diff --git a/parquet/src/encodings/decoding/alp_decoder.rs b/parquet/src/encodings/decoding/alp_decoder.rs index 9840acfb7912..30f4e06aecaa 100644 --- a/parquet/src/encodings/decoding/alp_decoder.rs +++ b/parquet/src/encodings/decoding/alp_decoder.rs @@ -22,7 +22,11 @@ use bytes::Bytes; use crate::basic::Encoding; use crate::data_type::DataType; -use crate::encodings::alp::{AlpEncodedForVectorInfo, AlpEncodedVectorInfo, AlpExact, AlpFloat, AlpHeader, ALP_COMPRESSION_MODE, ALP_HEADER_SIZE, ALP_INTEGER_ENCODING_FOR_BIT_PACK, ALP_MAX_EXPONENT_F32, ALP_MAX_EXPONENT_F64, ALP_MAX_LOG_VECTOR_SIZE, ALP_MIN_LOG_VECTOR_SIZE}; +use crate::encodings::alp::{ + ALP_COMPRESSION_MODE, ALP_HEADER_SIZE, ALP_INTEGER_ENCODING_FOR_BIT_PACK, ALP_MAX_EXPONENT_F32, + ALP_MAX_EXPONENT_F64, ALP_MAX_LOG_VECTOR_SIZE, ALP_MIN_LOG_VECTOR_SIZE, + AlpEncodedForVectorInfo, AlpEncodedVectorInfo, AlpExact, AlpFloat, AlpHeader, +}; use crate::encodings::decoding::Decoder; use crate::errors::{ParquetError, Result}; use crate::util::bit_util::{BitReader, FromBitpacked, FromBytes}; @@ -60,7 +64,6 @@ struct AlpPageLayout { vectors: Vec>, } - /// Parse and validate a full ALP-encoded page body. /// /// Validation includes: @@ -631,7 +634,9 @@ where mod tests { use super::*; use crate::data_type::FloatType; - use crate::encodings::alp::{ALP_NEG_POW10_F32, ALP_NEG_POW10_F64, ALP_POW10_F32, ALP_POW10_F64}; + use crate::encodings::alp::{ + ALP_NEG_POW10_F32, ALP_NEG_POW10_F64, ALP_POW10_F32, ALP_POW10_F64, + }; fn make_alp_page_bytes( compression_mode: u8, diff --git a/parquet/src/encodings/mod.rs b/parquet/src/encodings/mod.rs index 1b190a96317d..cb23625a8251 100644 --- a/parquet/src/encodings/mod.rs +++ b/parquet/src/encodings/mod.rs @@ -15,9 +15,9 @@ // specific language governing permissions and limitations // under the License. +mod alp; pub mod decoding; pub mod encoding; pub mod levels; -mod alp; experimental!(pub(crate) mod rle); From fbf4ad26dcad44e0631c5201d49d513325a71df8 Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Fri, 8 May 2026 23:05:28 -0400 Subject: [PATCH 26/50] small fix --- parquet/src/encodings/alp.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parquet/src/encodings/alp.rs b/parquet/src/encodings/alp.rs index 5af911a2d2fe..726958717bff 100644 --- a/parquet/src/encodings/alp.rs +++ b/parquet/src/encodings/alp.rs @@ -87,7 +87,7 @@ impl AlpHeader { /// /// ##### AlpInfo (4 bytes, both types) /// -/// ``` +/// ```text /// Byte: 0 1 2 3 /// +----------+----------+---------+---------+ /// | exponent | factor | num_exceptions | From f65ff9a24eb9b62c5f1660ffa52d3a31b6ef92f0 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Sat, 9 May 2026 06:40:54 -0400 Subject: [PATCH 27/50] Align structure names with spec --- parquet/src/encodings/alp.rs | 10 ++++++---- parquet/src/encodings/decoding/alp_decoder.rs | 18 +++++++++--------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/parquet/src/encodings/alp.rs b/parquet/src/encodings/alp.rs index 726958717bff..9d06cdf7b999 100644 --- a/parquet/src/encodings/alp.rs +++ b/parquet/src/encodings/alp.rs @@ -95,13 +95,13 @@ impl AlpHeader { /// +----------+----------+---------+---------+ /// ``` #[derive(Debug, Clone, Copy)] -pub(crate) struct AlpEncodedVectorInfo { +pub(crate) struct AlpInfo { pub(crate) exponent: u8, pub(crate) factor: u8, pub(crate) num_exceptions: u16, } -impl AlpEncodedVectorInfo { +impl AlpInfo { pub(crate) const STORED_SIZE: usize = 4; } @@ -109,6 +109,8 @@ impl AlpEncodedVectorInfo { /// /// Frame of reference (FOR) encoding /// +/// ###### ForInfo for FLOAT (5 bytes) / DOUBLE (9 bytes) +/// /// ```text /// Byte: 0 1 2 3 4 /// +----+----+----+----+-----------+ @@ -117,12 +119,12 @@ impl AlpEncodedVectorInfo { /// +----+----+----+----+-----------+ /// ``` #[derive(Debug, Clone, Copy)] -pub(crate) struct AlpEncodedForVectorInfo { +pub(crate) struct ForInfo { pub(crate) frame_of_reference: Exact, pub(crate) bit_width: u8, } -impl AlpEncodedForVectorInfo { +impl ForInfo { pub(crate) fn stored_size() -> usize { Exact::WIDTH + 1 } diff --git a/parquet/src/encodings/decoding/alp_decoder.rs b/parquet/src/encodings/decoding/alp_decoder.rs index 30f4e06aecaa..544715789e22 100644 --- a/parquet/src/encodings/decoding/alp_decoder.rs +++ b/parquet/src/encodings/decoding/alp_decoder.rs @@ -25,7 +25,7 @@ use crate::data_type::DataType; use crate::encodings::alp::{ ALP_COMPRESSION_MODE, ALP_HEADER_SIZE, ALP_INTEGER_ENCODING_FOR_BIT_PACK, ALP_MAX_EXPONENT_F32, ALP_MAX_EXPONENT_F64, ALP_MAX_LOG_VECTOR_SIZE, ALP_MIN_LOG_VECTOR_SIZE, - AlpEncodedForVectorInfo, AlpEncodedVectorInfo, AlpExact, AlpFloat, AlpHeader, + ForInfo, AlpInfo, AlpExact, AlpFloat, AlpHeader, }; use crate::encodings::decoding::Decoder; use crate::errors::{ParquetError, Result}; @@ -38,8 +38,8 @@ use crate::util::bit_util::{BitReader, FromBitpacked, FromBytes}; #[derive(Debug)] struct AlpEncodedVectorView { num_elements: u16, - alp_info: AlpEncodedVectorInfo, - for_info: AlpEncodedForVectorInfo, + alp_info: AlpInfo, + for_info: ForInfo, packed_values: Range, exception_positions: Vec, exception_values: Vec, @@ -47,8 +47,8 @@ struct AlpEncodedVectorView { impl AlpEncodedVectorView { fn expected_stored_size(&self) -> usize { - AlpEncodedVectorInfo::STORED_SIZE - + AlpEncodedForVectorInfo::::stored_size() + AlpInfo::STORED_SIZE + + ForInfo::::stored_size() + self .for_info .get_data_stored_size(self.num_elements, self.alp_info.num_exceptions) @@ -240,7 +240,7 @@ fn parse_vector_view( let vector_bytes = &body[vector_start..vector_end]; let metadata_size = - AlpEncodedVectorInfo::STORED_SIZE + AlpEncodedForVectorInfo::::stored_size(); + AlpInfo::STORED_SIZE + ForInfo::::stored_size(); if vector_bytes.len() < metadata_size { return Err(general_err!( "Invalid ALP page: vector metadata too short, expected at least {} bytes, got {}", @@ -249,7 +249,7 @@ fn parse_vector_view( )); } - let alp_info = AlpEncodedVectorInfo { + let alp_info = AlpInfo { exponent: vector_bytes[0], factor: vector_bytes[1], num_exceptions: u16::from_le_bytes([vector_bytes[2], vector_bytes[3]]), @@ -285,7 +285,7 @@ fn parse_vector_view( )); } - let for_start = AlpEncodedVectorInfo::STORED_SIZE; + let for_start = AlpInfo::STORED_SIZE; let for_end = for_start + Exact::WIDTH; let frame_of_reference = Exact::from_le_slice(&vector_bytes[for_start..for_end]); let bit_width = vector_bytes[for_end]; @@ -298,7 +298,7 @@ fn parse_vector_view( )); } - let for_info = AlpEncodedForVectorInfo:: { + let for_info = ForInfo:: { frame_of_reference, bit_width, }; From 26c382c538deacc8f73e12a6337ab6aedd82ece5 Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Sat, 9 May 2026 11:33:00 -0400 Subject: [PATCH 28/50] fmt --- parquet/src/encodings/decoding/alp_decoder.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/parquet/src/encodings/decoding/alp_decoder.rs b/parquet/src/encodings/decoding/alp_decoder.rs index 544715789e22..b2c91bfe939d 100644 --- a/parquet/src/encodings/decoding/alp_decoder.rs +++ b/parquet/src/encodings/decoding/alp_decoder.rs @@ -24,8 +24,8 @@ use crate::basic::Encoding; use crate::data_type::DataType; use crate::encodings::alp::{ ALP_COMPRESSION_MODE, ALP_HEADER_SIZE, ALP_INTEGER_ENCODING_FOR_BIT_PACK, ALP_MAX_EXPONENT_F32, - ALP_MAX_EXPONENT_F64, ALP_MAX_LOG_VECTOR_SIZE, ALP_MIN_LOG_VECTOR_SIZE, - ForInfo, AlpInfo, AlpExact, AlpFloat, AlpHeader, + ALP_MAX_EXPONENT_F64, ALP_MAX_LOG_VECTOR_SIZE, ALP_MIN_LOG_VECTOR_SIZE, AlpExact, AlpFloat, + AlpHeader, AlpInfo, ForInfo, }; use crate::encodings::decoding::Decoder; use crate::errors::{ParquetError, Result}; @@ -239,8 +239,7 @@ fn parse_vector_view( ) -> Result> { let vector_bytes = &body[vector_start..vector_end]; - let metadata_size = - AlpInfo::STORED_SIZE + ForInfo::::stored_size(); + let metadata_size = AlpInfo::STORED_SIZE + ForInfo::::stored_size(); if vector_bytes.len() < metadata_size { return Err(general_err!( "Invalid ALP page: vector metadata too short, expected at least {} bytes, got {}", From 82cdba3186a290b2edd135cbcfffa6f9614db160 Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Mon, 18 May 2026 10:08:13 -0400 Subject: [PATCH 29/50] use arrow-csv --- parquet/Cargo.toml | 1 + parquet/tests/arrow_reader/alp.rs | 57 ++++++------------------------- 2 files changed, 11 insertions(+), 47 deletions(-) diff --git a/parquet/Cargo.toml b/parquet/Cargo.toml index 9f4d2a33df0c..2366f6028030 100644 --- a/parquet/Cargo.toml +++ b/parquet/Cargo.toml @@ -91,6 +91,7 @@ zstd = { version = "0.13", default-features = false } serde_json = { version = "1.0", features = ["std"], default-features = false } arrow = { workspace = true, features = ["ipc", "test_utils", "prettyprint", "json"] } arrow-cast = { workspace = true } +arrow-csv = { workspace = true } tokio = { version = "1.0", default-features = false, features = ["macros", "rt-multi-thread", "io-util", "fs"] } rand = { version = "0.9", default-features = false, features = ["std", "std_rng", "thread_rng"] } object_store = { workspace = true, features = ["azure", "fs"] } diff --git a/parquet/tests/arrow_reader/alp.rs b/parquet/tests/arrow_reader/alp.rs index 93f36c1aaffb..e750bbc40e75 100644 --- a/parquet/tests/arrow_reader/alp.rs +++ b/parquet/tests/arrow_reader/alp.rs @@ -19,11 +19,11 @@ use arrow::compute::concat_batches; use arrow::util::test_util::parquet_test_data; use arrow_array::cast::as_primitive_array; use arrow_array::types::Float32Type; -use arrow_array::{Array, ArrayRef, Float32Array, RecordBatch}; +use arrow_array::{Array, RecordBatch}; +use arrow_csv::ReaderBuilder as CsvReaderBuilder; use arrow_schema::{DataType, Field, Schema}; use parquet::arrow::arrow_reader::ArrowReaderBuilder; use std::fs::File; -use std::io::{BufRead, BufReader}; use std::path::PathBuf; use std::sync::Arc; @@ -32,10 +32,6 @@ fn test_read_f32_alp() { let data_dir = PathBuf::from(parquet_test_data()); let parquet_path = data_dir.join("alp_float_arade.parquet"); let expected_csv_path = data_dir.join("alp_arade_expect.csv"); - if !parquet_path.exists() || !expected_csv_path.exists() { - eprintln!("Skipping ALP test files not found"); - return; - } let expected = read_expected_csv_batch(&expected_csv_path); let actual = read_parquet_batch(&parquet_path); @@ -93,45 +89,12 @@ fn read_parquet_batch(path: &PathBuf) -> RecordBatch { fn read_expected_csv_batch(path: &PathBuf) -> RecordBatch { let file = File::open(path).unwrap(); - let mut lines = BufReader::new(file).lines(); - - let header = lines.next().expect("expected csv header line").unwrap(); - assert_eq!( - header.trim(), - "value1,value2,value3,value4", - "unexpected csv header" - ); - - let mut c0 = Vec::new(); - let mut c1 = Vec::new(); - let mut c2 = Vec::new(); - let mut c3 = Vec::new(); - for (line_idx, line) in lines.enumerate() { - let line = line.unwrap(); - if line.trim().is_empty() { - continue; - } - let values: Vec<_> = line.split(',').map(str::trim).collect(); - assert_eq!( - values.len(), - 4, - "wrong csv column count at line {}", - line_idx + 2 - ); - c0.push(values[0].parse::().unwrap()); - c1.push(values[1].parse::().unwrap()); - c2.push(values[2].parse::().unwrap()); - c3.push(values[3].parse::().unwrap()); - } - - RecordBatch::try_new( - alp_schema(), - vec![ - Arc::new(Float32Array::from(c0)) as ArrayRef, - Arc::new(Float32Array::from(c1)) as ArrayRef, - Arc::new(Float32Array::from(c2)) as ArrayRef, - Arc::new(Float32Array::from(c3)) as ArrayRef, - ], - ) - .unwrap() + let schema = alp_schema(); + let reader = CsvReaderBuilder::new(schema.clone()) + .with_header(true) + .build(file) + .unwrap(); + let batches: Vec = reader.map(|b| b.unwrap()).collect(); + assert!(!batches.is_empty(), "expected non-empty csv batch set"); + concat_batches(&schema, &batches).unwrap() } From d15f0ca8b7cac7eebb6f5d1e0aa908694c3c5388 Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Mon, 18 May 2026 10:14:41 -0400 Subject: [PATCH 30/50] add page layout ASCII art --- parquet/src/encodings/alp.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/parquet/src/encodings/alp.rs b/parquet/src/encodings/alp.rs index 9d06cdf7b999..b813fe8d11fa 100644 --- a/parquet/src/encodings/alp.rs +++ b/parquet/src/encodings/alp.rs @@ -18,6 +18,24 @@ //! ALP (Adaptive Lossless floating-Point) Encoding //! //! Based on the draft Parquet spec: +//! +//! # Page layout +//! +//! An ALP-encoded page consists of a fixed-size header, an offset array +//! locating each vector inside the body, and the vector data itself: +//! +//! ```text +//! +-------------+-----------------------------+--------------------------------------+ +//! | Header | Offset Array | Vector Data | +//! | (7 bytes) | (num_vectors * 4 bytes) | (variable) | +//! +-------------+------+------+-----+---------+----------+----------+-----+----------+ +//! | Page Header | off0 | off1 | ... | off N-1 | Vector 0 | Vector 1 | ... | Vec N-1 | +//! | (7 bytes) | (4B) | (4B) | | (4B) |(variable)|(variable)| |(variable)| +//! +-------------+------+------+-----+---------+----------+----------+-----+----------+ +//! ``` +//! +//! Each vector entry has the form +//! `[AlpInfo][ForInfo][PackedValues][ExceptionPositions][ExceptionValues]`. use crate::util::bit_util::{FromBitpacked, FromBytes}; From a6b5f05c6eeccf9626f0436a481f91a8c06becab Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Tue, 2 Jun 2026 15:44:57 -0400 Subject: [PATCH 31/50] add `AlpHeader` serde methods for `vector_size` and `num_elements` --- parquet/src/encodings/alp.rs | 97 +++++++++++-- parquet/src/encodings/decoding/alp_decoder.rs | 130 +++++++++++------- 2 files changed, 167 insertions(+), 60 deletions(-) diff --git a/parquet/src/encodings/alp.rs b/parquet/src/encodings/alp.rs index b813fe8d11fa..de99ef7572ed 100644 --- a/parquet/src/encodings/alp.rs +++ b/parquet/src/encodings/alp.rs @@ -37,6 +37,7 @@ //! Each vector entry has the form //! `[AlpInfo][ForInfo][PackedValues][ExceptionPositions][ExceptionValues]`. +use crate::errors::{ParquetError, Result}; use crate::util::bit_util::{FromBitpacked, FromBytes}; pub(crate) const ALP_HEADER_SIZE: usize = 7; @@ -62,37 +63,111 @@ pub(crate) const ALP_MAX_EXPONENT_F64: u8 = 18; /// - `[1]` `integer_encoding` /// - `[2]` `log_vector_size` /// - `[3..7]` `num_elements` (little-endian `i32`) +/// +/// The fields hold the *decoded* values used throughout the decoder, not the +/// raw on-disk encoding: +/// - `num_elements` is stored on disk as an `i32`, kept in memory as a `usize`. +/// - vector size is stored on disk as a `u8` `log_vector_size`, kept in memory +/// as the actual `vector_size` (`1 << log_vector_size`) `usize`. +/// +/// Each conversion happens once, in [`AlpHeader::deserialize`] and +/// [`AlpHeader::serialize`], so the rest of the decoder computes offsets and +/// sizes in `usize`. Those methods reject only what the target type cannot +/// represent; spec-level validity, such as the allowed vector-size range, is +/// enforced by the page parser. #[derive(Debug, Clone, Copy)] pub(crate) struct AlpHeader { pub(crate) compression_mode: u8, pub(crate) integer_encoding: u8, - pub(crate) log_vector_size: u8, - pub(crate) num_elements: i32, + pub(crate) vector_size: usize, + pub(crate) num_elements: usize, } impl AlpHeader { - pub(crate) fn num_elements_usize(&self) -> usize { - self.num_elements as usize + /// Parse a 7-byte page header from its little-endian on-disk form, + /// converting each field to its in-memory type: + /// - `log_vector_size` (`u8`) is expanded to `vector_size` with an + /// overflow-checked shift. + /// - `num_elements` (`i32`) is checked for non-negativity. + pub(crate) fn deserialize(bytes: &[u8]) -> Result { + if bytes.len() < ALP_HEADER_SIZE { + return Err(general_err!( + "Invalid ALP page: expected at least {} bytes for header, got {}", + ALP_HEADER_SIZE, + bytes.len() + )); + } + + let log_vector_size = bytes[2]; + let vector_size = 1usize + .checked_shl(u32::from(log_vector_size)) + .ok_or_else(|| { + general_err!( + "Invalid ALP page: log_vector_size {} too large to represent a vector size", + log_vector_size + ) + })?; + + let num_elements_i32 = i32::from_le_bytes([bytes[3], bytes[4], bytes[5], bytes[6]]); + let num_elements = usize::try_from(num_elements_i32).map_err(|_| { + general_err!( + "Invalid ALP page: num_elements {} must be >= 0", + num_elements_i32 + ) + })?; + + Ok(Self { + compression_mode: bytes[0], + integer_encoding: bytes[1], + vector_size, + num_elements, + }) } - fn vector_size(&self) -> usize { - 1usize << self.log_vector_size + /// Serialize this header into its 7-byte little-endian on-disk form. + /// + /// Converts the in-memory values back to the on-disk encoding, rejecting + /// what cannot be represented: `vector_size` must be a power of two (its log + /// is the on-disk field), and `num_elements` must fit in an `i32`. + /// Counterpart to [`AlpHeader::deserialize`]; consumed by the ALP encoder. + #[allow(dead_code)] + pub(crate) fn serialize(&self) -> Result<[u8; ALP_HEADER_SIZE]> { + if !self.vector_size.is_power_of_two() { + return Err(general_err!( + "Invalid ALP page: vector_size {} is not a power of two", + self.vector_size + )); + } + let log_vector_size = self.vector_size.trailing_zeros() as u8; + + let num_elements = i32::try_from(self.num_elements).map_err(|_| { + general_err!( + "Invalid ALP page: num_elements {} exceeds i32::MAX", + self.num_elements + ) + })?; + + let mut out = [0u8; ALP_HEADER_SIZE]; + out[0] = self.compression_mode; + out[1] = self.integer_encoding; + out[2] = log_vector_size; + out[3..7].copy_from_slice(&num_elements.to_le_bytes()); + Ok(out) } pub(crate) fn num_vectors(&self) -> usize { if self.num_elements == 0 { 0 } else { - self.num_elements_usize().div_ceil(self.vector_size()) + self.num_elements.div_ceil(self.vector_size) } } pub(crate) fn vector_num_elements(&self, vector_index: usize) -> u16 { - let vector_size = self.vector_size(); - let num_full_vectors = self.num_elements_usize() / vector_size; - let remainder = self.num_elements_usize() % vector_size; + let num_full_vectors = self.num_elements / self.vector_size; + let remainder = self.num_elements % self.vector_size; if vector_index < num_full_vectors { - vector_size as u16 + self.vector_size as u16 } else if vector_index == num_full_vectors && remainder > 0 { remainder as u16 } else { diff --git a/parquet/src/encodings/decoding/alp_decoder.rs b/parquet/src/encodings/decoding/alp_decoder.rs index b2c91bfe939d..8a78e33cac6c 100644 --- a/parquet/src/encodings/decoding/alp_decoder.rs +++ b/parquet/src/encodings/decoding/alp_decoder.rs @@ -67,26 +67,13 @@ struct AlpPageLayout { /// Parse and validate a full ALP-encoded page body. /// /// Validation includes: -/// - header fields/encoding -/// - non-negative `num_elements` +/// - header decoding via [`AlpHeader::deserialize`] +/// - supported compression mode, integer encoding, and vector-size range /// - offsets bounds + monotonicity /// - per-vector metadata/data section lengths fn parse_alp_page_layout(data: Bytes) -> Result> { let data_ref = data.as_ref(); - if data_ref.len() < ALP_HEADER_SIZE { - return Err(general_err!( - "Invalid ALP page: expected at least {} bytes for header, got {}", - ALP_HEADER_SIZE, - data_ref.len() - )); - } - - let header = AlpHeader { - compression_mode: data_ref[0], - integer_encoding: data_ref[1], - log_vector_size: data_ref[2], - num_elements: i32::from_le_bytes([data_ref[3], data_ref[4], data_ref[5], data_ref[6]]), - }; + let header = AlpHeader::deserialize(data_ref)?; if header.compression_mode != ALP_COMPRESSION_MODE { return Err(general_err!( @@ -102,29 +89,21 @@ fn parse_alp_page_layout(data: Bytes) -> Result ALP_MAX_LOG_VECTOR_SIZE { + if header.vector_size > (1usize << ALP_MAX_LOG_VECTOR_SIZE) { return Err(general_err!( "Invalid ALP page: log_vector_size {} exceeds max {}", - header.log_vector_size, + header.vector_size.trailing_zeros(), ALP_MAX_LOG_VECTOR_SIZE )); } - if header.num_elements < 0 { - return Err(general_err!( - "Invalid ALP page: num_elements {} must be >= 0", - header.num_elements - )); - } - let num_vectors = header.num_vectors(); let offsets_len = num_vectors @@ -450,7 +429,7 @@ fn decode_vector_values( } fn decode_page_values(layout: &AlpPageLayout) -> Result> { - let total = layout.header.num_elements_usize(); + let total = layout.header.num_elements; let mut out = vec![Value::default(); total]; decode_page_values_into::(layout, &mut out)?; Ok(out) @@ -463,7 +442,7 @@ fn decode_page_values_into( layout: &AlpPageLayout, out: &mut [Value], ) -> Result<()> { - let total = layout.header.num_elements_usize(); + let total = layout.header.num_elements; if out.len() < total { return Err(general_err!( "Invalid ALP decode output: output length {} smaller than page values {}", @@ -555,7 +534,7 @@ where fn set_data(&mut self, data: Bytes, num_values: usize) -> Result<()> { let layout = parse_alp_page_layout::<::Exact>(data)?; - if layout.header.num_elements_usize() != num_values { + if layout.header.num_elements != num_values { return Err(general_err!( "Invalid ALP page: header num_elements {} does not match page num_values {}", layout.header.num_elements, @@ -733,23 +712,86 @@ mod tests { } #[test] - fn test_parse_alp_page_layout_valid() { - let data = make_alp_page_bytes(0, 0, 3, 4, &[4], 13); - let parsed = parse_alp_page_layout::(Bytes::from(data)).unwrap(); - assert_eq!(parsed.header.num_elements, 4); - assert_eq!(parsed.vectors.len(), 1); - assert_eq!(parsed.vectors[0].num_elements, 4); + fn test_alp_header_serialize_deserialize_round_trip() { + let header = AlpHeader { + compression_mode: ALP_COMPRESSION_MODE, + integer_encoding: ALP_INTEGER_ENCODING_FOR_BIT_PACK, + vector_size: 8, + num_elements: 1234, + }; + let bytes = header.serialize().unwrap(); + let parsed = AlpHeader::deserialize(&bytes).unwrap(); + assert_eq!(parsed.compression_mode, header.compression_mode); + assert_eq!(parsed.integer_encoding, header.integer_encoding); + assert_eq!(parsed.vector_size, header.vector_size); + assert_eq!(parsed.num_elements, header.num_elements); + } + + #[test] + fn test_alp_header_serialize_rejects_overflow() { + let header = AlpHeader { + compression_mode: 0, + integer_encoding: 0, + vector_size: 8, + num_elements: i32::MAX as usize + 1, + }; + let err = header.serialize().unwrap_err(); + assert!(err.to_string().contains("exceeds i32::MAX")); } #[test] - fn test_parse_alp_page_layout_short_header() { - let err = parse_alp_page_layout::(Bytes::from_static(&[0, 1, 2])).unwrap_err(); + fn test_alp_header_serialize_rejects_non_power_of_two_vector_size() { + let header = AlpHeader { + compression_mode: 0, + integer_encoding: 0, + vector_size: 100, + num_elements: 4, + }; + let err = header.serialize().unwrap_err(); + assert!(err.to_string().contains("is not a power of two")); + } + + #[test] + fn test_alp_header_deserialize_short_header() { + let err = AlpHeader::deserialize(&[0, 1, 2]).unwrap_err(); assert!( err.to_string() .contains("Invalid ALP page: expected at least 7 bytes for header") ); } + #[test] + fn test_alp_header_deserialize_negative_num_elements() { + let mut bytes = [0u8; ALP_HEADER_SIZE]; + bytes[2] = ALP_MIN_LOG_VECTOR_SIZE; + bytes[3..7].copy_from_slice(&(-1i32).to_le_bytes()); + let err = AlpHeader::deserialize(&bytes).unwrap_err(); + assert!( + err.to_string() + .contains("Invalid ALP page: num_elements -1 must be >= 0") + ); + } + + #[test] + fn test_alp_header_deserialize_log_vector_size_overflow() { + let mut bytes = [0u8; ALP_HEADER_SIZE]; + bytes[2] = 64; // 1 << 64 overflows usize + let err = AlpHeader::deserialize(&bytes).unwrap_err(); + assert!( + err.to_string() + .contains("too large to represent a vector size") + ); + } + + #[test] + fn test_parse_alp_page_layout_valid() { + let data = make_alp_page_bytes(0, 0, 3, 4, &[4], 13); + let parsed = parse_alp_page_layout::(Bytes::from(data)).unwrap(); + assert_eq!(parsed.header.num_elements, 4); + assert_eq!(parsed.vectors.len(), 1); + assert_eq!(parsed.vectors[0].num_elements, 4); + } + #[test] fn test_parse_alp_page_layout_too_small_log_vector_size() { let data = make_alp_page_bytes(0, 0, 2, 1, &[4], 8); @@ -780,16 +822,6 @@ mod tests { ); } - #[test] - fn test_parse_alp_page_layout_negative_num_elements() { - let data = make_alp_page_bytes(0, 0, 3, -1, &[4], 8); - let err = parse_alp_page_layout::(Bytes::from(data)).unwrap_err(); - assert!( - err.to_string() - .contains("Invalid ALP page: num_elements -1 must be >= 0") - ); - } - #[test] fn test_parse_alp_page_layout_invalid_exponent_f32() { let vector = make_vector(VectorSpec { From 10bbc6ba15784aa9d9338efe8eb8fdd9abd44e3e Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Sun, 7 Jun 2026 11:22:38 -0400 Subject: [PATCH 32/50] store offsets instead of vecs in `AlpEncodedVectorView` --- parquet/src/encodings/decoding/alp_decoder.rs | 105 +++++++++++------- 1 file changed, 64 insertions(+), 41 deletions(-) diff --git a/parquet/src/encodings/decoding/alp_decoder.rs b/parquet/src/encodings/decoding/alp_decoder.rs index 8a78e33cac6c..7f2f96e84185 100644 --- a/parquet/src/encodings/decoding/alp_decoder.rs +++ b/parquet/src/encodings/decoding/alp_decoder.rs @@ -31,18 +31,22 @@ use crate::encodings::decoding::Decoder; use crate::errors::{ParquetError, Result}; use crate::util::bit_util::{BitReader, FromBitpacked, FromBytes}; -/// Parsed view of one vector's metadata and data slices. +/// Parsed view of one vector's metadata and data sections. /// -/// `packed_values` is a zero-copy range into page body bytes. -/// Exception positions/values are copied for straightforward decode handling. -#[derive(Debug)] +/// Each data section is described by its start offset into the page body; the +/// section bytes themselves stay in the body and are decoded lazily when the +/// vector is decoded. Section lengths are fully determined by the fixed-size +/// metadata at the front of the vector (`bit_width` for `packed_values`, +/// `num_exceptions` for both exception sections), so only the start offset is +/// stored. +#[derive(Debug, Clone, Copy)] struct AlpEncodedVectorView { num_elements: u16, alp_info: AlpInfo, for_info: ForInfo, - packed_values: Range, - exception_positions: Vec, - exception_values: Vec, + packed_values: usize, + exception_positions: usize, + exception_values: usize, } impl AlpEncodedVectorView { @@ -53,6 +57,24 @@ impl AlpEncodedVectorView { .for_info .get_data_stored_size(self.num_elements, self.alp_info.num_exceptions) } + + /// Byte range of the bit-packed values section in the page body. + fn packed_values_range(&self) -> Range { + let len = self.for_info.get_bit_packed_size(self.num_elements); + self.packed_values..self.packed_values + len + } + + /// Byte range of the exception positions section (`u16` each) in the page body. + fn exception_positions_range(&self) -> Range { + let len = self.alp_info.num_exceptions as usize * std::mem::size_of::(); + self.exception_positions..self.exception_positions + len + } + + /// Byte range of the exception values section (`Exact::WIDTH` each) in the page body. + fn exception_values_range(&self) -> Range { + let len = self.alp_info.num_exceptions as usize * Exact::WIDTH; + self.exception_values..self.exception_values + len + } } /// Parsed ALP page layout for one exact integer width (`u32` for float pages, @@ -301,17 +323,16 @@ fn parse_vector_view( let data = &vector_bytes[metadata_size..expected_size]; let packed_size = for_info.get_bit_packed_size(num_elements); let positions_size = alp_info.num_exceptions as usize * std::mem::size_of::(); - let values_size = alp_info.num_exceptions as usize * Exact::WIDTH; - let packed_start = 0; - let packed_end = packed_start + packed_size; - let positions_start = packed_end; - let positions_end = positions_start + positions_size; - let values_start = positions_end; - let values_end = values_start + values_size; + // Section offsets relative to the start of the data section: packed values + // first, then exception positions, then exception values. + let positions_start = packed_size; + let values_start = positions_start + positions_size; - let mut exception_positions = Vec::with_capacity(alp_info.num_exceptions as usize); - for chunk in data[positions_start..positions_end].chunks_exact(2) { + // Validate exception positions without materializing them. They are decoded + // straight from the body when the vector is decoded; here we only enforce + // that every position is in range so the whole page is validated up front. + for chunk in data[positions_start..values_start].chunks_exact(2) { let position = u16::from_le_bytes([chunk[0], chunk[1]]); if position >= num_elements { return Err(general_err!( @@ -320,16 +341,14 @@ fn parse_vector_view( num_elements )); } - exception_positions.push(position); } - let packed_values = - (vector_start + metadata_size + packed_start)..(vector_start + metadata_size + packed_end); - - let mut exception_values = Vec::with_capacity(alp_info.num_exceptions as usize); - for chunk in data[values_start..values_end].chunks_exact(Exact::WIDTH) { - exception_values.push(Exact::from_le_slice(chunk)); - } + // Store each section's start offset into the page body. Lengths are derived + // from the vector metadata at decode time, so no end offset is stored. + let data_start = vector_start + metadata_size; + let packed_values = data_start; + let exception_positions = data_start + positions_start; + let exception_values = data_start + values_start; Ok(AlpEncodedVectorView { num_elements, @@ -387,7 +406,7 @@ fn decode_vector_values( vector: &AlpEncodedVectorView, ) -> Result> { let mut exact_values = bit_unpack_integers( - body.slice(vector.packed_values.clone()), + body.slice(vector.packed_values_range()), vector.for_info.bit_width, vector.num_elements, )?; @@ -401,20 +420,15 @@ fn decode_vector_values( out.push(Value::decode_value(signed_value, scale)); } - if vector.exception_positions.len() != vector.exception_values.len() { - return Err(general_err!( - "Invalid ALP page: exception positions ({}) and values ({}) length mismatch", - vector.exception_positions.len(), - vector.exception_values.len() - )); - } - - for (pos, value_bits) in vector - .exception_positions - .iter() - .zip(vector.exception_values.iter()) + // Positions and values are equal-count by construction (both sized from + // `num_exceptions`), so zipping the body sections never drops a pair. + let positions = &body[vector.exception_positions_range()]; + let values = &body[vector.exception_values_range()]; + for (pos_chunk, value_chunk) in positions + .chunks_exact(std::mem::size_of::()) + .zip(values.chunks_exact(Value::Exact::WIDTH)) { - let pos = *pos as usize; + let pos = u16::from_le_bytes([pos_chunk[0], pos_chunk[1]]) as usize; if pos >= out.len() { return Err(general_err!( "Invalid ALP page: exception position {} out of bounds for vector length {}", @@ -422,7 +436,7 @@ fn decode_vector_values( out.len() )); } - out[pos] = Value::from_exact_bits(*value_bits); + out[pos] = Value::from_exact_bits(Value::Exact::from_le_slice(value_chunk)); } Ok(out) @@ -1015,8 +1029,17 @@ mod tests { assert_eq!(parsed_vector.num_elements, 1); assert_eq!(parsed_vector.alp_info.num_exceptions, 1); assert_eq!(parsed_vector.for_info.bit_width, 0); - assert_eq!(parsed_vector.exception_positions, vec![0]); - assert_eq!(parsed_vector.exception_values, vec![42.5_f64.to_bits()]); + + let positions: Vec = parsed.body[parsed_vector.exception_positions_range()] + .chunks_exact(2) + .map(|c| u16::from_le_bytes([c[0], c[1]])) + .collect(); + let values: Vec = parsed.body[parsed_vector.exception_values_range()] + .chunks_exact(8) + .map(|c| u64::from_le_bytes(c.try_into().unwrap())) + .collect(); + assert_eq!(positions, vec![0]); + assert_eq!(values, vec![42.5_f64.to_bits()]); } #[test] From 9978b31915e8fc3357fba0c9baebd7ffb228acd9 Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Sun, 7 Jun 2026 13:08:29 -0400 Subject: [PATCH 33/50] pass output slice instead of allocating a vec --- parquet/src/encodings/decoding/alp_decoder.rs | 54 +++++++++++-------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/parquet/src/encodings/decoding/alp_decoder.rs b/parquet/src/encodings/decoding/alp_decoder.rs index 7f2f96e84185..88ccec2aec09 100644 --- a/parquet/src/encodings/decoding/alp_decoder.rs +++ b/parquet/src/encodings/decoding/alp_decoder.rs @@ -399,12 +399,15 @@ fn inverse_for(deltas: &mut [Exact], frame_of_reference: Exact) } } -/// Decode one vector into output floating values: -/// bit-unpack -> inverse FOR -> decimal decode -> patch exceptions. +/// Decode one vector directly into `out` (whose length must equal the vector's +/// element count): bit-unpack -> inverse FOR -> decimal decode -> patch exceptions. fn decode_vector_values( body: &Bytes, vector: &AlpEncodedVectorView, -) -> Result> { + out: &mut [Value], +) -> Result<()> { + debug_assert_eq!(out.len(), vector.num_elements as usize); + let mut exact_values = bit_unpack_integers( body.slice(vector.packed_values_range()), vector.for_info.bit_width, @@ -414,10 +417,9 @@ fn decode_vector_values( let scale = Value::decode_scale(vector.alp_info.exponent, vector.alp_info.factor); - let mut out = Vec::with_capacity(vector.num_elements as usize); - for exact_value in exact_values { + for (slot, exact_value) in out.iter_mut().zip(exact_values) { let signed_value = exact_value.reinterpret_as_signed(); - out.push(Value::decode_value(signed_value, scale)); + *slot = Value::decode_value(signed_value, scale); } // Positions and values are equal-count by construction (both sized from @@ -439,14 +441,7 @@ fn decode_vector_values( out[pos] = Value::from_exact_bits(Value::Exact::from_le_slice(value_chunk)); } - Ok(out) -} - -fn decode_page_values(layout: &AlpPageLayout) -> Result> { - let total = layout.header.num_elements; - let mut out = vec![Value::default(); total]; - decode_page_values_into::(layout, &mut out)?; - Ok(out) + Ok(()) } /// Decode a full ALP page into an existing output slice. @@ -467,9 +462,12 @@ fn decode_page_values_into( let mut output_offset = 0usize; for vector in &layout.vectors { - let vector_values = decode_vector_values::(&layout.body, vector)?; - let next_offset = output_offset + vector_values.len(); - out[output_offset..next_offset].copy_from_slice(&vector_values); + let next_offset = output_offset + vector.num_elements as usize; + decode_vector_values::( + &layout.body, + vector, + &mut out[output_offset..next_offset], + )?; output_offset = next_offset; } if output_offset != total { @@ -530,7 +528,11 @@ where general_err!("Invalid ALP decoder state: set_data must be called before get/skip") })?; - self.decoded_values = decode_page_values::(&layout)?; + // Reuse the buffer's capacity across pages instead of reallocating. + self.decoded_values.clear(); + self.decoded_values + .resize(layout.header.num_elements, T::T::default()); + decode_page_values_into::(&layout, &mut self.decoded_values)?; self.needs_decode = false; Ok(()) @@ -697,6 +699,12 @@ mod tests { out } + fn decode_page(layout: &AlpPageLayout) -> Vec { + let mut out = vec![Value::default(); layout.header.num_elements]; + decode_page_values_into::(layout, &mut out).unwrap(); + out + } + fn make_page_from_vectors( log_vector_size: u8, num_elements: i32, @@ -1075,7 +1083,7 @@ mod tests { }); let page = make_page_from_vectors(3, 4, &[vector]); let layout = parse_alp_page_layout::(Bytes::from(page)).unwrap(); - let decoded = decode_page_values::(&layout).unwrap(); + let decoded = decode_page::(&layout); assert_eq!(decoded, vec![10.0, 11.0, 12.0, 13.0]); } @@ -1101,7 +1109,7 @@ mod tests { }); let page = make_page_from_vectors(3, 9, &[vector0, vector1]); let layout = parse_alp_page_layout::(Bytes::from(page)).unwrap(); - let decoded = decode_page_values::(&layout).unwrap(); + let decoded = decode_page::(&layout); assert_eq!( decoded, vec![10.0, 42.5, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 7.0] @@ -1126,7 +1134,7 @@ mod tests { }); let page = make_page_from_vectors(3, 3, &[vector]); let layout = parse_alp_page_layout::(Bytes::from(page)).unwrap(); - let decoded = decode_page_values::(&layout).unwrap(); + let decoded = decode_page::(&layout); assert!(decoded[0].is_nan()); assert_eq!(decoded[1], -0.0); @@ -1148,7 +1156,7 @@ mod tests { }); let page = make_page_from_vectors(3, 1, &[vector]); let layout = parse_alp_page_layout::(Bytes::from(page)).unwrap(); - let decoded = decode_page_values::(&layout).unwrap(); + let decoded = decode_page::(&layout); let expected_two_step = ((encoded as f32) * ALP_POW10_F32[1]) * ALP_NEG_POW10_F32[1]; let one_step_scale = ALP_POW10_F32[1] * ALP_NEG_POW10_F32[1]; @@ -1172,7 +1180,7 @@ mod tests { }); let page = make_page_from_vectors(3, 1, &[vector]); let layout = parse_alp_page_layout::(Bytes::from(page)).unwrap(); - let decoded = decode_page_values::(&layout).unwrap(); + let decoded = decode_page::(&layout); let expected_two_step = ((encoded as f64) * ALP_POW10_F64[1]) * ALP_NEG_POW10_F64[1]; let one_step_scale = ALP_POW10_F64[1] * ALP_NEG_POW10_F64[1]; From 70e35a85d572bb18cecd6c09afc15f18ce45745e Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Sun, 7 Jun 2026 13:22:05 -0400 Subject: [PATCH 34/50] fuse inverse FOR and decimal decode in one pass + remove `PhantonData` marker --- parquet/src/encodings/decoding/alp_decoder.rs | 38 +++++-------------- 1 file changed, 9 insertions(+), 29 deletions(-) diff --git a/parquet/src/encodings/decoding/alp_decoder.rs b/parquet/src/encodings/decoding/alp_decoder.rs index 88ccec2aec09..bdf18674b8e0 100644 --- a/parquet/src/encodings/decoding/alp_decoder.rs +++ b/parquet/src/encodings/decoding/alp_decoder.rs @@ -15,7 +15,6 @@ // specific language governing permissions and limitations // under the License. -use std::marker::PhantomData; use std::ops::Range; use bytes::Bytes; @@ -392,15 +391,8 @@ fn bit_unpack_integers( Ok(out) } -/// Apply inverse FOR: `decoded = delta + frame_of_reference`. -fn inverse_for(deltas: &mut [Exact], frame_of_reference: Exact) { - for value in deltas { - *value = value.wrapping_add(frame_of_reference); - } -} - /// Decode one vector directly into `out` (whose length must equal the vector's -/// element count): bit-unpack -> inverse FOR -> decimal decode -> patch exceptions. +/// element count): bit-unpack -> inverse FOR + decimal decode -> patch exceptions. fn decode_vector_values( body: &Bytes, vector: &AlpEncodedVectorView, @@ -408,22 +400,25 @@ fn decode_vector_values( ) -> Result<()> { debug_assert_eq!(out.len(), vector.num_elements as usize); - let mut exact_values = bit_unpack_integers( + let exact_values = bit_unpack_integers::( body.slice(vector.packed_values_range()), vector.for_info.bit_width, vector.num_elements, )?; - inverse_for(&mut exact_values, vector.for_info.frame_of_reference); + let frame_of_reference = vector.for_info.frame_of_reference; let scale = Value::decode_scale(vector.alp_info.exponent, vector.alp_info.factor); + // Inverse FOR (`delta + frame_of_reference`) and decimal decode in one pass. for (slot, exact_value) in out.iter_mut().zip(exact_values) { - let signed_value = exact_value.reinterpret_as_signed(); + let signed_value = exact_value.wrapping_add(frame_of_reference).reinterpret_as_signed(); *slot = Value::decode_value(signed_value, scale); } // Positions and values are equal-count by construction (both sized from - // `num_exceptions`), so zipping the body sections never drops a pair. + // `num_exceptions`), so zipping the body sections never drops a pair. Every + // position was validated in bounds against `num_elements` in + // `parse_vector_view`, so indexing `out[pos]` cannot be out of range. let positions = &body[vector.exception_positions_range()]; let values = &body[vector.exception_values_range()]; for (pos_chunk, value_chunk) in positions @@ -431,13 +426,7 @@ fn decode_vector_values( .zip(values.chunks_exact(Value::Exact::WIDTH)) { let pos = u16::from_le_bytes([pos_chunk[0], pos_chunk[1]]) as usize; - if pos >= out.len() { - return Err(general_err!( - "Invalid ALP page: exception position {} out of bounds for vector length {}", - pos, - out.len() - )); - } + debug_assert!(pos < out.len()); out[pos] = Value::from_exact_bits(Value::Exact::from_le_slice(value_chunk)); } @@ -497,7 +486,6 @@ where current_offset: usize, needs_decode: bool, num_values: usize, - _marker: PhantomData, } impl AlpDecoder @@ -512,7 +500,6 @@ where current_offset: 0, needs_decode: false, num_values: 0, - _marker: PhantomData, } } @@ -1063,13 +1050,6 @@ mod tests { assert_eq!(unpacked, vec![3, 1, 2]); } - #[test] - fn test_inverse_for() { - let mut decoded = vec![0u32, 3, 2]; - inverse_for(&mut decoded, 10); - assert_eq!(decoded, vec![10, 13, 12]); - } - #[test] fn test_decode_page_values_f32_no_exceptions() { let vector = make_vector(VectorSpec { From 81c14cb323497aaa835983b05ab883edd8294bc5 Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Sun, 7 Jun 2026 16:31:20 -0400 Subject: [PATCH 35/50] lazy vector decode --- parquet/src/encodings/alp.rs | 2 +- parquet/src/encodings/decoding/alp_decoder.rs | 217 +++++++++++++----- 2 files changed, 163 insertions(+), 56 deletions(-) diff --git a/parquet/src/encodings/alp.rs b/parquet/src/encodings/alp.rs index de99ef7572ed..bc8ce2079516 100644 --- a/parquet/src/encodings/alp.rs +++ b/parquet/src/encodings/alp.rs @@ -370,7 +370,7 @@ pub(crate) const ALP_NEG_POW10_F64: [f64; 19] = [ pub(crate) trait AlpFloat: Copy + Default { type Exact: AlpExact + FromBytes; - type Scale: Copy; + type Scale: Copy + Send; /// Precompute vector-level ALP decimal scale constants for: /// `value = (encoded * 10^(factor)) * 10^(-exponent)`. diff --git a/parquet/src/encodings/decoding/alp_decoder.rs b/parquet/src/encodings/decoding/alp_decoder.rs index bdf18674b8e0..f02408c7f201 100644 --- a/parquet/src/encodings/decoding/alp_decoder.rs +++ b/parquet/src/encodings/decoding/alp_decoder.rs @@ -469,22 +469,91 @@ fn decode_page_values_into( Ok(()) } +/// Live decode state for the one vector currently being consumed. +/// +/// Holds the bit position inside that vector's packed values plus the +/// vector-level constants needed to turn each packed integer back into a float. +/// `delivered` is the vector-local index of the next element to produce, so +/// exception patches (which use vector-local positions) land in the right place +/// even when a vector is split across several `get`/`skip` calls. +struct CurrentVector { + reader: BitReader, + bit_width: u8, + frame_of_reference: Value::Exact, + scale: Value::Scale, + /// Number of this vector's elements not yet delivered or skipped. + remaining: usize, + /// Vector-local index of the next element to produce. + delivered: usize, + exception_positions: Bytes, + exception_values: Bytes, +} + +/// Decode the next `out.len()` elements of the current vector straight into +/// `out`, with no intermediate buffer: unpack one packed integer at a time, +/// apply inverse FOR + decimal decode, then patch any exceptions whose +/// vector-local position falls in the just-produced sub-range. +fn decode_range(cur: &mut CurrentVector, out: &mut [Value]) -> Result<()> { + let frame_of_reference = cur.frame_of_reference; + if cur.bit_width == 0 { + // Every packed delta is zero, so all values share `frame_of_reference`. + let signed = frame_of_reference.reinterpret_as_signed(); + out.fill(Value::decode_value(signed, cur.scale)); + } else { + let bit_width = cur.bit_width as usize; + for slot in out.iter_mut() { + let delta = cur.reader.get_value::(bit_width).ok_or_else(|| { + general_err!("Invalid ALP page: not enough packed bits to decode vector") + })?; + let signed = delta.wrapping_add(frame_of_reference).reinterpret_as_signed(); + *slot = Value::decode_value(signed, cur.scale); + } + } + + // Patch exceptions landing in `[delivered, delivered + out.len())`. Positions + // were validated in bounds when the vector was parsed, and patching is a + // positional overwrite, so it is independent of exception ordering. + let lo = cur.delivered; + let hi = cur.delivered + out.len(); + for (pos_chunk, value_chunk) in cur + .exception_positions + .chunks_exact(std::mem::size_of::()) + .zip(cur.exception_values.chunks_exact(Value::Exact::WIDTH)) + { + let pos = u16::from_le_bytes([pos_chunk[0], pos_chunk[1]]) as usize; + if (lo..hi).contains(&pos) { + out[pos - lo] = Value::from_exact_bits(Value::Exact::from_le_slice(value_chunk)); + } + } + + cur.delivered = hi; + cur.remaining -= out.len(); + Ok(()) +} + /// Decoder for ALP-encoded floating-point pages (`f32`/`f64`). /// -/// Current behavior: +/// Values are decoded directly into the caller's output buffer: /// - `set_data` parses + validates page metadata and stores ALP layout state. -/// - `get` uses a fast path to decode directly to output when all values are requested. -/// - otherwise, `get` lazily decodes the full page once, then copies from decoded buffer. -/// - `skip` advances the decoded cursor. +/// - a full read from a fresh page decodes the whole page directly into the +/// output. +/// - partial reads and skips advance a per-vector cursor, decoding each vector +/// on demand into the output as it is reached. pub(crate) struct AlpDecoder where T::T: AlpFloat, ::Exact: Send, { + /// Parsed + validated page layout, or `None` before `set_data` (and after a + /// full-read fast path consumes the page). layout: Option::Exact>>, - decoded_values: Vec, - current_offset: usize, - needs_decode: bool, + /// Index of the next vector to load once `current` is exhausted. + next_vector_idx: usize, + /// Live decode state for the vector currently being consumed. + current: Option>, + /// Whether the page is untouched, making the full-read fast path eligible. + fresh: bool, + /// Number of values still to be read from the page. num_values: usize, } @@ -496,32 +565,49 @@ where pub(crate) fn new() -> Self { Self { layout: None, - decoded_values: Vec::new(), - current_offset: 0, - needs_decode: false, + next_vector_idx: 0, + current: None, + fresh: false, num_values: 0, } } - /// Decode the stored page into `decoded_values` if it hasn't been decoded yet. - /// - /// Used by partial `get` / `skip` paths that need a stable decoded buffer. - fn ensure_decoded(&mut self) -> Result<()> { - if !self.needs_decode { - return Ok(()); - } - - let layout = self.layout.take().ok_or_else(|| { - general_err!("Invalid ALP decoder state: set_data must be called before get/skip") - })?; - - // Reuse the buffer's capacity across pages instead of reallocating. - self.decoded_values.clear(); - self.decoded_values - .resize(layout.header.num_elements, T::T::default()); - decode_page_values_into::(&layout, &mut self.decoded_values)?; - self.needs_decode = false; + /// Load the next vector into `current`: build a live bit reader over its + /// packed values and slice its exception sections out of the page body. + fn load_current_vector(&mut self) -> Result<()> { + // Pull everything we need out of the borrowed layout first, so the + // borrow ends before we mutate `self`. + let (packed, positions, values, bit_width, frame_of_reference, exponent, factor, count) = { + let layout = self.layout.as_ref().ok_or_else(|| { + general_err!("Invalid ALP decoder state: set_data must be called before get/skip") + })?; + let view = layout.vectors.get(self.next_vector_idx).ok_or_else(|| { + general_err!("Invalid ALP decoder state: no vector left to decode") + })?; + let body = &layout.body; + ( + body.slice(view.packed_values_range()), + body.slice(view.exception_positions_range()), + body.slice(view.exception_values_range()), + view.for_info.bit_width, + view.for_info.frame_of_reference, + view.alp_info.exponent, + view.alp_info.factor, + view.num_elements as usize, + ) + }; + self.current = Some(CurrentVector { + reader: BitReader::new(packed), + bit_width, + frame_of_reference, + scale: ::decode_scale(exponent, factor), + remaining: count, + delivered: 0, + exception_positions: positions, + exception_values: values, + }); + self.next_vector_idx += 1; Ok(()) } } @@ -533,7 +619,7 @@ where { /// Store validated page layout and reset read cursor state. /// - /// Actual value decoding is deferred until first `get`/`skip`. + /// Values are decoded lazily, on the first `get`/`skip`. fn set_data(&mut self, data: Bytes, num_values: usize) -> Result<()> { let layout = parse_alp_page_layout::<::Exact>(data)?; @@ -546,43 +632,50 @@ where } self.layout = Some(layout); - self.decoded_values.clear(); - self.current_offset = 0; - self.needs_decode = num_values > 0; + self.next_vector_idx = 0; + self.current = None; + self.fresh = num_values > 0; self.num_values = num_values; Ok(()) } /// Read up to `buffer.len()` decoded values. /// - /// Fast path: if caller requests all remaining values from a fresh page, - /// decode directly into `buffer` (matching C++ ALP decoder behavior). - /// Otherwise decode once into internal storage and copy slices from there. + /// Fast path: a full read from an untouched page decodes the whole page + /// straight into `buffer`. Otherwise the per-vector cursor decodes each + /// vector on demand directly into `buffer`. fn get(&mut self, buffer: &mut [T::T]) -> Result { let target = buffer.len().min(self.num_values); if target == 0 { return Ok(0); } - // C++ parity fast path: decode directly into caller output when it asks - // for all remaining values from a fresh page. - if self.needs_decode && target == self.num_values { + // Fast path: caller asks for the whole page from an untouched page, so + // decode every vector directly into the caller's output in one shot. + if self.fresh && target == self.num_values { let layout = self.layout.take().ok_or_else(|| { general_err!("Invalid ALP decoder state: set_data must be called before get/skip") })?; decode_page_values_into::(&layout, &mut buffer[..target])?; - self.needs_decode = false; - self.decoded_values.clear(); - self.current_offset = 0; + self.fresh = false; + self.current = None; + self.next_vector_idx = 0; self.num_values = 0; return Ok(target); } - self.ensure_decoded()?; - let end = self.current_offset + target; - buffer[..target].copy_from_slice(&self.decoded_values[self.current_offset..end]); - self.current_offset = end; - self.num_values -= target; + self.fresh = false; + let mut written = 0; + while written < target { + if self.current.as_ref().is_none_or(|c| c.remaining == 0) { + self.load_current_vector()?; + } + let cur = self.current.as_mut().unwrap(); + let n = cur.remaining.min(target - written); + decode_range::(cur, &mut buffer[written..written + n])?; + written += n; + self.num_values -= n; + } Ok(target) } @@ -594,19 +687,30 @@ where Encoding::ALP } - /// Skip up to `num_values` decoded values. - /// - /// For parity with C++ partial-read behavior, this may trigger deferred - /// page decoding before advancing the cursor. + /// Skip up to `num_values` values, advancing the per-vector cursor (and the + /// underlying bit reader) without decoding. fn skip(&mut self, num_values: usize) -> Result { let to_skip = num_values.min(self.num_values); if to_skip == 0 { return Ok(0); } - self.ensure_decoded()?; - self.current_offset += to_skip; - self.num_values -= to_skip; + self.fresh = false; + let mut skipped = 0; + while skipped < to_skip { + if self.current.as_ref().is_none_or(|c| c.remaining == 0) { + self.load_current_vector()?; + } + let cur = self.current.as_mut().unwrap(); + let n = cur.remaining.min(to_skip - skipped); + if cur.bit_width != 0 { + cur.reader.skip(n, cur.bit_width as usize); + } + cur.delivered += n; + cur.remaining -= n; + skipped += n; + self.num_values -= n; + } Ok(to_skip) } } @@ -1282,8 +1386,11 @@ mod tests { ] ); assert_eq!(decoder.values_left(), 0); - assert!(!decoder.needs_decode); - assert!(decoder.decoded_values.is_empty()); + assert!(decoder.current.is_none()); assert!(decoder.layout.is_none()); + + // The exhausted decoder yields nothing more. + let mut extra = [0.0f32; 1]; + assert_eq!(decoder.get(&mut extra).unwrap(), 0); } } From 93eb92511cd319d3c17a58d3ac250278ad783e63 Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Sun, 7 Jun 2026 21:31:54 -0400 Subject: [PATCH 36/50] even lazier decode --- parquet/src/encodings/alp.rs | 9 - parquet/src/encodings/decoding/alp_decoder.rs | 551 ++++++------------ 2 files changed, 175 insertions(+), 385 deletions(-) diff --git a/parquet/src/encodings/alp.rs b/parquet/src/encodings/alp.rs index bc8ce2079516..8d7535c4ba9c 100644 --- a/parquet/src/encodings/alp.rs +++ b/parquet/src/encodings/alp.rs @@ -248,7 +248,6 @@ pub(crate) trait AlpExact: Copy + std::fmt::Debug + FromBitpacked { const WIDTH: usize; type Signed: Copy; fn from_le_slice(slice: &[u8]) -> Self; - fn zero() -> Self; fn wrapping_add(self, rhs: Self) -> Self; fn reinterpret_as_signed(self) -> Self::Signed; } @@ -261,10 +260,6 @@ impl AlpExact for u32 { u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]]) } - fn zero() -> Self { - 0 - } - fn wrapping_add(self, rhs: Self) -> Self { self.wrapping_add(rhs) } @@ -284,10 +279,6 @@ impl AlpExact for u64 { ]) } - fn zero() -> Self { - 0 - } - fn wrapping_add(self, rhs: Self) -> Self { self.wrapping_add(rhs) } diff --git a/parquet/src/encodings/decoding/alp_decoder.rs b/parquet/src/encodings/decoding/alp_decoder.rs index f02408c7f201..c713c16f7422 100644 --- a/parquet/src/encodings/decoding/alp_decoder.rs +++ b/parquet/src/encodings/decoding/alp_decoder.rs @@ -28,7 +28,7 @@ use crate::encodings::alp::{ }; use crate::encodings::decoding::Decoder; use crate::errors::{ParquetError, Result}; -use crate::util::bit_util::{BitReader, FromBitpacked, FromBytes}; +use crate::util::bit_util::BitReader; /// Parsed view of one vector's metadata and data sections. /// @@ -76,25 +76,10 @@ impl AlpEncodedVectorView { } } -/// Parsed ALP page layout for one exact integer width (`u32` for float pages, -/// `u64` for double pages). -#[derive(Debug)] -struct AlpPageLayout { - header: AlpHeader, - body: Bytes, - vectors: Vec>, -} - -/// Parse and validate a full ALP-encoded page body. -/// -/// Validation includes: -/// - header decoding via [`AlpHeader::deserialize`] -/// - supported compression mode, integer encoding, and vector-size range -/// - offsets bounds + monotonicity -/// - per-vector metadata/data section lengths -fn parse_alp_page_layout(data: Bytes) -> Result> { - let data_ref = data.as_ref(); - let header = AlpHeader::deserialize(data_ref)?; +/// Parse and validate the 7-byte ALP page header: compression mode, integer +/// encoding, and vector-size range. +fn parse_alp_page_header(data: &[u8]) -> Result { + let header = AlpHeader::deserialize(data)?; if header.compression_mode != ALP_COMPRESSION_MODE { return Err(general_err!( @@ -102,14 +87,12 @@ fn parse_alp_page_layout(data: Bytes) -> Result(data: Bytes) -> Result()) - .ok_or_else(|| general_err!("Invalid ALP page: offsets length overflow"))?; - let offsets_end = ALP_HEADER_SIZE - .checked_add(offsets_len) - .ok_or_else(|| general_err!("Invalid ALP page: header + offsets length overflow"))?; - - if data_ref.len() < offsets_end { - return Err(general_err!( - "Invalid ALP page: expected at least {} bytes for {} offsets, got {}", - offsets_end, - num_vectors, - data_ref.len() - )); - } - - let body = data.slice(ALP_HEADER_SIZE..); - let body_ref = body.as_ref(); - let body_len = body_ref.len(); - let offsets_section_size = num_vectors * std::mem::size_of::(); - - let mut offsets = Vec::with_capacity(num_vectors); - for i in 0..num_vectors { - let start = ALP_HEADER_SIZE + i * 4; - let offset = u32::from_le_bytes([ - data_ref[start], - data_ref[start + 1], - data_ref[start + 2], - data_ref[start + 3], - ]); - - if offset as usize >= body_len { - return Err(general_err!( - "Invalid ALP page: vector offset {} out of bounds for body length {}", - offset, - body_len - )); - } - - if (offset as usize) < offsets_section_size { - return Err(general_err!( - "Invalid ALP page: vector offset {} points into offsets section {}", - offset, - offsets_section_size - )); - } - - offsets.push(offset); - } - - let mut vectors = Vec::with_capacity(num_vectors); - let mut expected_next_offset = offsets_section_size; - for (vector_idx, vector_offset) in offsets.iter().enumerate() { - let vector_start = *vector_offset as usize; - if vector_start != expected_next_offset { - return Err(general_err!( - "Invalid ALP page: vector offset {} at index {} does not match expected {}", - vector_start, - vector_idx, - expected_next_offset - )); - } - - let vector_end = if vector_idx + 1 < offsets.len() { - offsets[vector_idx + 1] as usize - } else { - body_len - }; - - if vector_end < vector_start { - return Err(general_err!( - "Invalid ALP page: vector offsets are not monotonic at index {}", - vector_idx - )); - } - - let vector_num_elements = header.vector_num_elements(vector_idx); - let vector = - parse_vector_view::(body_ref, vector_start, vector_end, vector_num_elements)?; - expected_next_offset = vector_start - .checked_add(vector.expected_stored_size()) - .ok_or_else(|| { - general_err!("Invalid ALP page: expected next vector offset overflow") - })?; - vectors.push(vector); - } - - if expected_next_offset != body_len { - return Err(general_err!( - "Invalid ALP page: body size {} does not match expected {} (offsets + vectors)", - body_len, - expected_next_offset - )); - } + Ok(header) +} - Ok(AlpPageLayout { - header, - body, - vectors, - }) +/// Read the little-endian `u32` vector offset at index `idx` from the offsets +/// section at the start of the page body. +fn read_offset(body: &[u8], idx: usize) -> Result { + let start = idx * std::mem::size_of::(); + let bytes = body + .get(start..start + std::mem::size_of::()) + .ok_or_else(|| general_err!("Invalid ALP page: offset index {} out of bounds", idx))?; + Ok(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize) } /// Parse a single vector section: @@ -359,116 +251,6 @@ fn parse_vector_view( }) } -/// Decode bit-packed deltas into exact integers. -fn bit_unpack_integers( - packed_values: Bytes, - bit_width: u8, - num_elements: u16, -) -> Result> { - if bit_width as usize > Exact::WIDTH * 8 { - return Err(general_err!( - "Invalid ALP page: bit width {} exceeds {}", - bit_width, - Exact::WIDTH * 8 - )); - } - - if bit_width == 0 { - return Ok(vec![Exact::zero(); num_elements as usize]); - } - - let mut out = vec![Exact::zero(); num_elements as usize]; - let mut reader = BitReader::new(packed_values); - let read = reader.get_batch::(&mut out, bit_width as usize); - if read != out.len() { - return Err(general_err!( - "Invalid ALP page: bit unpack read {} values, expected {}", - read, - out.len() - )); - } - - Ok(out) -} - -/// Decode one vector directly into `out` (whose length must equal the vector's -/// element count): bit-unpack -> inverse FOR + decimal decode -> patch exceptions. -fn decode_vector_values( - body: &Bytes, - vector: &AlpEncodedVectorView, - out: &mut [Value], -) -> Result<()> { - debug_assert_eq!(out.len(), vector.num_elements as usize); - - let exact_values = bit_unpack_integers::( - body.slice(vector.packed_values_range()), - vector.for_info.bit_width, - vector.num_elements, - )?; - - let frame_of_reference = vector.for_info.frame_of_reference; - let scale = Value::decode_scale(vector.alp_info.exponent, vector.alp_info.factor); - - // Inverse FOR (`delta + frame_of_reference`) and decimal decode in one pass. - for (slot, exact_value) in out.iter_mut().zip(exact_values) { - let signed_value = exact_value.wrapping_add(frame_of_reference).reinterpret_as_signed(); - *slot = Value::decode_value(signed_value, scale); - } - - // Positions and values are equal-count by construction (both sized from - // `num_exceptions`), so zipping the body sections never drops a pair. Every - // position was validated in bounds against `num_elements` in - // `parse_vector_view`, so indexing `out[pos]` cannot be out of range. - let positions = &body[vector.exception_positions_range()]; - let values = &body[vector.exception_values_range()]; - for (pos_chunk, value_chunk) in positions - .chunks_exact(std::mem::size_of::()) - .zip(values.chunks_exact(Value::Exact::WIDTH)) - { - let pos = u16::from_le_bytes([pos_chunk[0], pos_chunk[1]]) as usize; - debug_assert!(pos < out.len()); - out[pos] = Value::from_exact_bits(Value::Exact::from_le_slice(value_chunk)); - } - - Ok(()) -} - -/// Decode a full ALP page into an existing output slice. -/// -/// This walks pre-parsed vector views from `layout` and decodes into `out`. -fn decode_page_values_into( - layout: &AlpPageLayout, - out: &mut [Value], -) -> Result<()> { - let total = layout.header.num_elements; - if out.len() < total { - return Err(general_err!( - "Invalid ALP decode output: output length {} smaller than page values {}", - out.len(), - total - )); - } - - let mut output_offset = 0usize; - for vector in &layout.vectors { - let next_offset = output_offset + vector.num_elements as usize; - decode_vector_values::( - &layout.body, - vector, - &mut out[output_offset..next_offset], - )?; - output_offset = next_offset; - } - if output_offset != total { - return Err(general_err!( - "Invalid ALP decode output: decoded {} values, expected {}", - output_offset, - total - )); - } - Ok(()) -} - /// Live decode state for the one vector currently being consumed. /// /// Holds the bit position inside that vector's packed values plus the @@ -533,26 +315,25 @@ fn decode_range(cur: &mut CurrentVector, out: &mut [Valu /// Decoder for ALP-encoded floating-point pages (`f32`/`f64`). /// -/// Values are decoded directly into the caller's output buffer: -/// - `set_data` parses + validates page metadata and stores ALP layout state. -/// - a full read from a fresh page decodes the whole page directly into the -/// output. -/// - partial reads and skips advance a per-vector cursor, decoding each vector -/// on demand into the output as it is reached. +/// Values are decoded directly into the caller's output buffer, one vector at a +/// time: `set_data` validates the page header, and each vector is parsed and +/// decoded on demand as the read cursor reaches it. pub(crate) struct AlpDecoder where T::T: AlpFloat, ::Exact: Send, { - /// Parsed + validated page layout, or `None` before `set_data` (and after a - /// full-read fast path consumes the page). - layout: Option::Exact>>, - /// Index of the next vector to load once `current` is exhausted. + /// Validated page header; drives vector sizing and count. + header: AlpHeader, + /// Page body following the 7-byte header: `[offsets][vector data...]`. + body: Bytes, + /// Index of the next vector to parse once `current` is exhausted. next_vector_idx: usize, + /// Body offset where the next vector must begin; offsets are validated to be + /// contiguous as vectors are parsed. + expected_next_offset: usize, /// Live decode state for the vector currently being consumed. current: Option>, - /// Whether the page is untouched, making the full-read fast path eligible. - fresh: bool, /// Number of values still to be read from the page. num_values: usize, } @@ -564,48 +345,72 @@ where { pub(crate) fn new() -> Self { Self { - layout: None, + // Benign placeholder header; replaced on the first `set_data`. The + // cursor never consults it while `num_values == 0`. + header: AlpHeader { + compression_mode: 0, + integer_encoding: 0, + vector_size: 1, + num_elements: 0, + }, + body: Bytes::new(), next_vector_idx: 0, + expected_next_offset: 0, current: None, - fresh: false, num_values: 0, } } - /// Load the next vector into `current`: build a live bit reader over its - /// packed values and slice its exception sections out of the page body. + /// Parse the next vector on demand and install it as `current`: validate its + /// offset is contiguous with the previous vector, parse its metadata, build a + /// live bit reader over its packed values, and slice out its exception + /// sections. fn load_current_vector(&mut self) -> Result<()> { - // Pull everything we need out of the borrowed layout first, so the - // borrow ends before we mutate `self`. - let (packed, positions, values, bit_width, frame_of_reference, exponent, factor, count) = { - let layout = self.layout.as_ref().ok_or_else(|| { - general_err!("Invalid ALP decoder state: set_data must be called before get/skip") - })?; - let view = layout.vectors.get(self.next_vector_idx).ok_or_else(|| { - general_err!("Invalid ALP decoder state: no vector left to decode") - })?; - let body = &layout.body; - ( - body.slice(view.packed_values_range()), - body.slice(view.exception_positions_range()), - body.slice(view.exception_values_range()), - view.for_info.bit_width, - view.for_info.frame_of_reference, - view.alp_info.exponent, - view.alp_info.factor, - view.num_elements as usize, - ) + let idx = self.next_vector_idx; + let num_vectors = self.header.num_vectors(); + debug_assert!(idx < num_vectors, "load_current_vector with no vector left"); + + let body_ref = self.body.as_ref(); + let start = read_offset(body_ref, idx)?; + if start != self.expected_next_offset { + return Err(general_err!( + "Invalid ALP page: vector offset {} at index {} does not match expected {}", + start, + idx, + self.expected_next_offset + )); + } + let end = if idx + 1 < num_vectors { + read_offset(body_ref, idx + 1)? + } else { + body_ref.len() }; + if end < start || end > body_ref.len() { + return Err(general_err!( + "Invalid ALP page: vector offset {} out of bounds at index {}", + end, + idx + )); + } + + let count = self.header.vector_num_elements(idx); + let view = parse_vector_view::<::Exact>(body_ref, start, end, count)?; + // The next vector must start exactly where this one ends. + self.expected_next_offset = start + view.expected_stored_size(); + + let packed = self.body.slice(view.packed_values_range()); + let exception_positions = self.body.slice(view.exception_positions_range()); + let exception_values = self.body.slice(view.exception_values_range()); self.current = Some(CurrentVector { reader: BitReader::new(packed), - bit_width, - frame_of_reference, - scale: ::decode_scale(exponent, factor), - remaining: count, + bit_width: view.for_info.bit_width, + frame_of_reference: view.for_info.frame_of_reference, + scale: ::decode_scale(view.alp_info.exponent, view.alp_info.factor), + remaining: count as usize, delivered: 0, - exception_positions: positions, - exception_values: values, + exception_positions, + exception_values, }); self.next_vector_idx += 1; Ok(()) @@ -617,54 +422,52 @@ where T::T: AlpFloat, ::Exact: Send, { - /// Store validated page layout and reset read cursor state. - /// - /// Values are decoded lazily, on the first `get`/`skip`. + /// Validate the page header and reset the read cursor. Vectors are parsed + /// and decoded lazily, on the first `get`/`skip`. fn set_data(&mut self, data: Bytes, num_values: usize) -> Result<()> { - let layout = parse_alp_page_layout::<::Exact>(data)?; + let header = parse_alp_page_header(data.as_ref())?; - if layout.header.num_elements != num_values { + if header.num_elements != num_values { return Err(general_err!( "Invalid ALP page: header num_elements {} does not match page num_values {}", - layout.header.num_elements, + header.num_elements, num_values )); } - self.layout = Some(layout); + let offsets_section_size = header + .num_vectors() + .checked_mul(std::mem::size_of::()) + .ok_or_else(|| general_err!("Invalid ALP page: offsets length overflow"))?; + + // `parse_alp_page_header` guarantees `data.len() >= ALP_HEADER_SIZE`. + let body = data.slice(ALP_HEADER_SIZE..); + if body.len() < offsets_section_size { + return Err(general_err!( + "Invalid ALP page: expected at least {} bytes for {} offsets, got {}", + offsets_section_size, + header.num_vectors(), + body.len() + )); + } + + self.header = header; + self.body = body; self.next_vector_idx = 0; + self.expected_next_offset = offsets_section_size; self.current = None; - self.fresh = num_values > 0; self.num_values = num_values; Ok(()) } - /// Read up to `buffer.len()` decoded values. - /// - /// Fast path: a full read from an untouched page decodes the whole page - /// straight into `buffer`. Otherwise the per-vector cursor decodes each - /// vector on demand directly into `buffer`. + /// Read up to `buffer.len()` values, decoding each vector on demand directly + /// into `buffer`. fn get(&mut self, buffer: &mut [T::T]) -> Result { let target = buffer.len().min(self.num_values); if target == 0 { return Ok(0); } - // Fast path: caller asks for the whole page from an untouched page, so - // decode every vector directly into the caller's output in one shot. - if self.fresh && target == self.num_values { - let layout = self.layout.take().ok_or_else(|| { - general_err!("Invalid ALP decoder state: set_data must be called before get/skip") - })?; - decode_page_values_into::(&layout, &mut buffer[..target])?; - self.fresh = false; - self.current = None; - self.next_vector_idx = 0; - self.num_values = 0; - return Ok(target); - } - - self.fresh = false; let mut written = 0; while written < target { if self.current.as_ref().is_none_or(|c| c.remaining == 0) { @@ -695,7 +498,6 @@ where return Ok(0); } - self.fresh = false; let mut skipped = 0; while skipped < to_skip { if self.current.as_ref().is_none_or(|c| c.remaining == 0) { @@ -718,7 +520,7 @@ where #[cfg(test)] mod tests { use super::*; - use crate::data_type::FloatType; + use crate::data_type::{DoubleType, FloatType}; use crate::encodings::alp::{ ALP_NEG_POW10_F32, ALP_NEG_POW10_F64, ALP_POW10_F32, ALP_POW10_F64, }; @@ -790,12 +592,36 @@ mod tests { out } - fn decode_page(layout: &AlpPageLayout) -> Vec { - let mut out = vec![Value::default(); layout.header.num_elements]; - decode_page_values_into::(layout, &mut out).unwrap(); + /// Decode a full page through the streaming decoder. + fn decode_page(page: Vec, num_values: usize) -> Vec + where + T::T: AlpFloat, + ::Exact: Send, + { + let mut decoder = AlpDecoder::::new(); + decoder.set_data(Bytes::from(page), num_values).unwrap(); + let mut out = vec![T::T::default(); num_values]; + assert_eq!(decoder.get(&mut out).unwrap(), num_values); out } + /// Drive the streaming decoder to its first error. Header errors surface + /// from `set_data`; per-vector and offset errors surface from `get`. + fn decode_err(page: Vec, num_values: usize) -> ParquetError + where + T::T: AlpFloat, + ::Exact: Send, + { + let mut decoder = AlpDecoder::::new(); + match decoder.set_data(Bytes::from(page), num_values) { + Err(e) => e, + Ok(()) => { + let mut out = vec![T::T::default(); num_values]; + decoder.get(&mut out).unwrap_err() + } + } + } + fn make_page_from_vectors( log_vector_size: u8, num_elements: i32, @@ -897,18 +723,16 @@ mod tests { } #[test] - fn test_parse_alp_page_layout_valid() { + fn test_decode_valid_page() { let data = make_alp_page_bytes(0, 0, 3, 4, &[4], 13); - let parsed = parse_alp_page_layout::(Bytes::from(data)).unwrap(); - assert_eq!(parsed.header.num_elements, 4); - assert_eq!(parsed.vectors.len(), 1); - assert_eq!(parsed.vectors[0].num_elements, 4); + let decoded = decode_page::(data, 4); + assert_eq!(decoded, vec![0.0_f64; 4]); } #[test] - fn test_parse_alp_page_layout_too_small_log_vector_size() { + fn test_set_data_rejects_small_vector_size() { let data = make_alp_page_bytes(0, 0, 2, 1, &[4], 8); - let err = parse_alp_page_layout::(Bytes::from(data)).unwrap_err(); + let err = decode_err::(data, 1); assert!( err.to_string() .contains("Invalid ALP page: log_vector_size 2 below min 3") @@ -916,9 +740,9 @@ mod tests { } #[test] - fn test_parse_alp_page_layout_too_big_log_vector_size() { + fn test_set_data_rejects_big_vector_size() { let data = make_alp_page_bytes(0, 0, 16, 1, &[4], 8); - let err = parse_alp_page_layout::(Bytes::from(data)).unwrap_err(); + let err = decode_err::(data, 1); assert!( err.to_string() .contains("Invalid ALP page: log_vector_size 16 exceeds max 15") @@ -926,9 +750,9 @@ mod tests { } #[test] - fn test_parse_alp_page_layout_invalid_integer_encoding() { + fn test_set_data_rejects_integer_encoding() { let data = make_alp_page_bytes(0, 1, 3, 1, &[4], 8); - let err = parse_alp_page_layout::(Bytes::from(data)).unwrap_err(); + let err = decode_err::(data, 1); assert!( err.to_string() .contains("Invalid ALP page: unsupported integer encoding 1") @@ -936,7 +760,7 @@ mod tests { } #[test] - fn test_parse_alp_page_layout_invalid_exponent_f32() { + fn test_decode_rejects_invalid_exponent_f32() { let vector = make_vector(VectorSpec { exponent: 11, factor: 0, @@ -947,7 +771,7 @@ mod tests { exception_values: &[], }); let page = make_page_from_vectors(3, 1, &[vector]); - let err = parse_alp_page_layout::(Bytes::from(page)).unwrap_err(); + let err = decode_err::(page, 1); assert!( err.to_string() .contains("Invalid ALP page: exponent 11 exceeds max 10") @@ -955,7 +779,7 @@ mod tests { } #[test] - fn test_parse_alp_page_layout_invalid_factor_f32() { + fn test_decode_rejects_invalid_factor_f32() { let vector = make_vector(VectorSpec { exponent: 0, factor: 11, @@ -966,7 +790,7 @@ mod tests { exception_values: &[], }); let page = make_page_from_vectors(3, 1, &[vector]); - let err = parse_alp_page_layout::(Bytes::from(page)).unwrap_err(); + let err = decode_err::(page, 1); assert!( err.to_string() .contains("Invalid ALP page: factor 11 exceeds exponent 0") @@ -974,7 +798,7 @@ mod tests { } #[test] - fn test_parse_alp_page_layout_factor_exceeds_exponent() { + fn test_decode_rejects_factor_exceeds_exponent() { let vector = make_vector(VectorSpec { exponent: 2, factor: 3, @@ -985,7 +809,7 @@ mod tests { exception_values: &[], }); let page = make_page_from_vectors(3, 1, &[vector]); - let err = parse_alp_page_layout::(Bytes::from(page)).unwrap_err(); + let err = decode_err::(page, 1); assert!( err.to_string() .contains("Invalid ALP page: factor 3 exceeds exponent 2") @@ -993,7 +817,7 @@ mod tests { } #[test] - fn test_parse_alp_page_layout_invalid_num_exceptions() { + fn test_decode_rejects_invalid_num_exceptions() { let vector = make_vector(VectorSpec { exponent: 0, factor: 0, @@ -1004,7 +828,7 @@ mod tests { exception_values: &[0, 0], }); let page = make_page_from_vectors(3, 1, &[vector]); - let err = parse_alp_page_layout::(Bytes::from(page)).unwrap_err(); + let err = decode_err::(page, 1); assert!( err.to_string() .contains("Invalid ALP page: num_exceptions 2 exceeds vector num_elements 1") @@ -1012,7 +836,7 @@ mod tests { } #[test] - fn test_parse_alp_page_layout_invalid_exception_position() { + fn test_decode_rejects_invalid_exception_position() { let vector = make_vector(VectorSpec { exponent: 0, factor: 0, @@ -1023,7 +847,7 @@ mod tests { exception_values: &[123], }); let page = make_page_from_vectors(3, 1, &[vector]); - let err = parse_alp_page_layout::(Bytes::from(page)).unwrap_err(); + let err = decode_err::(page, 1); assert!( err.to_string().contains( "Invalid ALP page: exception position 1 out of bounds for vector length 1" @@ -1032,9 +856,9 @@ mod tests { } #[test] - fn test_parse_alp_page_layout_non_monotonic_offsets() { + fn test_decode_rejects_non_contiguous_offset() { let data = make_alp_page_bytes(0, 0, 3, 9, &[12, 8], 12); - let err = parse_alp_page_layout::(Bytes::from(data)).unwrap_err(); + let err = decode_err::(data, 9); assert!( err.to_string().contains( "Invalid ALP page: vector offset 12 at index 0 does not match expected 8" @@ -1043,7 +867,7 @@ mod tests { } #[test] - fn test_parse_alp_page_layout_truncated_vector_data() { + fn test_decode_rejects_truncated_vector() { let mut vector = Vec::new(); vector.push(0); vector.push(0); @@ -1051,7 +875,7 @@ mod tests { vector.extend_from_slice(&10u32.to_le_bytes()); vector.push(1); let page = make_page_from_vectors(3, 2, &[vector]); - let err = parse_alp_page_layout::(Bytes::from(page)).unwrap_err(); + let err = decode_err::(page, 2); assert!( err.to_string() .contains("Invalid ALP page: vector data too short") @@ -1059,7 +883,7 @@ mod tests { } #[test] - fn test_parse_alp_page_layout_vector_data_too_long() { + fn test_decode_rejects_vector_too_long() { let mut vector = make_vector(VectorSpec { exponent: 0, factor: 0, @@ -1072,7 +896,7 @@ mod tests { vector.push(0xAB); let page = make_page_from_vectors(3, 1, &[vector]); - let err = parse_alp_page_layout::(Bytes::from(page)).unwrap_err(); + let err = decode_err::(page, 1); assert!( err.to_string() .contains("Invalid ALP page: vector data too long, expected 9 bytes, got 10") @@ -1080,7 +904,7 @@ mod tests { } #[test] - fn test_parse_alp_page_layout_body_size_mismatch_unclaimed_bytes() { + fn test_decode_rejects_unclaimed_bytes() { let vector = make_vector(VectorSpec { exponent: 0, factor: 0, @@ -1097,7 +921,7 @@ mod tests { page.push(0); page.extend_from_slice(&vector); - let err = parse_alp_page_layout::(Bytes::from(page)).unwrap_err(); + let err = decode_err::(page, 1); assert!( err.to_string() .contains("Invalid ALP page: vector offset 5 at index 0 does not match expected 4") @@ -1105,7 +929,7 @@ mod tests { } #[test] - fn test_parse_alp_page_layout_parses_vector_view_data_only_f64() { + fn test_parse_vector_view_exception_sections() { let mut vector = Vec::new(); vector.push(2); @@ -1118,22 +942,17 @@ mod tests { vector.extend_from_slice(&0u16.to_le_bytes()); vector.extend_from_slice(&42.5_f64.to_le_bytes()); - let offsets = [4u32]; - let mut page = make_alp_page_bytes(0, 0, 3, 1, &offsets, 0); - page.extend_from_slice(&vector); + let body = Bytes::from(vector); + let view = parse_vector_view::(body.as_ref(), 0, body.len(), 1).unwrap(); + assert_eq!(view.num_elements, 1); + assert_eq!(view.alp_info.num_exceptions, 1); + assert_eq!(view.for_info.bit_width, 0); - let parsed = parse_alp_page_layout::(Bytes::from(page)).unwrap(); - assert_eq!(parsed.vectors.len(), 1); - let parsed_vector = &parsed.vectors[0]; - assert_eq!(parsed_vector.num_elements, 1); - assert_eq!(parsed_vector.alp_info.num_exceptions, 1); - assert_eq!(parsed_vector.for_info.bit_width, 0); - - let positions: Vec = parsed.body[parsed_vector.exception_positions_range()] + let positions: Vec = body[view.exception_positions_range()] .chunks_exact(2) .map(|c| u16::from_le_bytes([c[0], c[1]])) .collect(); - let values: Vec = parsed.body[parsed_vector.exception_values_range()] + let values: Vec = body[view.exception_values_range()] .chunks_exact(8) .map(|c| u64::from_le_bytes(c.try_into().unwrap())) .collect(); @@ -1141,19 +960,6 @@ mod tests { assert_eq!(values, vec![42.5_f64.to_bits()]); } - #[test] - fn test_bit_unpack_integers_width_zero() { - let unpacked = bit_unpack_integers::(Bytes::new(), 0, 3).unwrap(); - assert_eq!(unpacked, vec![0, 0, 0]); - } - - #[test] - fn test_bit_unpack_integers_width_two() { - let unpacked = - bit_unpack_integers::(Bytes::from_static(&[0b0010_0111]), 2, 3).unwrap(); - assert_eq!(unpacked, vec![3, 1, 2]); - } - #[test] fn test_decode_page_values_f32_no_exceptions() { let vector = make_vector(VectorSpec { @@ -1166,8 +972,7 @@ mod tests { exception_values: &[], }); let page = make_page_from_vectors(3, 4, &[vector]); - let layout = parse_alp_page_layout::(Bytes::from(page)).unwrap(); - let decoded = decode_page::(&layout); + let decoded = decode_page::(page, 4); assert_eq!(decoded, vec![10.0, 11.0, 12.0, 13.0]); } @@ -1192,8 +997,7 @@ mod tests { exception_values: &[], }); let page = make_page_from_vectors(3, 9, &[vector0, vector1]); - let layout = parse_alp_page_layout::(Bytes::from(page)).unwrap(); - let decoded = decode_page::(&layout); + let decoded = decode_page::(page, 9); assert_eq!( decoded, vec![10.0, 42.5, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 7.0] @@ -1217,8 +1021,7 @@ mod tests { exception_values: &edge_values, }); let page = make_page_from_vectors(3, 3, &[vector]); - let layout = parse_alp_page_layout::(Bytes::from(page)).unwrap(); - let decoded = decode_page::(&layout); + let decoded = decode_page::(page, 3); assert!(decoded[0].is_nan()); assert_eq!(decoded[1], -0.0); @@ -1239,8 +1042,7 @@ mod tests { exception_values: &[], }); let page = make_page_from_vectors(3, 1, &[vector]); - let layout = parse_alp_page_layout::(Bytes::from(page)).unwrap(); - let decoded = decode_page::(&layout); + let decoded = decode_page::(page, 1); let expected_two_step = ((encoded as f32) * ALP_POW10_F32[1]) * ALP_NEG_POW10_F32[1]; let one_step_scale = ALP_POW10_F32[1] * ALP_NEG_POW10_F32[1]; @@ -1263,8 +1065,7 @@ mod tests { exception_values: &[], }); let page = make_page_from_vectors(3, 1, &[vector]); - let layout = parse_alp_page_layout::(Bytes::from(page)).unwrap(); - let decoded = decode_page::(&layout); + let decoded = decode_page::(page, 1); let expected_two_step = ((encoded as f64) * ALP_POW10_F64[1]) * ALP_NEG_POW10_F64[1]; let one_step_scale = ALP_POW10_F64[1] * ALP_NEG_POW10_F64[1]; @@ -1352,7 +1153,7 @@ mod tests { } #[test] - fn test_alp_decoder_get_fast_path_full_read() { + fn test_alp_decoder_get_full_read() { let vector0 = make_vector(VectorSpec { exponent: 0, factor: 0, @@ -1386,8 +1187,6 @@ mod tests { ] ); assert_eq!(decoder.values_left(), 0); - assert!(decoder.current.is_none()); - assert!(decoder.layout.is_none()); // The exhausted decoder yields nothing more. let mut extra = [0.0f32; 1]; From ad84bde5bbe54887d04e28b419ad56fb3d95ec7f Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Mon, 8 Jun 2026 11:03:36 -0400 Subject: [PATCH 37/50] use `BitReader::get_batch` to support autovectorization with a scratch buffer --- parquet/src/encodings/alp.rs | 5 +- parquet/src/encodings/decoding/alp_decoder.rs | 244 ++++++++++++++++-- 2 files changed, 233 insertions(+), 16 deletions(-) diff --git a/parquet/src/encodings/alp.rs b/parquet/src/encodings/alp.rs index 8d7535c4ba9c..e7c5b54be395 100644 --- a/parquet/src/encodings/alp.rs +++ b/parquet/src/encodings/alp.rs @@ -45,6 +45,9 @@ pub(crate) const ALP_COMPRESSION_MODE: u8 = 0; pub(crate) const ALP_INTEGER_ENCODING_FOR_BIT_PACK: u8 = 0; pub(crate) const ALP_MIN_LOG_VECTOR_SIZE: u8 = 3; pub(crate) const ALP_MAX_LOG_VECTOR_SIZE: u8 = 15; +/// Spec-recommended default `log_vector_size`: 1024-value vectors, the canonical +/// ALP/FastLanes vector size. +pub(crate) const ALP_DEFAULT_LOG_VECTOR_SIZE: u8 = 10; pub(crate) const ALP_MAX_EXPONENT_F32: u8 = 10; pub(crate) const ALP_MAX_EXPONENT_F64: u8 = 18; @@ -244,7 +247,7 @@ impl ForInfo { /// - FOR stores non-negative deltas optimized for bitpacking. /// - Unsigned arithmetic avoids signed-overflow edge cases in FOR stage. /// - Signed interpretation is applied later during decimal reconstruction. -pub(crate) trait AlpExact: Copy + std::fmt::Debug + FromBitpacked { +pub(crate) trait AlpExact: Copy + std::fmt::Debug + FromBitpacked + Default { const WIDTH: usize; type Signed: Copy; fn from_le_slice(slice: &[u8]) -> Self; diff --git a/parquet/src/encodings/decoding/alp_decoder.rs b/parquet/src/encodings/decoding/alp_decoder.rs index c713c16f7422..c119eee19bb0 100644 --- a/parquet/src/encodings/decoding/alp_decoder.rs +++ b/parquet/src/encodings/decoding/alp_decoder.rs @@ -22,9 +22,10 @@ use bytes::Bytes; use crate::basic::Encoding; use crate::data_type::DataType; use crate::encodings::alp::{ - ALP_COMPRESSION_MODE, ALP_HEADER_SIZE, ALP_INTEGER_ENCODING_FOR_BIT_PACK, ALP_MAX_EXPONENT_F32, - ALP_MAX_EXPONENT_F64, ALP_MAX_LOG_VECTOR_SIZE, ALP_MIN_LOG_VECTOR_SIZE, AlpExact, AlpFloat, - AlpHeader, AlpInfo, ForInfo, + ALP_COMPRESSION_MODE, ALP_DEFAULT_LOG_VECTOR_SIZE, ALP_HEADER_SIZE, + ALP_INTEGER_ENCODING_FOR_BIT_PACK, ALP_MAX_EXPONENT_F32, ALP_MAX_EXPONENT_F64, + ALP_MAX_LOG_VECTOR_SIZE, ALP_MIN_LOG_VECTOR_SIZE, AlpExact, AlpFloat, AlpHeader, AlpInfo, + ForInfo, }; use crate::encodings::decoding::Decoder; use crate::errors::{ParquetError, Result}; @@ -271,11 +272,31 @@ struct CurrentVector { exception_values: Bytes, } -/// Decode the next `out.len()` elements of the current vector straight into -/// `out`, with no intermediate buffer: unpack one packed integer at a time, -/// apply inverse FOR + decimal decode, then patch any exceptions whose -/// vector-local position falls in the just-produced sub-range. -fn decode_range(cur: &mut CurrentVector, out: &mut [Value]) -> Result<()> { +/// Largest slice decoded in one unpack-then-decode pass: the canonical ALP +/// vector size - 1024. +/// +/// The unpack scratch is sized to `min(vector_size, this)`, so vectors at the +/// default size or smaller are decoded whole, while larger (non-default) vectors +/// are decoded in canonical-vector-sized tiles. +/// +/// Bounding the tile to one canonical vector keeps the scratch L1-resident, which +/// is what makes the staged unpack-then-decode beat an in-place decode. +const DECODE_TILE_CAP: usize = 1 << ALP_DEFAULT_LOG_VECTOR_SIZE; + +/// Decode the next `out.len()` elements of the current vector into `out`, +/// patching any exceptions whose vector-local position falls in the +/// just-produced sub-range. +/// +/// Deltas are bulk-unpacked a tile at a time into the caller-provided `scratch` +/// via `get_batch` (which dispatches to the SIMD-friendly fixed-width `unpack` +/// kernels), then the inverse FOR and decimal decode run as one branchless, +/// state-free loop over that contiguous tile so the compiler can autovectorize +/// it. +fn decode_range( + cur: &mut CurrentVector, + scratch: &mut [Value::Exact], + out: &mut [Value], +) -> Result<()> { let frame_of_reference = cur.frame_of_reference; if cur.bit_width == 0 { // Every packed delta is zero, so all values share `frame_of_reference`. @@ -283,12 +304,21 @@ fn decode_range(cur: &mut CurrentVector, out: &mut [Valu out.fill(Value::decode_value(signed, cur.scale)); } else { let bit_width = cur.bit_width as usize; - for slot in out.iter_mut() { - let delta = cur.reader.get_value::(bit_width).ok_or_else(|| { - general_err!("Invalid ALP page: not enough packed bits to decode vector") - })?; - let signed = delta.wrapping_add(frame_of_reference).reinterpret_as_signed(); - *slot = Value::decode_value(signed, cur.scale); + let scale = cur.scale; + for chunk in out.chunks_mut(scratch.len()) { + let deltas = &mut scratch[..chunk.len()]; + let unpacked = cur.reader.get_batch::(deltas, bit_width); + if unpacked != chunk.len() { + return Err(general_err!( + "Invalid ALP page: not enough packed bits to decode vector" + )); + } + for (slot, &delta) in chunk.iter_mut().zip(deltas.iter()) { + let signed = delta + .wrapping_add(frame_of_reference) + .reinterpret_as_signed(); + *slot = Value::decode_value(signed, scale); + } } } @@ -334,6 +364,9 @@ where expected_next_offset: usize, /// Live decode state for the vector currently being consumed. current: Option>, + /// Reused unpack buffer, sized once per page to `min(vector_size, cap)` and + /// shared across all vectors; holds bulk-unpacked deltas for one tile. + scratch: Vec<::Exact>, /// Number of values still to be read from the page. num_values: usize, } @@ -357,6 +390,7 @@ where next_vector_idx: 0, expected_next_offset: 0, current: None, + scratch: Vec::new(), num_values: 0, } } @@ -451,6 +485,18 @@ where )); } + // Size the reused unpack buffer to one vector, capped to stay L1-resident + // and to the page's value count so tiny pages don't over-allocate. The + // `.max(1)` keeps a non-empty tile so the `chunks_mut` decode never hits a + // zero stride (an empty page returns before any decode anyway). + let tile = header + .vector_size + .min(DECODE_TILE_CAP) + .min(num_values) + .max(1); + self.scratch.clear(); + self.scratch.resize(tile, Default::default()); + self.header = header; self.body = body; self.next_vector_idx = 0; @@ -475,7 +521,7 @@ where } let cur = self.current.as_mut().unwrap(); let n = cur.remaining.min(target - written); - decode_range::(cur, &mut buffer[written..written + n])?; + decode_range::(cur, &mut self.scratch, &mut buffer[written..written + n])?; written += n; self.num_values -= n; } @@ -1152,6 +1198,174 @@ mod tests { assert_eq!(decoder.values_left(), 2); } + /// Microbenchmark comparing three decode inner loops on identical packed + /// data, decoding into an 8 MB output to expose memory traffic: + /// 1. scalar `get_value` per element; + /// 2. bulk `get_batch` into a stack scratch tile, then a fused + /// inverse-FOR + decimal-decode pass writing into the output; + /// 3. in-place: `get_batch` straight into the output (viewed as packed + /// integers), then decode each slot over its own bytes — no scratch. + /// + /// Ignored by default; run with: + /// `cargo test -p parquet --release decode_inner_loop -- --ignored --nocapture` + #[test] + #[ignore = "microbenchmark"] + fn bench_decode_inner_loop_f64() { + use crate::util::bit_util::BitWriter; + use std::time::Instant; + + let n = 1024usize; // values per vector + let vectors = 1024usize; // -> 1M values, 8 MB output, well past L2 + let total = n * vectors; + let bit_width = 12usize; + let base = 1_000_000u64; + let scale = ::decode_scale(4, 2); + + let mut writer = BitWriter::new(n * 8); + for i in 0..n as u64 { + let delta = i.wrapping_mul(2_654_435_761) & ((1 << bit_width) - 1); + writer.put_value(delta, bit_width); + } + writer.flush(); + let packed = Bytes::from(writer.consume()); + + let iters = 30usize; + let mut out = vec![0.0f64; total]; + let mut sink = 0.0f64; + + // 1. scalar get_value + let t0 = Instant::now(); + for _ in 0..iters { + for chunk in out.chunks_mut(n) { + let mut reader = BitReader::new(packed.clone()); + for slot in chunk.iter_mut() { + let delta = reader.get_value::(bit_width).unwrap(); + let signed = delta.wrapping_add(base) as i64; + *slot = ((signed as f64) * scale.0) * scale.1; + } + } + sink += out[total - 1]; + } + let scalar = t0.elapsed(); + + // 2. batched into a stack scratch tile + let t1 = Instant::now(); + for _ in 0..iters { + let mut scratch = [0u64; 1024]; + for chunk in out.chunks_mut(n) { + let len = chunk.len(); + let mut reader = BitReader::new(packed.clone()); + reader.get_batch::(&mut scratch[..len], bit_width); + for (slot, &delta) in chunk.iter_mut().zip(scratch[..len].iter()) { + let signed = delta.wrapping_add(base) as i64; + *slot = ((signed as f64) * scale.0) * scale.1; + } + } + sink += out[total - 1]; + } + let scratch_t = t1.elapsed(); + + // 3. in-place, no scratch + let t2 = Instant::now(); + for _ in 0..iters { + for chunk in out.chunks_mut(n) { + let mut reader = BitReader::new(packed.clone()); + // SAFETY: f64 and u64 share size and alignment, so the output + // chunk can hold the unpacked integers and then be overwritten + // with the decoded float bit patterns. + let int_out: &mut [u64] = unsafe { + std::slice::from_raw_parts_mut(chunk.as_mut_ptr() as *mut u64, chunk.len()) + }; + reader.get_batch::(int_out, bit_width); + for slot in int_out.iter_mut() { + let signed = slot.wrapping_add(base) as i64; + *slot = (((signed as f64) * scale.0) * scale.1).to_bits(); + } + } + sink += out[total - 1]; + } + let inplace = t2.elapsed(); + + let total_vals = (iters * total) as f64; + let ns = |d: std::time::Duration| d.as_nanos() as f64 / total_vals; + println!( + "f64 decode ({} MB out) scalar: {:.3} scratch: {:.3} in-place: {:.3} ns/val | \ + scratch {:.2}x scalar, in-place {:.2}x scalar, in-place {:.2}x scratch (sink={sink})", + total * 8 / (1 << 20), + ns(scalar), + ns(scratch_t), + ns(inplace), + scalar.as_nanos() as f64 / scratch_t.as_nanos() as f64, + scalar.as_nanos() as f64 / inplace.as_nanos() as f64, + scratch_t.as_nanos() as f64 / inplace.as_nanos() as f64, + ); + } + + /// End-to-end decoder throughput on a real multi-vector page, across a few + /// `vector_size` values, exercising the production path: `set_data` (scratch + /// sizing), per-vector `load_current_vector`, and the tiled `decode_range` + /// over the reused buffer. + /// + /// `cargo test -p parquet --release decode_end_to_end -- --ignored --nocapture` + #[test] + #[ignore = "microbenchmark"] + fn bench_decode_end_to_end() { + use crate::util::bit_util::BitWriter; + use std::time::Instant; + + fn pack(count: usize, bit_width: usize) -> Vec { + let mut w = BitWriter::new(count * 8); + for i in 0..count as u64 { + let delta = i.wrapping_mul(2_654_435_761) & ((1u64 << bit_width) - 1); + w.put_value(delta, bit_width); + } + w.flush(); + w.consume() + } + + let total = 1usize << 20; // ~1M values + let bit_width = 12usize; + let iters = 50usize; + + for &log_vs in &[8u8, 10, 13] { + let vector_size = 1usize << log_vs; + let full_vectors = total / vector_size; + let packed = pack(vector_size, bit_width); + let vectors: Vec> = (0..full_vectors) + .map(|_| { + make_vector(VectorSpec { + exponent: 4, + factor: 2, + frame_of_reference: 1_000_000u64, + bit_width: bit_width as u8, + packed_values: &packed, + exception_positions: &[], + exception_values: &[], + }) + }) + .collect(); + let n = full_vectors * vector_size; + let page = Bytes::from(make_page_from_vectors(log_vs, n as i32, &vectors)); + + let mut out = vec![0.0f64; n]; + let mut decoder = AlpDecoder::::new(); + let mut sink = 0.0f64; + let t = Instant::now(); + for _ in 0..iters { + decoder.set_data(page.clone(), n).unwrap(); + decoder.get(&mut out).unwrap(); + sink += out[n - 1]; + } + let el = t.elapsed(); + println!( + "vector_size {:>5} ({:>4} vectors): {:.3} ns/val (sink={sink})", + vector_size, + full_vectors, + el.as_nanos() as f64 / (iters * n) as f64, + ); + } + } + #[test] fn test_alp_decoder_get_full_read() { let vector0 = make_vector(VectorSpec { From 23fac76e0c613053d9c7b1217a767c09587874b3 Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Mon, 8 Jun 2026 11:26:54 -0400 Subject: [PATCH 38/50] update parquet-testing dep --- parquet-testing | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parquet-testing b/parquet-testing index b49107d5e742..e7d32dac5b4c 160000 --- a/parquet-testing +++ b/parquet-testing @@ -1 +1 @@ -Subproject commit b49107d5e7426ccd657965dbf85fcac430812d64 +Subproject commit e7d32dac5b4cbf017fefd598a03686f53370b292 From b0199e096ccf144fc2496936f51295d96b78d391 Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Tue, 14 Jul 2026 10:33:20 -0400 Subject: [PATCH 39/50] avoid division --- parquet/src/encodings/alp.rs | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/parquet/src/encodings/alp.rs b/parquet/src/encodings/alp.rs index e7c5b54be395..d7258237156b 100644 --- a/parquet/src/encodings/alp.rs +++ b/parquet/src/encodings/alp.rs @@ -158,24 +158,20 @@ impl AlpHeader { Ok(out) } + /// `vector_size` is always `1 << log_vector_size` (see [`AlpHeader::deserialize`]), + /// so the division a `div_ceil` would emit is a shift. `vector_size` is a + /// runtime value, so the compiler cannot see that on its own. pub(crate) fn num_vectors(&self) -> usize { - if self.num_elements == 0 { - 0 - } else { - self.num_elements.div_ceil(self.vector_size) - } + debug_assert!(self.vector_size.is_power_of_two()); + (self.num_elements + self.vector_size - 1) >> self.vector_size.trailing_zeros() } + /// Number of elements in vector `vector_index`: a full vector, the short + /// trailing remainder, or zero past the end of the page. pub(crate) fn vector_num_elements(&self, vector_index: usize) -> u16 { - let num_full_vectors = self.num_elements / self.vector_size; - let remainder = self.num_elements % self.vector_size; - if vector_index < num_full_vectors { - self.vector_size as u16 - } else if vector_index == num_full_vectors && remainder > 0 { - remainder as u16 - } else { - 0 - } + let start = vector_index.saturating_mul(self.vector_size); + let remaining = self.num_elements.saturating_sub(start); + remaining.min(self.vector_size) as u16 } } From 532b025a7f5c766ec2da0db98319d9acdf6a4dbd Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Tue, 14 Jul 2026 10:53:22 -0400 Subject: [PATCH 40/50] v1 header doesn't know num_value upfront --- parquet/src/encodings/decoding/alp_decoder.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/parquet/src/encodings/decoding/alp_decoder.rs b/parquet/src/encodings/decoding/alp_decoder.rs index c119eee19bb0..572c0af7a91e 100644 --- a/parquet/src/encodings/decoding/alp_decoder.rs +++ b/parquet/src/encodings/decoding/alp_decoder.rs @@ -461,13 +461,17 @@ where fn set_data(&mut self, data: Bytes, num_values: usize) -> Result<()> { let header = parse_alp_page_header(data.as_ref())?; - if header.num_elements != num_values { + // `num_values` is an upper bound, not an exact count: only non-null values + // are encoded, and the caller passes the level count when the value count + // is not known up front. The header carries the authoritative count. + if header.num_elements > num_values { return Err(general_err!( - "Invalid ALP page: header num_elements {} does not match page num_values {}", + "Invalid ALP page: header num_elements {} exceeds page num_values {}", header.num_elements, num_values )); } + let num_values = header.num_elements; let offsets_section_size = header .num_vectors() From 3b7d36232891458df02af3af9ddcb6a82dc68b49 Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Tue, 14 Jul 2026 16:05:10 -0400 Subject: [PATCH 41/50] add encoder support --- parquet/src/arrow/arrow_writer/mod.rs | 2 +- parquet/src/data_type.rs | 1 + parquet/src/encodings/alp.rs | 191 ++++- parquet/src/encodings/decoding.rs | 2 +- parquet/src/encodings/encoding/alp_encoder.rs | 782 ++++++++++++++++++ parquet/src/encodings/encoding/mod.rs | 104 ++- parquet/tests/arrow_reader/alp.rs | 164 +++- 7 files changed, 1219 insertions(+), 27 deletions(-) create mode 100644 parquet/src/encodings/encoding/alp_encoder.rs diff --git a/parquet/src/arrow/arrow_writer/mod.rs b/parquet/src/arrow/arrow_writer/mod.rs index 9e61c575141e..0fb4746f1cc3 100644 --- a/parquet/src/arrow/arrow_writer/mod.rs +++ b/parquet/src/arrow/arrow_writer/mod.rs @@ -3131,7 +3131,7 @@ mod tests { Encoding::BYTE_STREAM_SPLIT, ], DataType::Float32 | DataType::Float64 => { - vec![Encoding::PLAIN, Encoding::BYTE_STREAM_SPLIT] + vec![Encoding::PLAIN, Encoding::BYTE_STREAM_SPLIT, Encoding::ALP] } _ => vec![Encoding::PLAIN], }; diff --git a/parquet/src/data_type.rs b/parquet/src/data_type.rs index d8c7b9201389..52e9c9e0d9df 100644 --- a/parquet/src/data_type.rs +++ b/parquet/src/data_type.rs @@ -702,6 +702,7 @@ pub(crate) mod private { + Send + HeapSize + crate::encodings::decoding::private::GetDecoder + + crate::encodings::encoding::private::GetEncoder + crate::file::statistics::private::MakeStatistics { const PHYSICAL_TYPE: Type; diff --git a/parquet/src/encodings/alp.rs b/parquet/src/encodings/alp.rs index d7258237156b..adf02760b0fd 100644 --- a/parquet/src/encodings/alp.rs +++ b/parquet/src/encodings/alp.rs @@ -133,7 +133,6 @@ impl AlpHeader { /// what cannot be represented: `vector_size` must be a power of two (its log /// is the on-disk field), and `num_elements` must fit in an `i32`. /// Counterpart to [`AlpHeader::deserialize`]; consumed by the ALP encoder. - #[allow(dead_code)] pub(crate) fn serialize(&self) -> Result<[u8; ALP_HEADER_SIZE]> { if !self.vector_size.is_power_of_two() { return Err(general_err!( @@ -195,6 +194,13 @@ pub(crate) struct AlpInfo { impl AlpInfo { pub(crate) const STORED_SIZE: usize = 4; + + /// Append this vector's ALP metadata in its on-disk little-endian form. + pub(crate) fn extend_serialized(&self, out: &mut Vec) { + out.push(self.exponent); + out.push(self.factor); + out.extend_from_slice(&self.num_exceptions.to_le_bytes()); + } } /// Per-vector FOR metadata for exact integer type (`u32` for `f32`, `u64` for `f64`). @@ -221,6 +227,12 @@ impl ForInfo { Exact::WIDTH + 1 } + /// Append this vector's FOR metadata in its on-disk little-endian form. + pub(crate) fn extend_serialized(&self, out: &mut Vec) { + self.frame_of_reference.extend_le_bytes(out); + out.push(self.bit_width); + } + pub(crate) fn get_bit_packed_size(&self, num_elements: u16) -> usize { (self.bit_width as usize * num_elements as usize).div_ceil(8) } @@ -243,12 +255,17 @@ impl ForInfo { /// - FOR stores non-negative deltas optimized for bitpacking. /// - Unsigned arithmetic avoids signed-overflow edge cases in FOR stage. /// - Signed interpretation is applied later during decimal reconstruction. -pub(crate) trait AlpExact: Copy + std::fmt::Debug + FromBitpacked + Default { +pub(crate) trait AlpExact: Copy + std::fmt::Debug + PartialEq + FromBitpacked + Default { const WIDTH: usize; - type Signed: Copy; + type Signed: Copy + Ord + std::fmt::Debug + Send; fn from_le_slice(slice: &[u8]) -> Self; fn wrapping_add(self, rhs: Self) -> Self; + fn wrapping_sub(self, rhs: Self) -> Self; fn reinterpret_as_signed(self) -> Self::Signed; + fn reinterpret_from_signed(signed: Self::Signed) -> Self; + /// Widen to `u64` for bit-packing, which is `u64`-oriented throughout. + fn to_u64(self) -> u64; + fn extend_le_bytes(self, out: &mut Vec); } impl AlpExact for u32 { @@ -263,9 +280,25 @@ impl AlpExact for u32 { self.wrapping_add(rhs) } + fn wrapping_sub(self, rhs: Self) -> Self { + self.wrapping_sub(rhs) + } + fn reinterpret_as_signed(self) -> Self::Signed { i32::from_ne_bytes(self.to_ne_bytes()) } + + fn reinterpret_from_signed(signed: Self::Signed) -> Self { + u32::from_ne_bytes(signed.to_ne_bytes()) + } + + fn to_u64(self) -> u64 { + u64::from(self) + } + + fn extend_le_bytes(self, out: &mut Vec) { + out.extend_from_slice(&self.to_le_bytes()); + } } impl AlpExact for u64 { @@ -282,9 +315,25 @@ impl AlpExact for u64 { self.wrapping_add(rhs) } + fn wrapping_sub(self, rhs: Self) -> Self { + self.wrapping_sub(rhs) + } + fn reinterpret_as_signed(self) -> Self::Signed { i64::from_ne_bytes(self.to_ne_bytes()) } + + fn reinterpret_from_signed(signed: Self::Signed) -> Self { + u64::from_ne_bytes(signed.to_ne_bytes()) + } + + fn to_u64(self) -> u64 { + self + } + + fn extend_le_bytes(self, out: &mut Vec) { + out.extend_from_slice(&self.to_le_bytes()); + } } pub(crate) const ALP_POW10_F32: [f32; 11] = [ 1.0, @@ -358,10 +407,26 @@ pub(crate) const ALP_NEG_POW10_F64: [f64; 19] = [ 0.000000000000000001, ]; -pub(crate) trait AlpFloat: Copy + Default { +pub(crate) trait AlpFloat: Copy + Default + PartialEq + std::ops::Mul { type Exact: AlpExact + FromBytes; type Scale: Copy + Send; + /// Largest `exponent` this type admits: 10 for `f32`, 18 for `f64`. + const MAX_EXPONENT: u8; + + /// Rounding magic number: `2^22 + 2^23` (`f32`) or `2^51 + 2^52` (`f64`). + const MAGIC_NUMBER: Self; + + /// Bounds outside which the scaled value cannot reach the exact integer + /// type: `i32` for `f32`, `i64` for `f64`. + const ENCODING_UPPER_LIMIT: Self; + const ENCODING_LOWER_LIMIT: Self; + + /// [`AlpFloat::ENCODING_UPPER_LIMIT`] as the exact signed integer. Stands in + /// for values ALP cannot represent, so that the round-trip check that + /// follows fails and the value is recorded as an exception. + const ENCODING_SENTINEL: ::Signed; + /// Precompute vector-level ALP decimal scale constants for: /// `value = (encoded * 10^(factor)) * 10^(-exponent)`. /// @@ -372,12 +437,58 @@ pub(crate) trait AlpFloat: Copy + Default { fn decode_value(signed_encoded: ::Signed, scale: Self::Scale) -> Self; fn from_exact_bits(bits: Self::Exact) -> Self; + + fn to_exact_bits(self) -> Self::Exact; + + /// Precompute vector-level ALP decimal scale constants for the encode + /// direction: `encoded = fast_round((value * 10^(exponent)) * 10^(-factor))`. + fn encode_scale(exponent: u8, factor: u8) -> Self::Scale; + + /// Apply a scale as the same two separate multiplications the decode side + /// uses. The spec requires two steps rather than one multiplication by a + /// combined constant, so that every implementation produces bit-identical + /// results. + fn apply_scale(self, scale: Self::Scale) -> Self; + + /// True for values ALP cannot turn into an exact integer: NaN, the + /// infinities, anything scaled past the exact integer type, and `-0.0` + /// (which would come back as `+0.0` and lose its sign). + fn is_impossible_to_encode(self) -> bool; + + /// Round to the nearest integer by the "magic number" technique: adding and + /// then subtracting the magic number lands the value in a binade where the + /// unit in the last place is exactly 1.0, so the fractional bits fall off. + /// + /// The sign branch is load-bearing, not a micro-optimization: negative + /// values must use `- magic + magic` to land in the corresponding negative + /// binade. Nor can the `+ magic - magic` be algebraically cancelled - the + /// rounding *is* the intermediate loss of precision. + fn fast_round(self) -> ::Signed; + + /// Encode one value with a precomputed [`AlpFloat::encode_scale`]. + /// + /// Values ALP cannot represent map to [`AlpFloat::ENCODING_SENTINEL`], whose + /// round trip is guaranteed to mismatch, so the caller's `decode == value` + /// check records them as exceptions without a separate test. + fn encode_value(self, scale: Self::Scale) -> ::Signed { + let scaled = self.apply_scale(scale); + if scaled.is_impossible_to_encode() { + return Self::ENCODING_SENTINEL; + } + scaled.fast_round() + } } impl AlpFloat for f32 { type Exact = u32; type Scale = (f32, f32); + const MAX_EXPONENT: u8 = ALP_MAX_EXPONENT_F32; + const MAGIC_NUMBER: Self = 12582912.0; // 2^22 + 2^23 + const ENCODING_UPPER_LIMIT: Self = 2147483520.0; + const ENCODING_LOWER_LIMIT: Self = -2147483520.0; + const ENCODING_SENTINEL: i32 = 2147483520; + fn decode_scale(exponent: u8, factor: u8) -> Self::Scale { debug_assert!(exponent <= ALP_MAX_EXPONENT_F32); debug_assert!(factor <= exponent); @@ -394,12 +505,51 @@ impl AlpFloat for f32 { fn from_exact_bits(bits: Self::Exact) -> Self { f32::from_bits(bits) } + + fn to_exact_bits(self) -> Self::Exact { + self.to_bits() + } + + fn encode_scale(exponent: u8, factor: u8) -> Self::Scale { + debug_assert!(exponent <= ALP_MAX_EXPONENT_F32); + debug_assert!(factor <= exponent); + ( + ALP_POW10_F32[exponent as usize], + ALP_NEG_POW10_F32[factor as usize], + ) + } + + fn apply_scale(self, scale: Self::Scale) -> Self { + (self * scale.0) * scale.1 + } + + fn is_impossible_to_encode(self) -> bool { + // The infinities need no separate test: they fall outside the limits. + self.is_nan() + || !(Self::ENCODING_LOWER_LIMIT..=Self::ENCODING_UPPER_LIMIT).contains(&self) + || (self == 0.0 && self.is_sign_negative()) + } + + fn fast_round(self) -> i32 { + let rounded = if self >= 0.0 { + (self + Self::MAGIC_NUMBER) - Self::MAGIC_NUMBER + } else { + (self - Self::MAGIC_NUMBER) + Self::MAGIC_NUMBER + }; + rounded as i32 + } } impl AlpFloat for f64 { type Exact = u64; type Scale = (f64, f64); + const MAX_EXPONENT: u8 = ALP_MAX_EXPONENT_F64; + const MAGIC_NUMBER: Self = 6755399441055744.0; // 2^51 + 2^52 + const ENCODING_UPPER_LIMIT: Self = 9223372036854774784.0; + const ENCODING_LOWER_LIMIT: Self = -9223372036854774784.0; + const ENCODING_SENTINEL: i64 = 9223372036854774784; + fn decode_scale(exponent: u8, factor: u8) -> Self::Scale { debug_assert!(exponent <= ALP_MAX_EXPONENT_F64); debug_assert!(factor <= exponent); @@ -416,4 +566,37 @@ impl AlpFloat for f64 { fn from_exact_bits(bits: Self::Exact) -> Self { f64::from_bits(bits) } + + fn to_exact_bits(self) -> Self::Exact { + self.to_bits() + } + + fn encode_scale(exponent: u8, factor: u8) -> Self::Scale { + debug_assert!(exponent <= ALP_MAX_EXPONENT_F64); + debug_assert!(factor <= exponent); + ( + ALP_POW10_F64[exponent as usize], + ALP_NEG_POW10_F64[factor as usize], + ) + } + + fn apply_scale(self, scale: Self::Scale) -> Self { + (self * scale.0) * scale.1 + } + + fn is_impossible_to_encode(self) -> bool { + // The infinities need no separate test: they fall outside the limits. + self.is_nan() + || !(Self::ENCODING_LOWER_LIMIT..=Self::ENCODING_UPPER_LIMIT).contains(&self) + || (self == 0.0 && self.is_sign_negative()) + } + + fn fast_round(self) -> i64 { + let rounded = if self >= 0.0 { + (self + Self::MAGIC_NUMBER) - Self::MAGIC_NUMBER + } else { + (self - Self::MAGIC_NUMBER) + Self::MAGIC_NUMBER + }; + rounded as i64 + } } diff --git a/parquet/src/encodings/decoding.rs b/parquet/src/encodings/decoding.rs index d1b22f1c3676..d56c75bed478 100644 --- a/parquet/src/encodings/decoding.rs +++ b/parquet/src/encodings/decoding.rs @@ -34,7 +34,7 @@ use crate::errors::{ParquetError, Result}; use crate::schema::types::ColumnDescPtr; use crate::util::bit_util::{self, BitReader, FromBitpacked}; -mod alp_decoder; +pub(crate) mod alp_decoder; mod byte_stream_split_decoder; pub(crate) mod private { diff --git a/parquet/src/encodings/encoding/alp_encoder.rs b/parquet/src/encodings/encoding/alp_encoder.rs new file mode 100644 index 000000000000..0a0156c3dead --- /dev/null +++ b/parquet/src/encodings/encoding/alp_encoder.rs @@ -0,0 +1,782 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//! ALP (Adaptive Lossless floating-Point) encoder. +//! +//! Based on the draft Parquet spec: +//! +//! Values are buffered until the page is flushed, then encoded a vector at a +//! time into the page layout that [`AlpDecoder`] reads back: +//! +//! ```text +//! [AlpHeader][offsets][vector 0][vector 1]...[vector N-1] +//! ``` +//! +//! [`AlpDecoder`]: crate::encodings::decoding::alp_decoder::AlpDecoder + +use std::cmp::Reverse; + +use bytes::Bytes; + +use crate::basic::Encoding; +use crate::data_type::DataType; +use crate::encodings::alp::{ + ALP_COMPRESSION_MODE, ALP_DEFAULT_LOG_VECTOR_SIZE, ALP_HEADER_SIZE, + ALP_INTEGER_ENCODING_FOR_BIT_PACK, AlpExact, AlpFloat, AlpHeader, AlpInfo, ForInfo, +}; +use crate::encodings::encoding::Encoder; +use crate::errors::{ParquetError, Result}; +use crate::util::bit_util::{BitWriter, num_required_bits}; + +/// Vectors are written at the spec's default size, the ALP paper's 1024. +const VECTOR_SIZE: usize = 1 << ALP_DEFAULT_LOG_VECTOR_SIZE; + +/// Values sampled from a vector when estimating a candidate's encoded size. +const SAMPLES_PER_VECTOR: usize = 256; + +/// Vectors sampled from the first page to build the column chunk's candidate set. +const SAMPLE_VECTORS: usize = 8; + +/// Candidate `(exponent, factor)` pairs carried forward from the sampling pass. +const MAX_COMBINATIONS: usize = 5; + +/// Consecutive non-improving candidates after which per-vector selection stops. +const SAMPLING_EARLY_EXIT_THRESHOLD: usize = 4; + +/// Bits of overhead an exception costs: the value itself plus its `u16` position. +fn exception_bits() -> u64 { + (F::Exact::WIDTH as u64 * 8) + 16 +} + +/// One ALP decimal-encoding candidate: `encoded = round(value * 10^e * 10^-f)`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct ExponentAndFactor { + exponent: u8, + factor: u8, +} + +/// A candidate together with how often it won across the sampled vectors. +#[derive(Debug, Clone, Copy)] +struct Combination { + params: ExponentAndFactor, + num_appearances: u64, + estimated_size_bits: u64, +} + +/// Sort key ranking candidates, larger is better: more appearances as a vector's +/// best candidate, then smaller estimated size, then larger exponent, then +/// larger factor. +/// +/// The last two are tie-breaks taken from the ALP paper (Afroozeh et al., SIGMOD +/// 2023, section 3.1.2), which prefers higher exponents and factors without +/// justifying it further. They are kept so the candidate set matches the +/// reference implementations. +fn rank(c: &Combination) -> (u64, Reverse, u8, u8) { + ( + c.num_appearances, + Reverse(c.estimated_size_bits), + c.params.exponent, + c.params.factor, + ) +} + +fn is_better(c1: &Combination, c2: &Combination) -> bool { + rank(c1) > rank(c2) +} + +/// Estimate, in bits, what `params` would cost on `sample`. +/// +/// Encodes each value, checks whether it round-trips, and prices the result as +/// `num_values * bit_width + num_exceptions * exception_bits`, where `bit_width` +/// covers the FOR range of the values that did round-trip. +/// +/// Returns `None` when `penalize_exceptions` is set and fewer than two values +/// round-trip: such a candidate encodes almost everything as an exception, and +/// its FOR range is meaningless. +fn estimate_size_bits( + sample: &[F], + params: ExponentAndFactor, + penalize_exceptions: bool, +) -> Option { + let encode_scale = F::encode_scale(params.exponent, params.factor); + let decode_scale = F::decode_scale(params.exponent, params.factor); + + let mut num_exceptions = 0u64; + let mut min = None; + let mut max = None; + + for &value in sample { + let encoded = value.encode_value(encode_scale); + if F::decode_value(encoded, decode_scale) == value { + min = Some(min.map_or(encoded, |m: ::Signed| m.min(encoded))); + max = Some(max.map_or(encoded, |m: ::Signed| m.max(encoded))); + } else { + num_exceptions += 1; + } + } + + let num_values = sample.len() as u64; + let num_non_exceptions = num_values - num_exceptions; + if penalize_exceptions && num_non_exceptions < 2 { + return None; + } + + // With nothing to frame, there is no packed section at all: every value is + // stored as an exception. + let (Some(min), Some(max)) = (min, max) else { + return Some(num_values * exception_bits::()); + }; + + let range = F::Exact::reinterpret_from_signed(max).wrapping_sub(F::Exact::reinterpret_from_signed(min)); + let bit_width = u64::from(num_required_bits(range.to_u64())); + + Some(num_values * bit_width + num_exceptions * exception_bits::()) +} + +/// Sample every `n`th value so the whole span is represented, capped at +/// [`SAMPLES_PER_VECTOR`] values. +fn sample_values(values: &[F], out: &mut Vec) { + out.clear(); + let stride = values.len().div_ceil(SAMPLES_PER_VECTOR).max(1); + out.extend(values.iter().step_by(stride).copied()); +} + +/// Build the column chunk's candidate set: the top [`MAX_COMBINATIONS`] pairs by +/// how often they win across sampled vectors. +/// +/// This is the first level of the ALP paper's two-level sampling. Each sampled +/// vector is searched exhaustively (66 pairs for `f32`, 190 for `f64`); the +/// winners are tallied, and the most frequent are carried forward so that each +/// vector only has to choose among a handful of candidates. +fn build_preset(values: &[F]) -> Vec { + let num_vectors = values.len().div_ceil(VECTOR_SIZE); + let vector_stride = num_vectors.div_ceil(SAMPLE_VECTORS).max(1); + + let mut sample = Vec::with_capacity(SAMPLES_PER_VECTOR); + let mut tally: Vec = Vec::new(); + + for vector in values.chunks(VECTOR_SIZE).step_by(vector_stride) { + sample_values(vector, &mut sample); + + // Start from the worst case - every value an exception, unpacked - so + // that any candidate that manages to encode anything beats it. + let mut best = Combination { + params: ExponentAndFactor { + exponent: F::MAX_EXPONENT, + factor: F::MAX_EXPONENT, + }, + num_appearances: 0, + estimated_size_bits: sample.len() as u64 + * (exception_bits::() + F::Exact::WIDTH as u64 * 8), + }; + + for exponent in 0..=F::MAX_EXPONENT { + for factor in 0..=exponent { + let params = ExponentAndFactor { exponent, factor }; + let Some(estimated_size_bits) = estimate_size_bits(&sample, params, true) else { + continue; + }; + let candidate = Combination { + params, + num_appearances: 0, + estimated_size_bits, + }; + if is_better(&candidate, &best) { + best = candidate; + } + } + } + + match tally.iter_mut().find(|c| c.params == best.params) { + Some(existing) => existing.num_appearances += 1, + None => tally.push(Combination { + num_appearances: 1, + ..best + }), + } + } + + // Size estimates from different vectors are not comparable with each other, + // so only the win counts rank the candidate set. + for combination in tally.iter_mut() { + combination.estimated_size_bits = 0; + } + tally.sort_by_key(|c| Reverse(rank(c))); + tally.truncate(MAX_COMBINATIONS); + + if tally.is_empty() { + // An empty page has nothing to sample; any valid pair will do. + return vec![ExponentAndFactor { + exponent: 0, + factor: 0, + }]; + } + tally.into_iter().map(|c| c.params).collect() +} + +/// Pick the candidate that encodes `vector` smallest. +/// +/// This is the second level of the two-level sampling: only the chunk's +/// candidates are tried, against a sample of the vector rather than all of it. +fn select_params( + vector: &[F], + preset: &[ExponentAndFactor], + sample: &mut Vec, +) -> ExponentAndFactor { + if preset.len() == 1 { + return preset[0]; + } + + sample_values(vector, sample); + + let mut best = preset[0]; + let mut best_size_bits = u64::MAX; + let mut worse_in_a_row = 0; + + for ¶ms in preset { + let Some(size_bits) = estimate_size_bits(sample, params, false) else { + continue; + }; + if size_bits >= best_size_bits { + worse_in_a_row += 1; + if worse_in_a_row == SAMPLING_EARLY_EXIT_THRESHOLD { + break; + } + continue; + } + best = params; + best_size_bits = size_bits; + worse_in_a_row = 0; + } + best +} + +/// Reusable per-vector buffers, kept across vectors and pages so encoding a page +/// does not allocate per vector. +struct Scratch { + /// Decimal-encoded integers, overwritten in place with their FOR deltas. + encoded: Vec<::Signed>, + exception_positions: Vec, + exception_values: Vec, + sample: Vec, +} + +impl Scratch { + fn new() -> Self { + Self { + encoded: Vec::new(), + exception_positions: Vec::new(), + exception_values: Vec::new(), + sample: Vec::new(), + } + } + + fn estimated_memory_size(&self) -> usize { + self.encoded.capacity() * std::mem::size_of::<::Signed>() + + self.exception_positions.capacity() * std::mem::size_of::() + + self.exception_values.capacity() * std::mem::size_of::() + + self.sample.capacity() * std::mem::size_of::() + } +} + +/// Encode one vector and append it to `out`: +/// `[AlpInfo][ForInfo][PackedValues][ExceptionPositions][ExceptionValues]`. +fn encode_vector( + values: &[F], + params: ExponentAndFactor, + scratch: &mut Scratch, + out: &mut Vec, +) -> Result<()> { + let encode_scale = F::encode_scale(params.exponent, params.factor); + let decode_scale = F::decode_scale(params.exponent, params.factor); + + let Scratch { + encoded, + exception_positions, + exception_values, + .. + } = scratch; + encoded.clear(); + exception_positions.clear(); + exception_values.clear(); + + // A value is an exception when it does not survive the round trip. Values + // ALP cannot represent at all (NaN, the infinities, -0.0) encode to a + // sentinel whose round trip is guaranteed to mismatch, so they need no + // separate test here. Note that `-0.0 == 0.0`, so a naive equality check + // against the *input* would let -0.0 through and silently lose its sign. + for (idx, &value) in values.iter().enumerate() { + let encoded_value = value.encode_value(encode_scale); + encoded.push(encoded_value); + if F::decode_value(encoded_value, decode_scale) != value { + exception_positions.push(idx as u16); + } + } + + let num_exceptions = u16::try_from(exception_positions.len()).map_err(|_| { + general_err!( + "Invalid ALP vector: {} exceptions exceeds u16::MAX", + exception_positions.len() + ) + })?; + + // An exception still occupies a slot in the packed section. Filling it with + // a real encoded value - rather than leaving the sentinel there - keeps the + // FOR range tight, which is what the bit width is derived from. The true + // value is written verbatim into the exception section. + let placeholder = first_non_exception_value::(encoded, exception_positions); + for &position in exception_positions.iter() { + exception_values.push(values[position as usize]); + encoded[position as usize] = placeholder; + } + + let zero = F::Exact::default().reinterpret_as_signed(); + let min = encoded.iter().copied().min().unwrap_or(zero); + let max = encoded.iter().copied().max().unwrap_or(zero); + + let frame_of_reference = F::Exact::reinterpret_from_signed(min); + let range = F::Exact::reinterpret_from_signed(max).wrapping_sub(frame_of_reference); + let bit_width = num_required_bits(range.to_u64()); + + let alp_info = AlpInfo { + exponent: params.exponent, + factor: params.factor, + num_exceptions, + }; + let for_info = ForInfo:: { + frame_of_reference, + bit_width, + }; + alp_info.extend_serialized(out); + for_info.extend_serialized(out); + + // PackedValues: FOR deltas, LSB-first, as the spec requires. A zero bit + // width means every value equals the frame of reference, so nothing is + // stored. This is the stage a FastLanes-ordered `integer_encoding` would + // replace. + if bit_width > 0 { + let mut writer = BitWriter::new_from_buf(std::mem::take(out)); + for &encoded_value in encoded.iter() { + let delta = F::Exact::reinterpret_from_signed(encoded_value) + .wrapping_sub(frame_of_reference); + writer.put_value(delta.to_u64(), bit_width as usize); + } + // Pads to a byte boundary, giving exactly the ceil(n * bit_width / 8) + // bytes the decoder derives from the metadata. + *out = writer.consume(); + } + + for &position in exception_positions.iter() { + out.extend_from_slice(&position.to_le_bytes()); + } + for &value in exception_values.iter() { + value.to_exact_bits().extend_le_bytes(out); + } + + Ok(()) +} + +/// The encoded integer of the first value that is not an exception, or zero if +/// every value is one. `exception_positions` is ascending, so the first index it +/// skips over is the first non-exception. +fn first_non_exception_value( + encoded: &[::Signed], + exception_positions: &[u16], +) -> ::Signed { + let mut candidate = 0usize; + for &position in exception_positions { + if position as usize != candidate { + break; + } + candidate += 1; + } + encoded + .get(candidate) + .copied() + .unwrap_or_else(|| F::Exact::default().reinterpret_as_signed()) +} + +/// Encode `values` as one ALP page. +fn encode_page( + values: &[F], + preset: &[ExponentAndFactor], + scratch: &mut Scratch, +) -> Result> { + let header = AlpHeader { + compression_mode: ALP_COMPRESSION_MODE, + integer_encoding: ALP_INTEGER_ENCODING_FOR_BIT_PACK, + vector_size: VECTOR_SIZE, + num_elements: values.len(), + }; + let num_vectors = header.num_vectors(); + + let mut page = Vec::with_capacity(ALP_HEADER_SIZE + num_vectors * 4 + values.len() * 4); + page.extend_from_slice(&header.serialize()?); + + // Offsets are only known once each vector is encoded, so leave room and + // backfill. + let offsets_start = page.len(); + page.resize(offsets_start + num_vectors * std::mem::size_of::(), 0); + + for (idx, vector) in values.chunks(VECTOR_SIZE).enumerate() { + // Offsets are relative to the start of the page body, which follows the + // fixed-size header. + let offset = u32::try_from(page.len() - ALP_HEADER_SIZE) + .map_err(|_| general_err!("Invalid ALP page: body exceeds u32 offset range"))?; + let offset_at = offsets_start + idx * std::mem::size_of::(); + page[offset_at..offset_at + 4].copy_from_slice(&offset.to_le_bytes()); + + let params = select_params(vector, preset, &mut scratch.sample); + encode_vector(vector, params, scratch, &mut page)?; + } + + Ok(page) +} + +/// Encoder for ALP-encoded floating-point pages (`f32`/`f64`). +/// +/// Values are buffered and encoded on flush, because ALP chooses its decimal +/// parameters per vector by sampling the values, and the candidate set for the +/// column chunk is derived from the first page's data. +pub struct AlpEncoder +where + T::T: AlpFloat, +{ + /// Values buffered for the page currently being built. + values: Vec, + /// Candidate `(exponent, factor)` pairs for this column chunk, sampled once + /// from the first page and reused for the rest of the chunk. + preset: Option>, + scratch: Scratch, +} + +impl AlpEncoder +where + T::T: AlpFloat, +{ + pub(crate) fn new() -> Self { + Self { + values: Vec::new(), + preset: None, + scratch: Scratch::new(), + } + } +} + +impl Encoder for AlpEncoder +where + T::T: AlpFloat, +{ + fn put(&mut self, values: &[T::T]) -> Result<()> { + self.values.extend_from_slice(values); + Ok(()) + } + + fn encoding(&self) -> Encoding { + Encoding::ALP + } + + fn estimated_data_encoded_size(&self) -> usize { + // Encoded size is not known until the parameters are chosen, so bound it + // by the unencoded size: a vector never encodes larger than storing every + // value as an exception. + let num_vectors = self.values.len().div_ceil(VECTOR_SIZE); + ALP_HEADER_SIZE + + num_vectors + * (std::mem::size_of::() + + AlpInfo::STORED_SIZE + + ForInfo::<::Exact>::stored_size()) + + self.values.len() + * (::Exact::WIDTH + std::mem::size_of::()) + } + + fn estimated_memory_size(&self) -> usize { + self.values.capacity() * std::mem::size_of::() + + self.preset.as_ref().map_or(0, |p| { + p.capacity() * std::mem::size_of::() + }) + + self.scratch.estimated_memory_size() + } + + fn flush_buffer(&mut self) -> Result { + let values = &self.values; + let preset = self.preset.get_or_insert_with(|| build_preset(values)); + let page = encode_page(&self.values, preset, &mut self.scratch)?; + self.values.clear(); + Ok(page.into()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::data_type::{DoubleType, FloatType}; + use crate::encodings::decoding::Decoder; + use crate::encodings::decoding::alp_decoder::AlpDecoder; + + /// Encode `values` and read them back through the decoder. + fn roundtrip(values: &[T::T]) -> Vec + where + T::T: AlpFloat, + ::Exact: Send, + { + let mut encoder = AlpEncoder::::new(); + encoder.put(values).unwrap(); + let page = encoder.flush_buffer().unwrap(); + + let mut decoder = AlpDecoder::::new(); + decoder.set_data(page, values.len()).unwrap(); + let mut out = vec![T::T::default(); values.len()]; + assert_eq!(decoder.get(&mut out).unwrap(), values.len()); + out + } + + /// Assert bit-for-bit equality, so that NaN and -0.0 are held to the same + /// standard as every other value. + fn assert_bits_eq(actual: &[F], expected: &[F]) { + assert_eq!(actual.len(), expected.len(), "length mismatch"); + for (idx, (a, e)) in actual.iter().zip(expected.iter()).enumerate() { + assert_eq!( + a.to_exact_bits(), + e.to_exact_bits(), + "value mismatch at {idx}: expected {e:?}, got {a:?}" + ); + } + } + + #[test] + fn test_roundtrip_f64_decimals() { + let values: Vec = (0..500).map(|i| (i as f64) * 0.01 + 1.23).collect(); + assert_bits_eq(&roundtrip::(&values), &values); + } + + #[test] + fn test_roundtrip_f32_decimals() { + let values: Vec = (0..500).map(|i| (i as f32) * 0.5 + 1.25).collect(); + assert_bits_eq(&roundtrip::(&values), &values); + } + + /// Spans several vectors, including a short trailing one. + #[test] + fn test_roundtrip_multiple_vectors() { + let values: Vec = (0..2600).map(|i| (i as f64) * 0.001).collect(); + assert_bits_eq(&roundtrip::(&values), &values); + } + + /// The values ALP cannot represent must survive verbatim through the + /// exception path - including -0.0, which compares equal to +0.0 and would + /// be silently lost by a naive round-trip check. + #[test] + fn test_roundtrip_exceptions() { + let values = vec![ + 1.5f64, + f64::NAN, + 2.5, + f64::INFINITY, + -0.0, + f64::NEG_INFINITY, + 3.5, + 0.0, + f64::MAX, + f64::MIN, + ]; + let decoded = roundtrip::(&values); + assert_bits_eq(&decoded, &values); + assert!(decoded[1].is_nan()); + assert!(decoded[4].is_sign_negative()); + } + + /// Every value identical means a zero FOR range, so `bit_width` is 0 and no + /// packed section is written at all. + #[test] + fn test_roundtrip_all_identical() { + let values = vec![42.42f64; 3000]; + assert_bits_eq(&roundtrip::(&values), &values); + } + + #[test] + fn test_roundtrip_all_exceptions() { + let values = vec![f64::NAN; 100]; + let decoded = roundtrip::(&values); + assert!(decoded.iter().all(|v| v.is_nan())); + } + + #[test] + fn test_roundtrip_single_value() { + assert_bits_eq(&roundtrip::(&[3.25]), &[3.25]); + } + + #[test] + fn test_roundtrip_empty() { + let mut encoder = AlpEncoder::::new(); + let page = encoder.flush_buffer().unwrap(); + assert_eq!(page.len(), ALP_HEADER_SIZE); + + let mut decoder = AlpDecoder::::new(); + decoder.set_data(page, 0).unwrap(); + assert_eq!(decoder.values_left(), 0); + } + + /// The encoder must be reusable across pages, and the candidate set chosen on + /// the first page must still produce valid pages for later ones. + #[test] + fn test_roundtrip_multiple_pages() { + let mut encoder = AlpEncoder::::new(); + + for page_idx in 0..3 { + let values: Vec = (0..1500) + .map(|i| (i as f64) * 0.01 + (page_idx as f64)) + .collect(); + encoder.put(&values).unwrap(); + let page = encoder.flush_buffer().unwrap(); + + let mut decoder = AlpDecoder::::new(); + decoder.set_data(page, values.len()).unwrap(); + let mut out = vec![0.0f64; values.len()]; + assert_eq!(decoder.get(&mut out).unwrap(), values.len()); + assert_bits_eq(&out, &values); + } + } + + /// Two-decimal data must encode with no exceptions and pack far below the 64 + /// bits an unencoded double takes. + /// + /// The parameter to check is `exponent - factor`, not `exponent`: the encoded + /// integers depend only on the effective scale `10^(exponent - factor)`, so + /// (3, 1) and (2, 0) encode identically and tie on estimated size. The ALP + /// paper's tie-break then prefers the larger exponent and factor, which is + /// why the pair chosen here is not simply (2, 0). + #[test] + fn test_selects_decimal_parameters() { + let values: Vec = (0..1024).map(|i| (i as f64) * 0.01).collect(); + + let mut encoder = AlpEncoder::::new(); + encoder.put(&values).unwrap(); + let page = encoder.flush_buffer().unwrap(); + + // [header][one u32 offset][exponent][factor][num_exceptions]... + let exponent = page[ALP_HEADER_SIZE + 4]; + let factor = page[ALP_HEADER_SIZE + 5]; + let num_exceptions = + u16::from_le_bytes([page[ALP_HEADER_SIZE + 6], page[ALP_HEADER_SIZE + 7]]); + + assert_eq!( + exponent - factor, + 2, + "two-decimal data should encode at an effective scale of 10^2, \ + got exponent {exponent} factor {factor}" + ); + assert_eq!(num_exceptions, 0, "two-decimal data should not except"); + + let plain_size = values.len() * std::mem::size_of::(); + assert!( + page.len() * 4 < plain_size, + "expected at least 4x compression, got {} bytes vs {plain_size} plain", + page.len() + ); + } + + /// A single exception must not blow up the frame of reference: its slot is + /// filled with a real encoded value, so the bit width stays tight. + #[test] + fn test_exception_placeholder_keeps_bit_width_tight() { + let mut values: Vec = (0..1024).map(|i| (i as f64) * 0.01).collect(); + values[500] = f64::NAN; + + let mut encoder = AlpEncoder::::new(); + encoder.put(&values).unwrap(); + let page = encoder.flush_buffer().unwrap(); + + // [header][offset][AlpInfo(4)][frame_of_reference(8)][bit_width(1)] + let bit_width = page[ALP_HEADER_SIZE + 4 + AlpInfo::STORED_SIZE + 8]; + assert!( + bit_width <= 17, + "one exception should not widen the frame; got bit_width {bit_width}" + ); + + assert_bits_eq(&roundtrip::(&values), &values); + } +} + +#[cfg(test)] +mod conformance { + use super::*; + use crate::column::page::Page; + use crate::data_type::FloatType; + use crate::encodings::decoding::Decoder; + use crate::encodings::decoding::alp_decoder::AlpDecoder; + use crate::file::reader::{FileReader, SerializedFileReader}; + + /// Re-encode the values from a page written by the arrow-cpp ALP + /// implementation and require our bytes to match theirs exactly. + /// + /// The parameter search is explicitly non-normative - the spec says any + /// valid (exponent, factor) pair decodes correctly - so this is not a + /// conformance requirement in itself. It is a much sharper test than one: + /// byte identity pins the decimal parameters, the exception set, the + /// placeholder substitution, the frame of reference, the bit width and the + /// packing order all at once, against an independent implementation. + #[test] + fn test_matches_cpp_reference_page() { + let path = format!( + "{}/alp_float_arade.parquet", + arrow::util::test_util::parquet_test_data() + ); + let reader = SerializedFileReader::new(std::fs::File::open(&path).unwrap()).unwrap(); + let mut page_reader = reader + .get_row_group(0) + .unwrap() + .get_column_page_reader(0) + .unwrap(); + + let mut pages_checked = 0; + while let Some(page) = page_reader.get_next_page().unwrap() { + let Page::DataPage { + buf, + num_values, + encoding: Encoding::ALP, + .. + } = &page + else { + continue; + }; + let num_values = *num_values as usize; + + // A v1 data page stores its definition levels ahead of the encoded + // values: a 4-byte length, then that many bytes. + let def_len = u32::from_le_bytes(buf.as_ref()[0..4].try_into().unwrap()) as usize; + let cpp_page = buf.slice(4 + def_len..); + + let mut decoder = AlpDecoder::::new(); + decoder.set_data(cpp_page.clone(), num_values).unwrap(); + let mut values = vec![0.0f32; num_values]; + assert_eq!(decoder.get(&mut values).unwrap(), num_values); + + let mut encoder = AlpEncoder::::new(); + encoder.put(&values).unwrap(); + let our_page = encoder.flush_buffer().unwrap(); + + assert_eq!( + our_page.as_ref(), + cpp_page.as_ref(), + "re-encoded page differs from the arrow-cpp reference page" + ); + pages_checked += 1; + } + assert!(pages_checked > 0, "no ALP data page found in the test file"); + } +} diff --git a/parquet/src/encodings/encoding/mod.rs b/parquet/src/encodings/encoding/mod.rs index eeabcf4ba5ce..cece022fb1c4 100644 --- a/parquet/src/encodings/encoding/mod.rs +++ b/parquet/src/encodings/encoding/mod.rs @@ -27,10 +27,12 @@ use crate::errors::{ParquetError, Result}; use crate::schema::types::ColumnDescPtr; use crate::util::bit_util::{BitWriter, num_required_bits}; +use alp_encoder::AlpEncoder; use byte_stream_split_encoder::{ByteStreamSplitEncoder, VariableWidthByteStreamSplitEncoder}; use bytes::Bytes; pub use dict_encoder::DictEncoder; +mod alp_encoder; mod byte_stream_split_encoder; mod dict_encoder; @@ -84,26 +86,90 @@ pub fn get_encoder( encoding: Encoding, descr: &ColumnDescPtr, ) -> Result>> { - let encoder: Box> = match encoding { - Encoding::PLAIN => Box::new(PlainEncoder::new()), - Encoding::RLE_DICTIONARY | Encoding::PLAIN_DICTIONARY => { - return Err(general_err!( - "Cannot initialize this encoding through this function" - )); + ::get_encoder(descr, encoding) +} + +pub(crate) mod private { + use super::*; + + /// A trait that allows getting an [`Encoder`] implementation for a [`DataType`] + /// with the corresponding [`ParquetValueType`]. This is necessary to support + /// [`Encoder`] implementations that may not be applicable for all [`DataType`] + /// and by extension all [`ParquetValueType`], such as ALP, which encodes only + /// floating-point columns. + /// + /// [`ParquetValueType`]: crate::data_type::private::ParquetValueType + pub trait GetEncoder { + fn get_encoder>( + descr: &ColumnDescPtr, + encoding: Encoding, + ) -> Result>> { + get_encoder_default(descr, encoding) } - Encoding::RLE => Box::new(RleValueEncoder::new()), - Encoding::DELTA_BINARY_PACKED => Box::new(DeltaBitPackEncoder::new()), - Encoding::DELTA_LENGTH_BYTE_ARRAY => Box::new(DeltaLengthByteArrayEncoder::new()), - Encoding::DELTA_BYTE_ARRAY => Box::new(DeltaByteArrayEncoder::new()), - Encoding::BYTE_STREAM_SPLIT => match T::get_physical_type() { - Type::FIXED_LEN_BYTE_ARRAY => Box::new(VariableWidthByteStreamSplitEncoder::new( - descr.type_length(), - )), - _ => Box::new(ByteStreamSplitEncoder::new()), - }, - e => return Err(nyi_err!("Encoding {} is not supported", e)), - }; - Ok(encoder) + } + + fn get_encoder_default( + descr: &ColumnDescPtr, + encoding: Encoding, + ) -> Result>> { + let encoder: Box> = match encoding { + Encoding::PLAIN => Box::new(PlainEncoder::new()), + Encoding::RLE_DICTIONARY | Encoding::PLAIN_DICTIONARY => { + return Err(general_err!( + "Cannot initialize this encoding through this function" + )); + } + Encoding::RLE => Box::new(RleValueEncoder::new()), + Encoding::DELTA_BINARY_PACKED => Box::new(DeltaBitPackEncoder::new()), + Encoding::DELTA_LENGTH_BYTE_ARRAY => Box::new(DeltaLengthByteArrayEncoder::new()), + Encoding::DELTA_BYTE_ARRAY => Box::new(DeltaByteArrayEncoder::new()), + Encoding::BYTE_STREAM_SPLIT => match T::get_physical_type() { + Type::FIXED_LEN_BYTE_ARRAY => Box::new(VariableWidthByteStreamSplitEncoder::new( + descr.type_length(), + )), + _ => Box::new(ByteStreamSplitEncoder::new()), + }, + Encoding::ALP => { + return Err(general_err!( + "Encoding {} is not supported for type", + encoding + )); + } + e => return Err(nyi_err!("Encoding {} is not supported", e)), + }; + Ok(encoder) + } + + impl GetEncoder for bool {} + impl GetEncoder for i32 {} + impl GetEncoder for i64 {} + impl GetEncoder for Int96 {} + impl GetEncoder for ByteArray {} + impl GetEncoder for FixedLenByteArray {} + + impl GetEncoder for f32 { + fn get_encoder>( + descr: &ColumnDescPtr, + encoding: Encoding, + ) -> Result>> { + match encoding { + Encoding::ALP => Ok(Box::new(AlpEncoder::new())), + _ => get_encoder_default(descr, encoding), + } + } + } + + impl GetEncoder for f64 { + fn get_encoder>( + descr: &ColumnDescPtr, + encoding: Encoding, + ) -> Result>> { + match encoding { + Encoding::ALP => Ok(Box::new(AlpEncoder::new())), + _ => get_encoder_default(descr, encoding), + } + } + } } // ---------------------------------------------------------------------- diff --git a/parquet/tests/arrow_reader/alp.rs b/parquet/tests/arrow_reader/alp.rs index e750bbc40e75..88cb6e983ee2 100644 --- a/parquet/tests/arrow_reader/alp.rs +++ b/parquet/tests/arrow_reader/alp.rs @@ -18,11 +18,15 @@ use arrow::compute::concat_batches; use arrow::util::test_util::parquet_test_data; use arrow_array::cast::as_primitive_array; -use arrow_array::types::Float32Type; -use arrow_array::{Array, RecordBatch}; +use arrow_array::types::{Float32Type, Float64Type}; +use arrow_array::{Array, Float32Array, Float64Array, RecordBatch}; use arrow_csv::ReaderBuilder as CsvReaderBuilder; use arrow_schema::{DataType, Field, Schema}; +use bytes::Bytes; +use parquet::arrow::ArrowWriter; use parquet::arrow::arrow_reader::ArrowReaderBuilder; +use parquet::basic::Encoding; +use parquet::file::properties::{WriterProperties, WriterVersion}; use std::fs::File; use std::path::PathBuf; use std::sync::Arc; @@ -67,6 +71,162 @@ fn test_read_f32_alp() { } } +/// Write the arade values with the ALP encoder and read them back, over real +/// float data rather than synthetic decimals. +/// +/// This checks losslessness only, not compression: these are `f32` values whose +/// encoded integers need 31 bits and which except 6.4% of the time, so ALP is +/// larger than PLAIN here. The arrow-cpp implementation produces a byte-identical +/// page from the same values (see `test_matches_cpp_reference_page`), so that is +/// a property of the data, not of the encoder. ALP's win on arade in the paper is +/// on the `f64` version of the dataset. +#[test] +fn test_write_f32_alp_roundtrip() { + let data_dir = PathBuf::from(parquet_test_data()); + let expected = read_expected_csv_batch(&data_dir.join("alp_arade_expect.csv")); + + let alp_bytes = write_batch(&expected, Encoding::ALP); + let actual = read_parquet_bytes(alp_bytes); + assert_eq!(actual.num_rows(), expected.num_rows(), "row count mismatch"); + + for col_idx in 0..expected.num_columns() { + let col_name = expected.schema().field(col_idx).name().clone(); + let actual_col = as_primitive_array::(actual.column(col_idx).as_ref()); + let expected_col = as_primitive_array::(expected.column(col_idx).as_ref()); + + for row_idx in 0..expected.num_rows() { + assert_eq!( + actual_col.is_valid(row_idx), + expected_col.is_valid(row_idx), + "null mismatch at column {col_name} row {row_idx}" + ); + if expected_col.is_valid(row_idx) { + // Bitwise, so that NaN and -0.0 are held to the same standard. + assert_eq!( + actual_col.value(row_idx).to_bits(), + expected_col.value(row_idx).to_bits(), + "bit mismatch at column {col_name} row {row_idx}" + ); + } + } + } +} + +/// Round-trip a nullable column under both data page versions. +/// +/// The versions differ in what the reader can tell the value decoder. A v2 page +/// header carries `num_nulls`, so the decoder is handed the exact count of +/// encoded values; a v1 header carries only `num_values`, which counts nulls, so +/// the decoder receives the level count instead. Only non-null values are +/// encoded either way, which is why the ALP header's element count - not the +/// count the reader passes in - is what governs the decode. +/// +/// The exception values are here deliberately: they make the encoded and +/// unencoded value counts differ from the level count in two different ways at +/// once. +#[test] +fn test_alp_roundtrip_page_versions_with_nulls() { + let f32_values: Vec> = (0..3000) + .map(|i| match i % 100 { + 0 => None, + 7 => Some(f32::NAN), + 13 => Some(-0.0), + _ => Some(i as f32 * 0.01), + }) + .collect(); + let f64_values: Vec> = (0..3000) + .map(|i| match i % 100 { + 5 => None, + 11 => Some(f64::INFINITY), + _ => Some(i as f64 * 0.001), + }) + .collect(); + + let schema = Arc::new(Schema::new(vec![ + Field::new("f32", DataType::Float32, true), + Field::new("f64", DataType::Float64, true), + ])); + let batch = RecordBatch::try_new( + schema, + vec![ + Arc::new(Float32Array::from(f32_values.clone())), + Arc::new(Float64Array::from(f64_values.clone())), + ], + ) + .unwrap(); + + for version in [WriterVersion::PARQUET_1_0, WriterVersion::PARQUET_2_0] { + let props = WriterProperties::builder() + .set_dictionary_enabled(false) + .set_encoding(Encoding::ALP) + .set_writer_version(version) + .build(); + + let mut buf = Vec::new(); + let mut writer = ArrowWriter::try_new(&mut buf, batch.schema(), Some(props)).unwrap(); + writer.write(&batch).unwrap(); + writer.close().unwrap(); + + let actual = read_parquet_bytes(buf.into()); + assert_eq!( + actual.num_rows(), + batch.num_rows(), + "{version:?}: row count" + ); + + let actual_f32 = as_primitive_array::(actual.column(0).as_ref()); + for (row, expected) in f32_values.iter().enumerate() { + match expected { + None => assert!( + actual_f32.is_null(row), + "{version:?}: f32 row {row} not null" + ), + // Bitwise, so NaN and -0.0 are held to the same standard. + Some(value) => assert_eq!( + actual_f32.value(row).to_bits(), + value.to_bits(), + "{version:?}: f32 row {row}" + ), + } + } + + let actual_f64 = as_primitive_array::(actual.column(1).as_ref()); + for (row, expected) in f64_values.iter().enumerate() { + match expected { + None => assert!( + actual_f64.is_null(row), + "{version:?}: f64 row {row} not null" + ), + Some(value) => assert_eq!( + actual_f64.value(row).to_bits(), + value.to_bits(), + "{version:?}: f64 row {row}" + ), + } + } + } +} + +fn write_batch(batch: &RecordBatch, encoding: Encoding) -> Bytes { + let props = WriterProperties::builder() + .set_dictionary_enabled(false) + .set_encoding(encoding) + .build(); + + let mut buf = Vec::new(); + let mut writer = ArrowWriter::try_new(&mut buf, batch.schema(), Some(props)).unwrap(); + writer.write(batch).unwrap(); + writer.close().unwrap(); + buf.into() +} + +fn read_parquet_bytes(bytes: Bytes) -> RecordBatch { + let reader = ArrowReaderBuilder::try_new(bytes).unwrap().build().unwrap(); + let batches: Vec = reader.map(|b| b.unwrap()).collect(); + assert!(!batches.is_empty(), "expected non-empty parquet batch set"); + concat_batches(batches[0].schema_ref(), &batches).unwrap() +} + fn alp_schema() -> Arc { Arc::new(Schema::new(vec![ Field::new("value1", DataType::Float32, true), From dc193a612a55333207c00df26a321b5f23f8f3c5 Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Thu, 16 Jul 2026 07:57:43 -0400 Subject: [PATCH 42/50] skip loading each vector during skip --- parquet/src/encodings/decoding/alp_decoder.rs | 165 ++++++++++++++++-- 1 file changed, 153 insertions(+), 12 deletions(-) diff --git a/parquet/src/encodings/decoding/alp_decoder.rs b/parquet/src/encodings/decoding/alp_decoder.rs index 572c0af7a91e..fde4a0549590 100644 --- a/parquet/src/encodings/decoding/alp_decoder.rs +++ b/parquet/src/encodings/decoding/alp_decoder.rs @@ -540,29 +540,67 @@ where Encoding::ALP } - /// Skip up to `num_values` values, advancing the per-vector cursor (and the - /// underlying bit reader) without decoding. + /// Skip up to `num_values` values. + /// + /// Skipped-over vectors are not parsed, so a multi-vector skip is O(1) in + /// vectors skipped - and, unlike a sequential read, they are not validated. fn skip(&mut self, num_values: usize) -> Result { let to_skip = num_values.min(self.num_values); if to_skip == 0 { return Ok(0); } - let mut skipped = 0; - while skipped < to_skip { - if self.current.as_ref().is_none_or(|c| c.remaining == 0) { - self.load_current_vector()?; + let mut left = to_skip; + + // The current vector carries live bit-reader/cursor state, so a skip + // within it advances it in place rather than jumping. + if let Some(cur) = self.current.as_mut() { + let within = left.min(cur.remaining); + if cur.bit_width != 0 { + cur.reader.skip(within, cur.bit_width as usize); } + cur.delivered += within; + cur.remaining -= within; + self.num_values -= within; + left -= within; + } + if left == 0 { + return Ok(to_skip); + } + + let vector_size = self.header.vector_size; + let num_elements = self.header.num_elements; + let target = (num_elements - self.num_values) + left; + + self.current = None; + self.num_values = num_elements - target; + + if target == num_elements { + self.next_vector_idx = self.header.num_vectors(); + return Ok(to_skip); + } + + // vector_size is a power of two: shift and mask instead of div/rem. + let landing = target >> vector_size.trailing_zeros(); + let within = target & (vector_size - 1); + + // Seed expected_next_offset so the jump satisfies load_current_vector's + // contiguity check; validation resumes from the landing vector. + self.next_vector_idx = landing; + self.expected_next_offset = read_offset(self.body.as_ref(), landing)?; + + // within == 0 lands on a boundary and is loaded lazily by the next + // get/skip; a mid-vector landing must be parsed and advanced now. + if within > 0 { + self.load_current_vector()?; let cur = self.current.as_mut().unwrap(); - let n = cur.remaining.min(to_skip - skipped); if cur.bit_width != 0 { - cur.reader.skip(n, cur.bit_width as usize); + cur.reader.skip(within, cur.bit_width as usize); } - cur.delivered += n; - cur.remaining -= n; - skipped += n; - self.num_values -= n; + cur.delivered = within; + cur.remaining -= within; } + Ok(to_skip) } } @@ -1410,4 +1448,107 @@ mod tests { let mut extra = [0.0f32; 1]; assert_eq!(decoder.get(&mut extra).unwrap(), 0); } + + /// A skip must land where a sequential decode would, across boundaries and + /// over vectors carrying exceptions. `exponent = factor = 0` makes the + /// decode the identity `frame_of_reference + delta`, so expected values fall + /// out of the packed deltas. + #[test] + fn test_alp_decoder_skip_matches_sequential_decode() { + use crate::util::bit_util::BitWriter; + + fn pack(deltas: &[u64], bit_width: usize) -> Vec { + let mut w = BitWriter::new(deltas.len() * 2 + 16); + for &d in deltas { + w.put_value(d, bit_width); + } + w.flush(); + w.consume() + } + + struct Spec { + for_ref: u64, + bit_width: usize, + len: usize, + exc_pos: u16, + exc_val: f64, + } + let specs = [ + Spec { for_ref: 1000, bit_width: 4, len: 1024, exc_pos: 100, exc_val: f64::NAN }, + Spec { for_ref: 5000, bit_width: 8, len: 1024, exc_pos: 300, exc_val: f64::INFINITY }, + Spec { for_ref: 100, bit_width: 2, len: 552, exc_pos: 200, exc_val: -0.0 }, + ]; + + let mut vectors = Vec::new(); + let mut expected: Vec = Vec::new(); + for s in &specs { + let modulo = 1u64 << s.bit_width; + let deltas: Vec = (0..s.len).map(|j| (j as u64) % modulo).collect(); + let packed = pack(&deltas, s.bit_width); + vectors.push(make_vector(VectorSpec { + exponent: 0, + factor: 0, + frame_of_reference: s.for_ref, + bit_width: s.bit_width as u8, + packed_values: &packed, + exception_positions: &[s.exc_pos], + exception_values: &[s.exc_val.to_bits()], + })); + for (j, &d) in deltas.iter().enumerate() { + expected.push(if j as u16 == s.exc_pos { + s.exc_val + } else { + (s.for_ref + d) as f64 + }); + } + } + let n = expected.len(); + let page = make_page_from_vectors(10, n as i32, &vectors); + + let assert_bits = |actual: &[f64], expected: &[f64], ctx: &str| { + assert_eq!(actual.len(), expected.len(), "{ctx}: length"); + for (i, (a, e)) in actual.iter().zip(expected.iter()).enumerate() { + assert_eq!(a.to_bits(), e.to_bits(), "{ctx}: mismatch at {i}"); + } + }; + + // Sanity check: a straight sequential decode reproduces `expected`. + assert_bits(&decode_page::(page.clone(), n), &expected, "sequential"); + + // Skip from the start: boundary-aligned (1024, 2048), mid-vector (1, 1025, + // 2222), just before the end (2599), and exactly to the end (2600). + for &k in &[0usize, 1, 1023, 1024, 1025, 2048, 2222, 2599, 2600] { + let mut d = AlpDecoder::::new(); + d.set_data(Bytes::from(page.clone()), n).unwrap(); + assert_eq!(d.skip(k).unwrap(), k); + assert_eq!(d.values_left(), n - k); + + let mut tail = vec![0.0f64; n - k]; + assert_eq!(d.get(&mut tail).unwrap(), n - k); + assert_bits(&tail, &expected[k..], &format!("skip {k}")); + } + + // Skip that starts mid-vector: consume part of vector 0, then skip across + // the vector-0/1 boundary before reading the rest. + let mut d = AlpDecoder::::new(); + d.set_data(Bytes::from(page.clone()), n).unwrap(); + let mut head = vec![0.0f64; 500]; + assert_eq!(d.get(&mut head).unwrap(), 500); + assert_bits(&head, &expected[..500], "head"); + assert_eq!(d.skip(700).unwrap(), 700); // 500..1200, crossing into vector 1 + assert_eq!(d.values_left(), n - 1200); + let mut tail = vec![0.0f64; n - 1200]; + assert_eq!(d.get(&mut tail).unwrap(), n - 1200); + assert_bits(&tail, &expected[1200..], "tail"); + + // Two consecutive skips, the first landing boundary-aligned (lazy, no + // parse), the second jumping again from that lazy position. + let mut d = AlpDecoder::::new(); + d.set_data(Bytes::from(page.clone()), n).unwrap(); + assert_eq!(d.skip(1024).unwrap(), 1024); // boundary -> lazy landing + assert_eq!(d.skip(600).unwrap(), 600); // jump again from the boundary + let mut tail = vec![0.0f64; n - 1624]; + assert_eq!(d.get(&mut tail).unwrap(), n - 1624); + assert_bits(&tail, &expected[1624..], "double skip"); + } } From 5b0d8adb430a940f36faa81843ce7b330e383316 Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Fri, 17 Jul 2026 10:19:15 -0400 Subject: [PATCH 43/50] add streaming ALP encoder + throughput bench StreamingPage encodes pages 2..N vector-by-vector as values arrive via put(), holding only a <1024 carry plus the compressed body instead of buffering the whole page. Page 1 still buffers to build the preset. Output is provably byte-identical to the buffered encoder (pinned by test_streaming_matches_buffered / test_streaming_irregular_puts). Adds an #[ignore]d rdtsc throughput bench (mod throughput) reporting cyc/val for encode buffered/streaming and decode. Co-Authored-By: Claude Opus 4.8 (1M context) --- parquet/src/encodings/encoding/alp_encoder.rs | 387 +++++++++++++++++- 1 file changed, 373 insertions(+), 14 deletions(-) diff --git a/parquet/src/encodings/encoding/alp_encoder.rs b/parquet/src/encodings/encoding/alp_encoder.rs index 0a0156c3dead..38e173e10f01 100644 --- a/parquet/src/encodings/encoding/alp_encoder.rs +++ b/parquet/src/encodings/encoding/alp_encoder.rs @@ -447,21 +447,157 @@ fn encode_page( Ok(page) } +/// A page encoded incrementally, one vector at a time. +/// +/// Once the column chunk's preset is known (after the first page), later pages do +/// not need all their values at once: each vector is encoded and appended to +/// `body` as soon as its [`VECTOR_SIZE`] values arrive, and the raw floats are +/// dropped. Only a sub-vector remainder is held in `carry` between `put`s. The +/// header and offset array - which depend on the final vector count - are +/// prepended in [`StreamingPage::finish`]. +/// +/// The bytes produced are identical to encoding the same values in one pass with +/// [`encode_page`]: same vectors, same per-vector parameters, same contiguous +/// offsets. What changes is peak memory - the whole page of raw floats is never +/// resident - and that the encode work is spread across `put`s. +struct StreamingPage { + /// Encoded vectors, concatenated; becomes the page body after the offsets. + body: Vec, + /// Body-relative start offset of each completed vector. + vector_offsets: Vec, + /// Values not yet forming a complete vector, carried across `put`s. + carry: Vec, + /// Total values appended to this page so far. + count: usize, +} + +impl StreamingPage { + fn new() -> Self { + Self { + body: Vec::new(), + vector_offsets: Vec::new(), + carry: Vec::new(), + count: 0, + } + } + + fn estimated_memory_size(&self) -> usize { + self.body.capacity() + + self.vector_offsets.capacity() * std::mem::size_of::() + + self.carry.capacity() * std::mem::size_of::() + } + + /// Encode one complete vector and append it to `body`. + fn push_vector( + &mut self, + vector: &[F], + preset: &[ExponentAndFactor], + scratch: &mut Scratch, + ) -> Result<()> { + let body_start = u32::try_from(self.body.len()) + .map_err(|_| general_err!("Invalid ALP page: body exceeds u32 offset range"))?; + self.vector_offsets.push(body_start); + let params = select_params(vector, preset, &mut scratch.sample); + encode_vector(vector, params, scratch, &mut self.body) + } + + /// Buffer `values`, encoding and dropping every complete vector they form. + fn put( + &mut self, + mut values: &[F], + preset: &[ExponentAndFactor], + scratch: &mut Scratch, + ) -> Result<()> { + self.count += values.len(); + + // Finish a vector left partially filled by a previous `put`. + if !self.carry.is_empty() { + let need = VECTOR_SIZE - self.carry.len(); + if values.len() < need { + self.carry.extend_from_slice(values); + return Ok(()); + } + let (head, tail) = values.split_at(need); + self.carry.extend_from_slice(head); + // Move the carry out so the vector slice does not alias `self`; the + // allocation is handed back, cleared, for the next partial to reuse. + let mut vector = std::mem::take(&mut self.carry); + self.push_vector(&vector, preset, scratch)?; + vector.clear(); + self.carry = vector; + values = tail; + } + + // Encode whole vectors straight from the input, no copy. + let mut chunks = values.chunks_exact(VECTOR_SIZE); + for vector in chunks.by_ref() { + self.push_vector(vector, preset, scratch)?; + } + self.carry.extend_from_slice(chunks.remainder()); + Ok(()) + } + + /// Encode the trailing partial vector, then assemble `[header][offsets][body]` + /// and reset for the next page. + fn finish( + &mut self, + preset: &[ExponentAndFactor], + scratch: &mut Scratch, + ) -> Result> { + if !self.carry.is_empty() { + let mut vector = std::mem::take(&mut self.carry); + self.push_vector(&vector, preset, scratch)?; + vector.clear(); + self.carry = vector; + } + + let num_vectors = self.vector_offsets.len(); + let offsets_section = num_vectors * std::mem::size_of::(); + let header = AlpHeader { + compression_mode: ALP_COMPRESSION_MODE, + integer_encoding: ALP_INTEGER_ENCODING_FOR_BIT_PACK, + vector_size: VECTOR_SIZE, + num_elements: self.count, + }; + + let mut page = Vec::with_capacity(ALP_HEADER_SIZE + offsets_section + self.body.len()); + page.extend_from_slice(&header.serialize()?); + for &body_start in &self.vector_offsets { + // Offsets are measured from the start of the offset array: the array's + // own size plus the vector's position within the body. + let page_offset = u32::try_from(offsets_section + body_start as usize) + .map_err(|_| general_err!("Invalid ALP page: body exceeds u32 offset range"))?; + page.extend_from_slice(&page_offset.to_le_bytes()); + } + page.extend_from_slice(&self.body); + + self.body.clear(); + self.vector_offsets.clear(); + self.count = 0; + Ok(page) + } +} + /// Encoder for ALP-encoded floating-point pages (`f32`/`f64`). /// -/// Values are buffered and encoded on flush, because ALP chooses its decimal -/// parameters per vector by sampling the values, and the candidate set for the -/// column chunk is derived from the first page's data. +/// The first page is buffered whole: ALP samples it to choose the column chunk's +/// candidate `(exponent, factor)` set, which needs all of the page's data. Once +/// that preset is fixed, later pages are encoded incrementally, a vector at a +/// time, without ever holding the whole page of raw floats (see [`StreamingPage`]). pub struct AlpEncoder where T::T: AlpFloat, { - /// Values buffered for the page currently being built. + /// Values buffered for the first page, until the preset is built. Empty once + /// streaming begins. values: Vec, /// Candidate `(exponent, factor)` pairs for this column chunk, sampled once - /// from the first page and reused for the rest of the chunk. + /// from the first page and reused for the rest of the chunk. `None` until the + /// first page is flushed; its presence is what switches `put` to streaming. preset: Option>, scratch: Scratch, + /// The page built incrementally, used for every page after the first. + streaming: StreamingPage, } impl AlpEncoder @@ -473,6 +609,17 @@ where values: Vec::new(), preset: None, scratch: Scratch::new(), + streaming: StreamingPage::new(), + } + } + + /// Values buffered for the current page: the first page lives in `values`, + /// later pages in the streaming buffer. Used only for size estimates. + fn current_page_len(&self) -> usize { + if self.preset.is_none() { + self.values.len() + } else { + self.streaming.count } } } @@ -482,7 +629,18 @@ where T::T: AlpFloat, { fn put(&mut self, values: &[T::T]) -> Result<()> { - self.values.extend_from_slice(values); + let Self { + values: buffer, + preset, + scratch, + streaming, + } = self; + match preset.as_deref() { + // First page: buffer until the preset can be built on flush. + None => buffer.extend_from_slice(values), + // Later pages: encode incrementally against the fixed preset. + Some(preset) => streaming.put(values, preset, scratch)?, + } Ok(()) } @@ -493,15 +651,17 @@ where fn estimated_data_encoded_size(&self) -> usize { // Encoded size is not known until the parameters are chosen, so bound it // by the unencoded size: a vector never encodes larger than storing every - // value as an exception. - let num_vectors = self.values.len().div_ceil(VECTOR_SIZE); + // value as an exception. Bounding by value count (not the running encoded + // size) keeps the writer's page-boundary decisions identical whether the + // page is buffered or streamed. + let len = self.current_page_len(); + let num_vectors = len.div_ceil(VECTOR_SIZE); ALP_HEADER_SIZE + num_vectors * (std::mem::size_of::() + AlpInfo::STORED_SIZE + ForInfo::<::Exact>::stored_size()) - + self.values.len() - * (::Exact::WIDTH + std::mem::size_of::()) + + len * (::Exact::WIDTH + std::mem::size_of::()) } fn estimated_memory_size(&self) -> usize { @@ -510,13 +670,29 @@ where p.capacity() * std::mem::size_of::() }) + self.scratch.estimated_memory_size() + + self.streaming.estimated_memory_size() } fn flush_buffer(&mut self) -> Result { - let values = &self.values; - let preset = self.preset.get_or_insert_with(|| build_preset(values)); - let page = encode_page(&self.values, preset, &mut self.scratch)?; - self.values.clear(); + let Self { + values, + preset, + scratch, + streaming, + } = self; + + // The first flush builds the preset from the whole buffered page and + // encodes it in one pass; that also arms streaming for later pages. + let page = match preset { + None => { + let built = build_preset(values); + let page = encode_page(values, &built, scratch)?; + values.clear(); + *preset = Some(built); + page + } + Some(preset) => streaming.finish(preset.as_slice(), scratch)?, + }; Ok(page.into()) } } @@ -710,6 +886,65 @@ mod tests { assert_bits_eq(&roundtrip::(&values), &values); } + + /// Streaming pages (every page after the first) must be byte-for-byte + /// identical to encoding the same values in one pass with the same preset. + /// This is the guarantee that makes the streaming path a pure refactor. + #[test] + fn test_streaming_matches_buffered() { + let page1: Vec = (0..3000).map(|i| (i as f64) * 0.01 + 1.23).collect(); + let page2: Vec = (0..3000).map(|i| (i as f64) * 0.03 - 7.0).collect(); + + let mut encoder = AlpEncoder::::new(); + encoder.put(&page1).unwrap(); + let _ = encoder.flush_buffer().unwrap(); // builds and caches the preset + + // Feed page 2 in irregular chunks that cross vector boundaries. + for chunk in page2.chunks(997) { + encoder.put(chunk).unwrap(); + } + let streamed = encoder.flush_buffer().unwrap(); + + // The encoder built its preset from page 1; reproduce it to encode page 2 + // in a single pass as the reference. + let preset = build_preset(&page1); + let mut scratch = Scratch::::new(); + let reference = encode_page(&page2, &preset, &mut scratch).unwrap(); + + assert_eq!( + streamed.as_ref(), + reference.as_slice(), + "streamed page differs from single-pass encoding" + ); + } + + /// The carry logic must reassemble vectors correctly no matter how `put` calls + /// land relative to vector boundaries: sizes below, at, and above a vector. + #[test] + fn test_streaming_irregular_puts() { + let page1: Vec = (0..2048).map(|i| (i as f64) * 0.01).collect(); + let page2: Vec = (0..5000).map(|i| (i as f64) * 0.01 + 100.0).collect(); + + let mut encoder = AlpEncoder::::new(); + encoder.put(&page1).unwrap(); + let _ = encoder.flush_buffer().unwrap(); + + let sizes = [1usize, 1023, 2, 1024, 1025, 7, 900, 118]; + let (mut offset, mut i) = (0usize, 0usize); + while offset < page2.len() { + let n = sizes[i % sizes.len()].min(page2.len() - offset); + encoder.put(&page2[offset..offset + n]).unwrap(); + offset += n; + i += 1; + } + let page = encoder.flush_buffer().unwrap(); + + let mut decoder = AlpDecoder::::new(); + decoder.set_data(page, page2.len()).unwrap(); + let mut out = vec![0.0f64; page2.len()]; + assert_eq!(decoder.get(&mut out).unwrap(), page2.len()); + assert_bits_eq(&out, &page2); + } } #[cfg(test)] @@ -780,3 +1015,127 @@ mod conformance { assert!(pages_checked > 0, "no ALP data page found in the test file"); } } + +/// Throughput microbench (not a correctness test). Reports cycles/value for the +/// three hot paths, using `rdtsc`. The absolute numbers are TSC reference cycles +/// on this machine, not portable core cycles - but the *ratios* (streaming vs +/// buffered, encode vs decode) are robust, since the TSC skew is identical +/// across measurements. +/// +/// Run with: +/// ```text +/// cargo test -p parquet --release --lib alp_encoder::throughput -- --ignored --nocapture +/// ``` +#[cfg(all(test, target_arch = "x86_64"))] +mod throughput { + use super::*; + use crate::data_type::{DoubleType, FloatType}; + use crate::encodings::decoding::Decoder; + use crate::encodings::decoding::alp_decoder::AlpDecoder; + use core::arch::x86_64::{_mm_lfence, _rdtsc}; + use std::hint::black_box; + + const N: usize = 1 << 17; // ~one rowgroup + const PUT_CHUNK: usize = 2048; // a plausible writer sub-batch + const ITERS: u64 = 500; + + /// Minimum single-run TSC delta over `ITERS`, after warmup. Min rejects + /// scheduling/interrupt noise better than mean. + fn min_cycles(mut f: F) -> u64 { + for _ in 0..5 { + f(); + } + let mut best = u64::MAX; + for _ in 0..ITERS { + unsafe { + _mm_lfence(); + let t0 = _rdtsc(); + _mm_lfence(); + f(); + _mm_lfence(); + let t1 = _rdtsc(); + best = best.min(t1.wrapping_sub(t0)); + } + } + best + } + + fn report(label: &str, cycles: u64, n: usize) { + let cpv = cycles as f64 / n as f64; + println!("{label}: {cpv:6.2} cyc/val {:5.3} val/cyc", 1.0 / cpv); + } + + #[test] + #[ignore = "throughput bench; run with --release --ignored --nocapture"] + fn throughput_f64() { + // 2-decimal data, ~one rowgroup, few exceptions - the ALP common case. + let values: Vec = (0..N).map(|i| ((i % 100_000) as f64) * 0.01 + 1.0).collect(); + let preset = build_preset(&values); + let mut scratch = Scratch::::new(); + + let enc_buffered = min_cycles(|| { + let page = encode_page(black_box(&values), &preset, &mut scratch).unwrap(); + black_box(page); + }); + + let mut sp = StreamingPage::::new(); + let enc_streaming = min_cycles(|| { + for chunk in black_box(&values).chunks(PUT_CHUNK) { + sp.put(chunk, &preset, &mut scratch).unwrap(); + } + black_box(sp.finish(&preset, &mut scratch).unwrap()); + }); + + let page: Bytes = encode_page(&values, &preset, &mut scratch).unwrap().into(); + let mut out = vec![0.0f64; values.len()]; + let dec = min_cycles(|| { + let mut decoder = AlpDecoder::::new(); + decoder + .set_data(black_box(page.clone()), values.len()) + .unwrap(); + decoder.get(&mut out).unwrap(); + black_box(&out); + }); + + println!(); + report("f64 encode buffered ", enc_buffered, N); + report("f64 encode streaming", enc_streaming, N); + report("f64 decode ", dec, N); + } + + #[test] + #[ignore = "throughput bench; run with --release --ignored --nocapture"] + fn throughput_f32() { + let values: Vec = (0..N).map(|i| ((i % 100_000) as f32) * 0.01 + 1.0).collect(); + let preset = build_preset(&values); + let mut scratch = Scratch::::new(); + + let enc_buffered = min_cycles(|| { + black_box(encode_page(black_box(&values), &preset, &mut scratch).unwrap()); + }); + + let mut sp = StreamingPage::::new(); + let enc_streaming = min_cycles(|| { + for chunk in black_box(&values).chunks(PUT_CHUNK) { + sp.put(chunk, &preset, &mut scratch).unwrap(); + } + black_box(sp.finish(&preset, &mut scratch).unwrap()); + }); + + let page: Bytes = encode_page(&values, &preset, &mut scratch).unwrap().into(); + let mut out = vec![0.0f32; values.len()]; + let dec = min_cycles(|| { + let mut decoder = AlpDecoder::::new(); + decoder + .set_data(black_box(page.clone()), values.len()) + .unwrap(); + decoder.get(&mut out).unwrap(); + black_box(&out); + }); + + println!(); + report("f32 encode buffered ", enc_buffered, N); + report("f32 encode streaming", enc_streaming, N); + report("f32 decode ", dec, N); + } +} From 0dd03242da4b36b20bd913b8d2e280a2838ca0c2 Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Sat, 18 Jul 2026 20:14:31 -0400 Subject: [PATCH 44/50] use copysign for fast_round --- parquet/src/encodings/alp.rs | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/parquet/src/encodings/alp.rs b/parquet/src/encodings/alp.rs index adf02760b0fd..36fcf7c68d97 100644 --- a/parquet/src/encodings/alp.rs +++ b/parquet/src/encodings/alp.rs @@ -459,10 +459,13 @@ pub(crate) trait AlpFloat: Copy + Default + PartialEq + std::ops::Mul= 0 { +magic } else { -magic }` form but has no data-dependent + /// branch, which keeps the per-value encode loop autovectorizable. The + /// `+ magic - magic` cannot be algebraically cancelled - the rounding *is* + /// the intermediate loss of precision. fn fast_round(self) -> ::Signed; /// Encode one value with a precomputed [`AlpFloat::encode_scale`]. @@ -531,12 +534,8 @@ impl AlpFloat for f32 { } fn fast_round(self) -> i32 { - let rounded = if self >= 0.0 { - (self + Self::MAGIC_NUMBER) - Self::MAGIC_NUMBER - } else { - (self - Self::MAGIC_NUMBER) + Self::MAGIC_NUMBER - }; - rounded as i32 + let magic = Self::MAGIC_NUMBER.copysign(self); + ((self + magic) - magic) as i32 } } @@ -592,11 +591,7 @@ impl AlpFloat for f64 { } fn fast_round(self) -> i64 { - let rounded = if self >= 0.0 { - (self + Self::MAGIC_NUMBER) - Self::MAGIC_NUMBER - } else { - (self - Self::MAGIC_NUMBER) + Self::MAGIC_NUMBER - }; - rounded as i64 + let magic = Self::MAGIC_NUMBER.copysign(self); + ((self + magic) - magic) as i64 } } From e5d9bf97f16cda923705608f623da4c29326af52 Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Sat, 18 Jul 2026 20:42:01 -0400 Subject: [PATCH 45/50] branchless exceptions encode --- parquet/src/encodings/encoding/alp_encoder.rs | 49 ++++++++++++------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/parquet/src/encodings/encoding/alp_encoder.rs b/parquet/src/encodings/encoding/alp_encoder.rs index 38e173e10f01..47101b64e2a9 100644 --- a/parquet/src/encodings/encoding/alp_encoder.rs +++ b/parquet/src/encodings/encoding/alp_encoder.rs @@ -270,6 +270,9 @@ fn select_params( struct Scratch { /// Decimal-encoded integers, overwritten in place with their FOR deltas. encoded: Vec<::Signed>, + /// Per-value exception flag (`1` = round-trip failed). A byte, not a bitset, + /// so the map loop's per-lane store stays vectorizable. + exc_mask: Vec, exception_positions: Vec, exception_values: Vec, sample: Vec, @@ -279,6 +282,7 @@ impl Scratch { fn new() -> Self { Self { encoded: Vec::new(), + exc_mask: Vec::new(), exception_positions: Vec::new(), exception_values: Vec::new(), sample: Vec::new(), @@ -287,6 +291,7 @@ impl Scratch { fn estimated_memory_size(&self) -> usize { self.encoded.capacity() * std::mem::size_of::<::Signed>() + + self.exc_mask.capacity() + self.exception_positions.capacity() * std::mem::size_of::() + self.exception_values.capacity() * std::mem::size_of::() + self.sample.capacity() * std::mem::size_of::() @@ -306,27 +311,39 @@ fn encode_vector( let Scratch { encoded, + exc_mask, exception_positions, exception_values, .. } = scratch; - encoded.clear(); - exception_positions.clear(); + + let zero = F::Exact::default().reinterpret_as_signed(); + let n_values = values.len(); + encoded.resize(n_values, zero); + exc_mask.resize(n_values, 0); exception_values.clear(); - // A value is an exception when it does not survive the round trip. Values - // ALP cannot represent at all (NaN, the infinities, -0.0) encode to a - // sentinel whose round trip is guaranteed to mismatch, so they need no - // separate test here. Note that `-0.0 == 0.0`, so a naive equality check - // against the *input* would let -0.0 through and silently lose its sign. - for (idx, &value) in values.iter().enumerate() { + // Comparing the *decoded* value to the input (not the input to itself) flags + // -0.0, which encodes to +0.0. + for ((&value, enc_slot), mask_slot) in values + .iter() + .zip(encoded.iter_mut()) + .zip(exc_mask.iter_mut()) + { let encoded_value = value.encode_value(encode_scale); - encoded.push(encoded_value); - if F::decode_value(encoded_value, decode_scale) != value { - exception_positions.push(idx as u16); - } + *enc_slot = encoded_value; + *mask_slot = u8::from(F::decode_value(encoded_value, decode_scale) != value); } + // Branchless compaction: always write the index, advance only when flagged. + exception_positions.resize(n_values, 0); + let mut num_exceptions_usize = 0usize; + for (idx, &is_exception) in exc_mask.iter().enumerate() { + exception_positions[num_exceptions_usize] = idx as u16; + num_exceptions_usize += usize::from(is_exception); + } + exception_positions.truncate(num_exceptions_usize); + let num_exceptions = u16::try_from(exception_positions.len()).map_err(|_| { general_err!( "Invalid ALP vector: {} exceptions exceeds u16::MAX", @@ -334,17 +351,15 @@ fn encode_vector( ) })?; - // An exception still occupies a slot in the packed section. Filling it with - // a real encoded value - rather than leaving the sentinel there - keeps the - // FOR range tight, which is what the bit width is derived from. The true - // value is written verbatim into the exception section. + // Fill each exception's packed slot with a real value (not the sentinel) so + // the FOR range - and thus the bit width - stays tight; the true value goes + // in the exception section. let placeholder = first_non_exception_value::(encoded, exception_positions); for &position in exception_positions.iter() { exception_values.push(values[position as usize]); encoded[position as usize] = placeholder; } - let zero = F::Exact::default().reinterpret_as_signed(); let min = encoded.iter().copied().min().unwrap_or(zero); let max = encoded.iter().copied().max().unwrap_or(zero); From b58a7e5ef34e051f8c7a8855a867933399ab0d02 Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Sat, 18 Jul 2026 20:42:08 -0400 Subject: [PATCH 46/50] comment trim --- parquet/src/encodings/alp.rs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/parquet/src/encodings/alp.rs b/parquet/src/encodings/alp.rs index 36fcf7c68d97..d5ba4b2925ee 100644 --- a/parquet/src/encodings/alp.rs +++ b/parquet/src/encodings/alp.rs @@ -455,17 +455,11 @@ pub(crate) trait AlpFloat: Copy + Default + PartialEq + std::ops::Mul bool; - /// Round to the nearest integer by the "magic number" technique: adding and - /// then subtracting the magic number lands the value in a binade where the - /// unit in the last place is exactly 1.0, so the fractional bits fall off. - /// - /// The magic number must carry the sign of the value: negatives round in the - /// corresponding negative binade, so the constant is `copysign`ed onto the - /// input rather than branched on. This is byte-identical to the equivalent - /// `if self >= 0 { +magic } else { -magic }` form but has no data-dependent - /// branch, which keeps the per-value encode loop autovectorizable. The - /// `+ magic - magic` cannot be algebraically cancelled - the rounding *is* - /// the intermediate loss of precision. + /// Round to nearest by the "magic number" technique: `(x + magic) - magic` + /// lands `x` where the ULP is 1.0, dropping the fraction. The magic carries + /// the sign of `x` (`copysign`) so negatives round in their own binade - + /// byte-identical to branching on the sign but branchless, so the encode loop + /// vectorizes. The add/sub must not be cancelled: it *is* the rounding. fn fast_round(self) -> ::Signed; /// Encode one value with a precomputed [`AlpFloat::encode_scale`]. From 526b8afef95b1ec77d9cf32bf08a4e8b4908311a Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Fri, 31 Jul 2026 16:00:12 -0400 Subject: [PATCH 47/50] drop the sign in fast rounding --- parquet/src/encodings/alp.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/parquet/src/encodings/alp.rs b/parquet/src/encodings/alp.rs index d5ba4b2925ee..414ac081c703 100644 --- a/parquet/src/encodings/alp.rs +++ b/parquet/src/encodings/alp.rs @@ -445,9 +445,9 @@ pub(crate) trait AlpFloat: Copy + Default + PartialEq + std::ops::Mul Self::Scale; /// Apply a scale as the same two separate multiplications the decode side - /// uses. The spec requires two steps rather than one multiplication by a - /// combined constant, so that every implementation produces bit-identical - /// results. + /// uses. Two steps rather than one multiplication by a combined constant: + /// the spec requires this on the normative decode path, and encoding with + /// the same arithmetic maximizes the values that round-trip. fn apply_scale(self, scale: Self::Scale) -> Self; /// True for values ALP cannot turn into an exact integer: NaN, the @@ -455,11 +455,13 @@ pub(crate) trait AlpFloat: Copy + Default + PartialEq + std::ops::Mul bool; - /// Round to nearest by the "magic number" technique: `(x + magic) - magic` - /// lands `x` where the ULP is 1.0, dropping the fraction. The magic carries - /// the sign of `x` (`copysign`) so negatives round in their own binade - - /// byte-identical to branching on the sign but branchless, so the encode loop - /// vectorizes. The add/sub must not be cancelled: it *is* the rounding. + /// Round to nearest by the "magic number" technique. Adding `magic` pushes + /// `x` into the binade where floats are spaced exactly 1.0 apart, so the + /// add itself snaps to the nearest integer, and subtracting `magic` back + /// is exact. `magic = 1.5 * 2^mantissa_bits` keeps the sum in that binade + /// for negative `x` too. Values large enough to be mis-rounded just fail + /// the caller's round-trip check and become exceptions. The add/sub must + /// not be simplified away: it *is* the rounding. fn fast_round(self) -> ::Signed; /// Encode one value with a precomputed [`AlpFloat::encode_scale`]. @@ -528,8 +530,7 @@ impl AlpFloat for f32 { } fn fast_round(self) -> i32 { - let magic = Self::MAGIC_NUMBER.copysign(self); - ((self + magic) - magic) as i32 + ((self + Self::MAGIC_NUMBER) - Self::MAGIC_NUMBER) as i32 } } @@ -585,7 +586,6 @@ impl AlpFloat for f64 { } fn fast_round(self) -> i64 { - let magic = Self::MAGIC_NUMBER.copysign(self); - ((self + magic) - magic) as i64 + ((self + Self::MAGIC_NUMBER) - Self::MAGIC_NUMBER) as i64 } } From 21963463d29bee667cf5aa774a0fa95fdf72a9a8 Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Fri, 31 Jul 2026 16:12:32 -0400 Subject: [PATCH 48/50] drop the bench --- parquet/src/encodings/decoding/alp_decoder.rs | 168 ------------------ parquet/src/encodings/encoding/alp_encoder.rs | 124 ------------- 2 files changed, 292 deletions(-) diff --git a/parquet/src/encodings/decoding/alp_decoder.rs b/parquet/src/encodings/decoding/alp_decoder.rs index fde4a0549590..2e9cd07eebe7 100644 --- a/parquet/src/encodings/decoding/alp_decoder.rs +++ b/parquet/src/encodings/decoding/alp_decoder.rs @@ -1240,174 +1240,6 @@ mod tests { assert_eq!(decoder.values_left(), 2); } - /// Microbenchmark comparing three decode inner loops on identical packed - /// data, decoding into an 8 MB output to expose memory traffic: - /// 1. scalar `get_value` per element; - /// 2. bulk `get_batch` into a stack scratch tile, then a fused - /// inverse-FOR + decimal-decode pass writing into the output; - /// 3. in-place: `get_batch` straight into the output (viewed as packed - /// integers), then decode each slot over its own bytes — no scratch. - /// - /// Ignored by default; run with: - /// `cargo test -p parquet --release decode_inner_loop -- --ignored --nocapture` - #[test] - #[ignore = "microbenchmark"] - fn bench_decode_inner_loop_f64() { - use crate::util::bit_util::BitWriter; - use std::time::Instant; - - let n = 1024usize; // values per vector - let vectors = 1024usize; // -> 1M values, 8 MB output, well past L2 - let total = n * vectors; - let bit_width = 12usize; - let base = 1_000_000u64; - let scale = ::decode_scale(4, 2); - - let mut writer = BitWriter::new(n * 8); - for i in 0..n as u64 { - let delta = i.wrapping_mul(2_654_435_761) & ((1 << bit_width) - 1); - writer.put_value(delta, bit_width); - } - writer.flush(); - let packed = Bytes::from(writer.consume()); - - let iters = 30usize; - let mut out = vec![0.0f64; total]; - let mut sink = 0.0f64; - - // 1. scalar get_value - let t0 = Instant::now(); - for _ in 0..iters { - for chunk in out.chunks_mut(n) { - let mut reader = BitReader::new(packed.clone()); - for slot in chunk.iter_mut() { - let delta = reader.get_value::(bit_width).unwrap(); - let signed = delta.wrapping_add(base) as i64; - *slot = ((signed as f64) * scale.0) * scale.1; - } - } - sink += out[total - 1]; - } - let scalar = t0.elapsed(); - - // 2. batched into a stack scratch tile - let t1 = Instant::now(); - for _ in 0..iters { - let mut scratch = [0u64; 1024]; - for chunk in out.chunks_mut(n) { - let len = chunk.len(); - let mut reader = BitReader::new(packed.clone()); - reader.get_batch::(&mut scratch[..len], bit_width); - for (slot, &delta) in chunk.iter_mut().zip(scratch[..len].iter()) { - let signed = delta.wrapping_add(base) as i64; - *slot = ((signed as f64) * scale.0) * scale.1; - } - } - sink += out[total - 1]; - } - let scratch_t = t1.elapsed(); - - // 3. in-place, no scratch - let t2 = Instant::now(); - for _ in 0..iters { - for chunk in out.chunks_mut(n) { - let mut reader = BitReader::new(packed.clone()); - // SAFETY: f64 and u64 share size and alignment, so the output - // chunk can hold the unpacked integers and then be overwritten - // with the decoded float bit patterns. - let int_out: &mut [u64] = unsafe { - std::slice::from_raw_parts_mut(chunk.as_mut_ptr() as *mut u64, chunk.len()) - }; - reader.get_batch::(int_out, bit_width); - for slot in int_out.iter_mut() { - let signed = slot.wrapping_add(base) as i64; - *slot = (((signed as f64) * scale.0) * scale.1).to_bits(); - } - } - sink += out[total - 1]; - } - let inplace = t2.elapsed(); - - let total_vals = (iters * total) as f64; - let ns = |d: std::time::Duration| d.as_nanos() as f64 / total_vals; - println!( - "f64 decode ({} MB out) scalar: {:.3} scratch: {:.3} in-place: {:.3} ns/val | \ - scratch {:.2}x scalar, in-place {:.2}x scalar, in-place {:.2}x scratch (sink={sink})", - total * 8 / (1 << 20), - ns(scalar), - ns(scratch_t), - ns(inplace), - scalar.as_nanos() as f64 / scratch_t.as_nanos() as f64, - scalar.as_nanos() as f64 / inplace.as_nanos() as f64, - scratch_t.as_nanos() as f64 / inplace.as_nanos() as f64, - ); - } - - /// End-to-end decoder throughput on a real multi-vector page, across a few - /// `vector_size` values, exercising the production path: `set_data` (scratch - /// sizing), per-vector `load_current_vector`, and the tiled `decode_range` - /// over the reused buffer. - /// - /// `cargo test -p parquet --release decode_end_to_end -- --ignored --nocapture` - #[test] - #[ignore = "microbenchmark"] - fn bench_decode_end_to_end() { - use crate::util::bit_util::BitWriter; - use std::time::Instant; - - fn pack(count: usize, bit_width: usize) -> Vec { - let mut w = BitWriter::new(count * 8); - for i in 0..count as u64 { - let delta = i.wrapping_mul(2_654_435_761) & ((1u64 << bit_width) - 1); - w.put_value(delta, bit_width); - } - w.flush(); - w.consume() - } - - let total = 1usize << 20; // ~1M values - let bit_width = 12usize; - let iters = 50usize; - - for &log_vs in &[8u8, 10, 13] { - let vector_size = 1usize << log_vs; - let full_vectors = total / vector_size; - let packed = pack(vector_size, bit_width); - let vectors: Vec> = (0..full_vectors) - .map(|_| { - make_vector(VectorSpec { - exponent: 4, - factor: 2, - frame_of_reference: 1_000_000u64, - bit_width: bit_width as u8, - packed_values: &packed, - exception_positions: &[], - exception_values: &[], - }) - }) - .collect(); - let n = full_vectors * vector_size; - let page = Bytes::from(make_page_from_vectors(log_vs, n as i32, &vectors)); - - let mut out = vec![0.0f64; n]; - let mut decoder = AlpDecoder::::new(); - let mut sink = 0.0f64; - let t = Instant::now(); - for _ in 0..iters { - decoder.set_data(page.clone(), n).unwrap(); - decoder.get(&mut out).unwrap(); - sink += out[n - 1]; - } - let el = t.elapsed(); - println!( - "vector_size {:>5} ({:>4} vectors): {:.3} ns/val (sink={sink})", - vector_size, - full_vectors, - el.as_nanos() as f64 / (iters * n) as f64, - ); - } - } - #[test] fn test_alp_decoder_get_full_read() { let vector0 = make_vector(VectorSpec { diff --git a/parquet/src/encodings/encoding/alp_encoder.rs b/parquet/src/encodings/encoding/alp_encoder.rs index 47101b64e2a9..d3e806adb624 100644 --- a/parquet/src/encodings/encoding/alp_encoder.rs +++ b/parquet/src/encodings/encoding/alp_encoder.rs @@ -1030,127 +1030,3 @@ mod conformance { assert!(pages_checked > 0, "no ALP data page found in the test file"); } } - -/// Throughput microbench (not a correctness test). Reports cycles/value for the -/// three hot paths, using `rdtsc`. The absolute numbers are TSC reference cycles -/// on this machine, not portable core cycles - but the *ratios* (streaming vs -/// buffered, encode vs decode) are robust, since the TSC skew is identical -/// across measurements. -/// -/// Run with: -/// ```text -/// cargo test -p parquet --release --lib alp_encoder::throughput -- --ignored --nocapture -/// ``` -#[cfg(all(test, target_arch = "x86_64"))] -mod throughput { - use super::*; - use crate::data_type::{DoubleType, FloatType}; - use crate::encodings::decoding::Decoder; - use crate::encodings::decoding::alp_decoder::AlpDecoder; - use core::arch::x86_64::{_mm_lfence, _rdtsc}; - use std::hint::black_box; - - const N: usize = 1 << 17; // ~one rowgroup - const PUT_CHUNK: usize = 2048; // a plausible writer sub-batch - const ITERS: u64 = 500; - - /// Minimum single-run TSC delta over `ITERS`, after warmup. Min rejects - /// scheduling/interrupt noise better than mean. - fn min_cycles(mut f: F) -> u64 { - for _ in 0..5 { - f(); - } - let mut best = u64::MAX; - for _ in 0..ITERS { - unsafe { - _mm_lfence(); - let t0 = _rdtsc(); - _mm_lfence(); - f(); - _mm_lfence(); - let t1 = _rdtsc(); - best = best.min(t1.wrapping_sub(t0)); - } - } - best - } - - fn report(label: &str, cycles: u64, n: usize) { - let cpv = cycles as f64 / n as f64; - println!("{label}: {cpv:6.2} cyc/val {:5.3} val/cyc", 1.0 / cpv); - } - - #[test] - #[ignore = "throughput bench; run with --release --ignored --nocapture"] - fn throughput_f64() { - // 2-decimal data, ~one rowgroup, few exceptions - the ALP common case. - let values: Vec = (0..N).map(|i| ((i % 100_000) as f64) * 0.01 + 1.0).collect(); - let preset = build_preset(&values); - let mut scratch = Scratch::::new(); - - let enc_buffered = min_cycles(|| { - let page = encode_page(black_box(&values), &preset, &mut scratch).unwrap(); - black_box(page); - }); - - let mut sp = StreamingPage::::new(); - let enc_streaming = min_cycles(|| { - for chunk in black_box(&values).chunks(PUT_CHUNK) { - sp.put(chunk, &preset, &mut scratch).unwrap(); - } - black_box(sp.finish(&preset, &mut scratch).unwrap()); - }); - - let page: Bytes = encode_page(&values, &preset, &mut scratch).unwrap().into(); - let mut out = vec![0.0f64; values.len()]; - let dec = min_cycles(|| { - let mut decoder = AlpDecoder::::new(); - decoder - .set_data(black_box(page.clone()), values.len()) - .unwrap(); - decoder.get(&mut out).unwrap(); - black_box(&out); - }); - - println!(); - report("f64 encode buffered ", enc_buffered, N); - report("f64 encode streaming", enc_streaming, N); - report("f64 decode ", dec, N); - } - - #[test] - #[ignore = "throughput bench; run with --release --ignored --nocapture"] - fn throughput_f32() { - let values: Vec = (0..N).map(|i| ((i % 100_000) as f32) * 0.01 + 1.0).collect(); - let preset = build_preset(&values); - let mut scratch = Scratch::::new(); - - let enc_buffered = min_cycles(|| { - black_box(encode_page(black_box(&values), &preset, &mut scratch).unwrap()); - }); - - let mut sp = StreamingPage::::new(); - let enc_streaming = min_cycles(|| { - for chunk in black_box(&values).chunks(PUT_CHUNK) { - sp.put(chunk, &preset, &mut scratch).unwrap(); - } - black_box(sp.finish(&preset, &mut scratch).unwrap()); - }); - - let page: Bytes = encode_page(&values, &preset, &mut scratch).unwrap().into(); - let mut out = vec![0.0f32; values.len()]; - let dec = min_cycles(|| { - let mut decoder = AlpDecoder::::new(); - decoder - .set_data(black_box(page.clone()), values.len()) - .unwrap(); - decoder.get(&mut out).unwrap(); - black_box(&out); - }); - - println!(); - report("f32 encode buffered ", enc_buffered, N); - report("f32 encode streaming", enc_streaming, N); - report("f32 decode ", dec, N); - } -} From e23b476fd799bd006b339340ffdc0cfe1c194ecf Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Fri, 31 Jul 2026 16:46:05 -0400 Subject: [PATCH 49/50] fmt --- parquet/src/encodings/alp.rs | 8 +++-- parquet/src/encodings/decoding/alp_decoder.rs | 30 ++++++++++++++++--- parquet/src/encodings/encoding/alp_encoder.rs | 7 +++-- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/parquet/src/encodings/alp.rs b/parquet/src/encodings/alp.rs index 414ac081c703..1306ecac91ef 100644 --- a/parquet/src/encodings/alp.rs +++ b/parquet/src/encodings/alp.rs @@ -255,7 +255,9 @@ impl ForInfo { /// - FOR stores non-negative deltas optimized for bitpacking. /// - Unsigned arithmetic avoids signed-overflow edge cases in FOR stage. /// - Signed interpretation is applied later during decimal reconstruction. -pub(crate) trait AlpExact: Copy + std::fmt::Debug + PartialEq + FromBitpacked + Default { +pub(crate) trait AlpExact: + Copy + std::fmt::Debug + PartialEq + FromBitpacked + Default +{ const WIDTH: usize; type Signed: Copy + Ord + std::fmt::Debug + Send; fn from_le_slice(slice: &[u8]) -> Self; @@ -407,7 +409,9 @@ pub(crate) const ALP_NEG_POW10_F64: [f64; 19] = [ 0.000000000000000001, ]; -pub(crate) trait AlpFloat: Copy + Default + PartialEq + std::ops::Mul { +pub(crate) trait AlpFloat: + Copy + Default + PartialEq + std::ops::Mul +{ type Exact: AlpExact + FromBytes; type Scale: Copy + Send; diff --git a/parquet/src/encodings/decoding/alp_decoder.rs b/parquet/src/encodings/decoding/alp_decoder.rs index 2e9cd07eebe7..2955adaa7340 100644 --- a/parquet/src/encodings/decoding/alp_decoder.rs +++ b/parquet/src/encodings/decoding/alp_decoder.rs @@ -1306,9 +1306,27 @@ mod tests { exc_val: f64, } let specs = [ - Spec { for_ref: 1000, bit_width: 4, len: 1024, exc_pos: 100, exc_val: f64::NAN }, - Spec { for_ref: 5000, bit_width: 8, len: 1024, exc_pos: 300, exc_val: f64::INFINITY }, - Spec { for_ref: 100, bit_width: 2, len: 552, exc_pos: 200, exc_val: -0.0 }, + Spec { + for_ref: 1000, + bit_width: 4, + len: 1024, + exc_pos: 100, + exc_val: f64::NAN, + }, + Spec { + for_ref: 5000, + bit_width: 8, + len: 1024, + exc_pos: 300, + exc_val: f64::INFINITY, + }, + Spec { + for_ref: 100, + bit_width: 2, + len: 552, + exc_pos: 200, + exc_val: -0.0, + }, ]; let mut vectors = Vec::new(); @@ -1345,7 +1363,11 @@ mod tests { }; // Sanity check: a straight sequential decode reproduces `expected`. - assert_bits(&decode_page::(page.clone(), n), &expected, "sequential"); + assert_bits( + &decode_page::(page.clone(), n), + &expected, + "sequential", + ); // Skip from the start: boundary-aligned (1024, 2048), mid-vector (1, 1025, // 2222), just before the end (2599), and exactly to the end (2600). diff --git a/parquet/src/encodings/encoding/alp_encoder.rs b/parquet/src/encodings/encoding/alp_encoder.rs index d3e806adb624..2c785f6f24e2 100644 --- a/parquet/src/encodings/encoding/alp_encoder.rs +++ b/parquet/src/encodings/encoding/alp_encoder.rs @@ -141,7 +141,8 @@ fn estimate_size_bits( return Some(num_values * exception_bits::()); }; - let range = F::Exact::reinterpret_from_signed(max).wrapping_sub(F::Exact::reinterpret_from_signed(min)); + let range = + F::Exact::reinterpret_from_signed(max).wrapping_sub(F::Exact::reinterpret_from_signed(min)); let bit_width = u64::from(num_required_bits(range.to_u64())); Some(num_values * bit_width + num_exceptions * exception_bits::()) @@ -386,8 +387,8 @@ fn encode_vector( if bit_width > 0 { let mut writer = BitWriter::new_from_buf(std::mem::take(out)); for &encoded_value in encoded.iter() { - let delta = F::Exact::reinterpret_from_signed(encoded_value) - .wrapping_sub(frame_of_reference); + let delta = + F::Exact::reinterpret_from_signed(encoded_value).wrapping_sub(frame_of_reference); writer.put_value(delta.to_u64(), bit_width as usize); } // Pads to a byte boundary, giving exactly the ceil(n * bit_width / 8) From 2c25e55c1df1bbff54b36eb8b7692f03b2da99ed Mon Sep 17 00:00:00 2001 From: sdf-jkl Date: Fri, 31 Jul 2026 17:24:35 -0400 Subject: [PATCH 50/50] comments clean up pass --- parquet/src/encodings/alp.rs | 37 +++++++------------ parquet/src/encodings/encoding/alp_encoder.rs | 9 ++--- parquet/tests/arrow_reader/alp.rs | 6 +-- 3 files changed, 18 insertions(+), 34 deletions(-) diff --git a/parquet/src/encodings/alp.rs b/parquet/src/encodings/alp.rs index 1306ecac91ef..c8ef5752f986 100644 --- a/parquet/src/encodings/alp.rs +++ b/parquet/src/encodings/alp.rs @@ -17,7 +17,7 @@ //! ALP (Adaptive Lossless floating-Point) Encoding //! -//! Based on the draft Parquet spec: +//! Spec: //! //! # Page layout //! @@ -45,8 +45,7 @@ pub(crate) const ALP_COMPRESSION_MODE: u8 = 0; pub(crate) const ALP_INTEGER_ENCODING_FOR_BIT_PACK: u8 = 0; pub(crate) const ALP_MIN_LOG_VECTOR_SIZE: u8 = 3; pub(crate) const ALP_MAX_LOG_VECTOR_SIZE: u8 = 15; -/// Spec-recommended default `log_vector_size`: 1024-value vectors, the canonical -/// ALP/FastLanes vector size. +/// Spec-recommended default `log_vector_size`: 1024-value vectors. pub(crate) const ALP_DEFAULT_LOG_VECTOR_SIZE: u8 = 10; pub(crate) const ALP_MAX_EXPONENT_F32: u8 = 10; pub(crate) const ALP_MAX_EXPONENT_F64: u8 = 18; @@ -174,9 +173,7 @@ impl AlpHeader { } } -/// Per-vector ALP metadata (4 bytes), equivalent to C++ `AlpEncodedVectorInfo`. -/// -/// ##### AlpInfo (4 bytes, both types) +/// Per-vector ALP metadata (4 bytes). /// /// ```text /// Byte: 0 1 2 3 @@ -203,18 +200,13 @@ impl AlpInfo { } } -/// Per-vector FOR metadata for exact integer type (`u32` for `f32`, `u64` for `f64`). -/// -/// Frame of reference (FOR) encoding -/// -/// ###### ForInfo for FLOAT (5 bytes) / DOUBLE (9 bytes) +/// Per-vector FOR (frame of reference) metadata: 5 bytes for `f32`, 9 for `f64`. /// /// ```text -/// Byte: 0 1 2 3 4 -/// +----+----+----+----+-----------+ +/// +--------------------+-----------+ /// | frame_of_reference | bit_width | -/// | (int32 LE) | (uint8) | -/// +----+----+----+----+-----------+ +/// | (Exact::WIDTH, LE) | (uint8) | +/// +--------------------+-----------+ /// ``` #[derive(Debug, Clone, Copy)] pub(crate) struct ForInfo { @@ -245,16 +237,13 @@ impl ForInfo { } } -/// Exact integer type used by FOR reconstruction. -/// -/// This mirrors C++: -/// - `float` -> `uint32_t` -/// - `double` -> `uint64_t` +/// Exact integer type used by FOR reconstruction: `u32` for `f32`, `u64` for +/// `f64`. /// -/// Why unsigned (not `i32`/`i64`)? -/// - FOR stores non-negative deltas optimized for bitpacking. -/// - Unsigned arithmetic avoids signed-overflow edge cases in FOR stage. -/// - Signed interpretation is applied later during decimal reconstruction. +/// Why unsigned (not `i32`/`i64`)? The spec computes and stores deltas in +/// unsigned wrapping arithmetic: this avoids signed overflow when a vector's +/// range exceeds the signed maximum, and unpacking needs no sign extension. +/// Signed interpretation is applied later during decimal reconstruction. pub(crate) trait AlpExact: Copy + std::fmt::Debug + PartialEq + FromBitpacked + Default { diff --git a/parquet/src/encodings/encoding/alp_encoder.rs b/parquet/src/encodings/encoding/alp_encoder.rs index 2c785f6f24e2..7e12a5883bf7 100644 --- a/parquet/src/encodings/encoding/alp_encoder.rs +++ b/parquet/src/encodings/encoding/alp_encoder.rs @@ -17,7 +17,7 @@ //! ALP (Adaptive Lossless floating-Point) encoder. //! -//! Based on the draft Parquet spec: +//! Spec: //! //! Values are buffered until the page is flushed, then encoded a vector at a //! time into the page layout that [`AlpDecoder`] reads back: @@ -83,8 +83,7 @@ struct Combination { /// /// The last two are tie-breaks taken from the ALP paper (Afroozeh et al., SIGMOD /// 2023, section 3.1.2), which prefers higher exponents and factors without -/// justifying it further. They are kept so the candidate set matches the -/// reference implementations. +/// justifying it further. fn rank(c: &Combination) -> (u64, Reverse, u8, u8) { ( c.num_appearances, @@ -382,8 +381,7 @@ fn encode_vector( // PackedValues: FOR deltas, LSB-first, as the spec requires. A zero bit // width means every value equals the frame of reference, so nothing is - // stored. This is the stage a FastLanes-ordered `integer_encoding` would - // replace. + // stored. if bit_width > 0 { let mut writer = BitWriter::new_from_buf(std::mem::take(out)); for &encoded_value in encoded.iter() { @@ -905,7 +903,6 @@ mod tests { /// Streaming pages (every page after the first) must be byte-for-byte /// identical to encoding the same values in one pass with the same preset. - /// This is the guarantee that makes the streaming path a pure refactor. #[test] fn test_streaming_matches_buffered() { let page1: Vec = (0..3000).map(|i| (i as f64) * 0.01 + 1.23).collect(); diff --git a/parquet/tests/arrow_reader/alp.rs b/parquet/tests/arrow_reader/alp.rs index 88cb6e983ee2..734aae6bc7b9 100644 --- a/parquet/tests/arrow_reader/alp.rs +++ b/parquet/tests/arrow_reader/alp.rs @@ -76,10 +76,8 @@ fn test_read_f32_alp() { /// /// This checks losslessness only, not compression: these are `f32` values whose /// encoded integers need 31 bits and which except 6.4% of the time, so ALP is -/// larger than PLAIN here. The arrow-cpp implementation produces a byte-identical -/// page from the same values (see `test_matches_cpp_reference_page`), so that is -/// a property of the data, not of the encoder. ALP's win on arade in the paper is -/// on the `f64` version of the dataset. +/// larger than PLAIN here, a property of the data rather than the encoder. +/// ALP's win on arade in the paper is on the `f64` version of the dataset. #[test] fn test_write_f32_alp_roundtrip() { let data_dir = PathBuf::from(parquet_test_data());