1use super::{checked_fixed_values, try_vec_with_capacity};
7use crate::{
8 buffer::LanceBuffer,
9 compression::{BlockCompressor, BlockDecompressor, validate_delta_child_encoding},
10 data::{BlockInfo, DataBlock, FixedWidthDataBlock},
11 format::{EncodingUtils, metadata::CompressiveEncoding},
12};
13use lance_core::{Error, Result};
14
15#[derive(Debug)]
16pub struct DeltaEncoder {
18 bits_per_value: u64,
19 base: u64,
20 child: Box<dyn BlockCompressor>,
21}
22
23impl DeltaEncoder {
24 pub fn new(bits_per_value: u64, base: u64, child: Box<dyn BlockCompressor>) -> Self {
26 Self {
27 bits_per_value,
28 base,
29 child,
30 }
31 }
32}
33
34impl BlockCompressor for DeltaEncoder {
35 fn compress(&self, data: DataBlock) -> Result<(Option<LanceBuffer>, CompressiveEncoding)> {
36 let DataBlock::FixedWidth(data) = data else {
37 return Err(Error::invalid_input(
38 "Delta encoding requires a fixed-width data block",
39 ));
40 };
41 if data.bits_per_value != self.bits_per_value {
42 return Err(Error::invalid_input(format!(
43 "Delta codec expects {}-bit values, got {}",
44 self.bits_per_value, data.bits_per_value
45 )));
46 }
47 let deltas = encode_deltas(data, self.base)?;
48 let (payload, child_encoding) = self.child.compress(DataBlock::FixedWidth(deltas))?;
49 validate_delta_child_encoding(&child_encoding, self.bits_per_value)?;
50 Ok((
51 payload,
52 EncodingUtils::delta(self.bits_per_value, self.base, child_encoding),
53 ))
54 }
55}
56
57fn encode_deltas(data: FixedWidthDataBlock, expected_base: u64) -> Result<FixedWidthDataBlock> {
58 if !matches!(data.bits_per_value, 32 | 64) {
59 return Err(Error::invalid_input(format!(
60 "Delta only supports 32 or 64-bit values, got {}",
61 data.bits_per_value
62 )));
63 }
64 if data.num_values < 2 {
65 return Err(Error::invalid_input(format!(
66 "Delta requires at least 2 values, got {}",
67 data.num_values
68 )));
69 }
70
71 let deltas = match data.bits_per_value {
72 32 => {
73 let values = checked_fixed_values::<u32>(&data, "Delta input")?;
74 if u64::from(values[0]) != expected_base {
75 return Err(Error::invalid_input(format!(
76 "Delta base mismatch: codec expects {expected_base}, input starts with {}",
77 values[0]
78 )));
79 }
80 let mut deltas = try_vec_with_capacity::<u32>(data.num_values - 1, "Delta output")?;
81 for (index, pair) in values.windows(2).enumerate() {
82 deltas.push(pair[1].checked_sub(pair[0]).ok_or_else(|| {
83 Error::invalid_input(format!(
84 "Delta input decreases at index {}: {} -> {}",
85 index + 1,
86 pair[0],
87 pair[1]
88 ))
89 })?);
90 }
91 LanceBuffer::reinterpret_vec(deltas)
92 }
93 64 => {
94 let values = checked_fixed_values::<u64>(&data, "Delta input")?;
95 if values[0] != expected_base {
96 return Err(Error::invalid_input(format!(
97 "Delta base mismatch: codec expects {expected_base}, input starts with {}",
98 values[0]
99 )));
100 }
101 let mut deltas = try_vec_with_capacity::<u64>(data.num_values - 1, "Delta output")?;
102 for (index, pair) in values.windows(2).enumerate() {
103 deltas.push(pair[1].checked_sub(pair[0]).ok_or_else(|| {
104 Error::invalid_input(format!(
105 "Delta input decreases at index {}: {} -> {}",
106 index + 1,
107 pair[0],
108 pair[1]
109 ))
110 })?);
111 }
112 LanceBuffer::reinterpret_vec(deltas)
113 }
114 _ => unreachable!("delta width was validated above"),
115 };
116 Ok(FixedWidthDataBlock {
117 bits_per_value: data.bits_per_value,
118 data: deltas,
119 num_values: data.num_values - 1,
120 block_info: BlockInfo::default(),
121 })
122}
123
124#[derive(Debug)]
125pub(crate) struct DeltaDecompressor {
126 bits_per_value: u64,
127 base: u64,
128 child: Box<dyn BlockDecompressor>,
129}
130
131impl DeltaDecompressor {
132 pub(crate) fn new(bits_per_value: u64, base: u64, child: Box<dyn BlockDecompressor>) -> Self {
133 Self {
134 bits_per_value,
135 base,
136 child,
137 }
138 }
139}
140
141impl BlockDecompressor for DeltaDecompressor {
142 fn decompress(&self, data: Option<LanceBuffer>, num_values: u64) -> Result<DataBlock> {
143 validate_delta_header(self.bits_per_value, self.base, num_values)?;
144 let child = self.child.decompress(data, num_values - 1)?;
145 reconstruct_deltas(child, self.bits_per_value, self.base, num_values)
146 }
147
148 fn requires_payload(&self) -> bool {
149 self.child.requires_payload()
150 }
151}
152
153pub(crate) fn validate_delta_header(bits_per_value: u64, base: u64, num_values: u64) -> Result<()> {
154 if !matches!(bits_per_value, 32 | 64) {
155 return Err(Error::invalid_input(format!(
156 "Delta only supports 32 or 64-bit values, got {bits_per_value}"
157 )));
158 }
159 if bits_per_value == 32 && base > u32::MAX as u64 {
160 return Err(Error::invalid_input(format!(
161 "Delta base {base} exceeds u32::MAX"
162 )));
163 }
164 if num_values < 2 {
165 return Err(Error::invalid_input(format!(
166 "Delta requires at least 2 values, got {num_values}"
167 )));
168 }
169 Ok(())
170}
171
172fn reconstruct_deltas(
173 child: DataBlock,
174 bits_per_value: u64,
175 base: u64,
176 num_values: u64,
177) -> Result<DataBlock> {
178 let DataBlock::FixedWidth(child) = child else {
179 return Err(Error::invalid_input(
180 "Delta child decoded to a non fixed-width block",
181 ));
182 };
183 if child.bits_per_value != bits_per_value || child.num_values != num_values - 1 {
184 return Err(Error::invalid_input(format!(
185 "Delta child decoded {} {}-bit values, expected {} {}-bit values",
186 child.num_values,
187 child.bits_per_value,
188 num_values - 1,
189 bits_per_value
190 )));
191 }
192
193 let data = match bits_per_value {
194 32 => {
195 let deltas = checked_fixed_values::<u32>(&child, "Delta child")?;
196 let mut values = try_vec_with_capacity::<u32>(num_values, "Delta output")?;
197 let mut current = u32::try_from(base)
198 .map_err(|_| Error::invalid_input(format!("Delta base {base} exceeds u32::MAX")))?;
199 values.push(current);
200 for (index, delta) in deltas.iter().enumerate() {
201 current = current.checked_add(*delta).ok_or_else(|| {
202 Error::invalid_input(format!(
203 "Delta prefix sum overflows u32 at index {}",
204 index + 1
205 ))
206 })?;
207 values.push(current);
208 }
209 LanceBuffer::reinterpret_vec(values)
210 }
211 64 => {
212 let deltas = checked_fixed_values::<u64>(&child, "Delta child")?;
213 let mut values = try_vec_with_capacity::<u64>(num_values, "Delta output")?;
214 let mut current = base;
215 values.push(current);
216 for (index, delta) in deltas.iter().enumerate() {
217 current = current.checked_add(*delta).ok_or_else(|| {
218 Error::invalid_input(format!(
219 "Delta prefix sum overflows u64 at index {}",
220 index + 1
221 ))
222 })?;
223 values.push(current);
224 }
225 LanceBuffer::reinterpret_vec(values)
226 }
227 _ => unreachable!("delta header was validated before reconstruction"),
228 };
229
230 Ok(DataBlock::FixedWidth(FixedWidthDataBlock {
231 bits_per_value,
232 data,
233 num_values,
234 block_info: BlockInfo::default(),
235 }))
236}
237
238#[cfg(test)]
239mod tests {
240 use crate::compression::{DecompressionStrategy, DefaultDecompressionStrategy};
241 use crate::encodings::physical::{
242 constant::ConstantEncoder, range::RangeEncoder, rle::RleEncoder, value::ValueEncoder,
243 };
244
245 use super::*;
246
247 #[test]
248 fn delta_round_trip_with_payload() {
249 let input = DataBlock::FixedWidth(FixedWidthDataBlock {
250 bits_per_value: 64,
251 data: LanceBuffer::reinterpret_vec(vec![5_u64, 5, 9, 12]),
252 num_values: 4,
253 block_info: BlockInfo::default(),
254 });
255 let encoder = DeltaEncoder::new(64, 5, Box::new(ValueEncoder::default()));
256 let (payload, encoding) = encoder.compress(input).unwrap();
257 let crate::format::metadata::compressive_encoding::Compression::Delta(delta) =
258 encoding.compression.as_ref().unwrap()
259 else {
260 panic!("expected Delta encoding");
261 };
262 let child = crate::compression::DefaultDecompressionStrategy::default()
263 .create_block_decompressor(delta.deltas.as_deref().unwrap())
264 .unwrap();
265 let decoded = DeltaDecompressor::new(64, 5, child)
266 .decompress(payload, 4)
267 .unwrap()
268 .as_fixed_width()
269 .unwrap();
270 assert_eq!(
271 decoded.data.borrow_to_typed_slice::<u64>().as_ref(),
272 &[5, 5, 9, 12]
273 );
274 }
275
276 #[rstest::rstest]
277 #[case::range(vec![0_u32, 1, 3, 6], Box::new(RangeEncoder::new(32, 1, 1)))]
278 #[case::constant(vec![0_u32, 3, 6, 9], Box::new(ConstantEncoder::new(32, 3)))]
279 fn delta_propagates_metadata_only_child(
280 #[case] values: Vec<u32>,
281 #[case] child: Box<dyn BlockCompressor>,
282 ) {
283 let input = DataBlock::FixedWidth(FixedWidthDataBlock {
284 bits_per_value: 32,
285 data: LanceBuffer::reinterpret_vec(values.clone()),
286 num_values: 4,
287 block_info: BlockInfo::default(),
288 });
289 let encoder = DeltaEncoder::new(32, 0, child);
290 let (payload, encoding) = encoder.compress(input).unwrap();
291 assert!(payload.is_none());
292
293 let decoder = crate::compression::DefaultDecompressionStrategy::default()
294 .create_block_decompressor(&encoding)
295 .unwrap();
296 assert!(!decoder.requires_payload());
297 let decoded = decoder
298 .decompress(None, 4)
299 .unwrap()
300 .as_fixed_width()
301 .unwrap();
302 assert_eq!(
303 decoded.data.borrow_to_typed_slice::<u32>().as_ref(),
304 values.as_slice()
305 );
306 }
307
308 #[test]
309 fn delta_rejects_decreasing_input() {
310 let input = FixedWidthDataBlock {
311 bits_per_value: 32,
312 data: LanceBuffer::reinterpret_vec(vec![2_u32, 1]),
313 num_values: 2,
314 block_info: BlockInfo::default(),
315 };
316 let error = encode_deltas(input, 2).unwrap_err();
317 assert!(error.to_string().contains("decreases"));
318 }
319
320 #[test]
321 fn delta_rejects_unsupported_rle_child_before_emitting_encoding() {
322 let input = DataBlock::FixedWidth(FixedWidthDataBlock {
323 bits_per_value: 32,
324 data: LanceBuffer::reinterpret_vec(vec![10_u32, 10, 12, 12, 15]),
325 num_values: 5,
326 block_info: BlockInfo::default(),
327 });
328 let encoder = DeltaEncoder::new(32, 10, Box::new(RleEncoder::new()));
329
330 let error = encoder.compress(input).unwrap_err();
331
332 assert!(matches!(&error, Error::InvalidInput { .. }));
333 assert!(error.to_string().contains("does not support a rle child"));
334 }
335
336 #[test]
337 fn range_and_delta_return_errors_in_miniblock_positions() {
338 let range = EncodingUtils::range(32, 0, 1);
339 let delta = EncodingUtils::delta(32, 0, range.clone());
340 let strategy = DefaultDecompressionStrategy::default();
341
342 for encoding in [&range, &delta] {
343 let Err(error) = strategy.create_miniblock_decompressor(encoding, &strategy) else {
344 panic!("expected block-only encoding to be rejected in a mini-block position");
345 };
346 assert!(matches!(&error, Error::NotSupported { .. }));
347 assert!(
348 error
349 .to_string()
350 .contains("only supported in block positions")
351 );
352 }
353 }
354}