|
| 1 | +// SPDX-License-Identifier: Apache-2.0 |
| 2 | +// SPDX-FileCopyrightText: Copyright the Vortex contributors |
| 3 | + |
| 4 | +use std::marker::PhantomData; |
| 5 | + |
| 6 | +use num_traits::AsPrimitive; |
| 7 | +use vortex::array::ExecutionCtx; |
| 8 | +use vortex::array::IntoArray; |
| 9 | +use vortex::array::arrays::PrimitiveArray; |
| 10 | +use vortex::array::match_each_unsigned_integer_ptype; |
| 11 | +use vortex::array::validity::Validity; |
| 12 | +use vortex::dtype::IntegerPType; |
| 13 | +use vortex::encodings::fastlanes::FL_CHUNK_SIZE; |
| 14 | +use vortex::encodings::fastlanes::RLEArray; |
| 15 | +use vortex::encodings::fastlanes::RLEArrayExt; |
| 16 | +use vortex::error::VortexResult; |
| 17 | + |
| 18 | +use crate::duckdb::ReusableDict; |
| 19 | +use crate::duckdb::SelectionVector; |
| 20 | +use crate::duckdb::VectorRef; |
| 21 | +use crate::exporter::ColumnExporter; |
| 22 | +use crate::exporter::all_invalid; |
| 23 | +use crate::exporter::cache::ConversionCache; |
| 24 | +use crate::exporter::cached_values_dict; |
| 25 | +use crate::exporter::canonical; |
| 26 | + |
| 27 | +struct RLEExporter<I: IntegerPType, O: IntegerPType> { |
| 28 | + values: ReusableDict, |
| 29 | + indices: PrimitiveArray, |
| 30 | + values_idx_offsets: PrimitiveArray, |
| 31 | + /// Offset relative to the first chunk |
| 32 | + offset: usize, |
| 33 | + indices_type: PhantomData<I>, |
| 34 | + values_idx_offsets_type: PhantomData<O>, |
| 35 | +} |
| 36 | + |
| 37 | +pub(crate) fn new_exporter_with_flatten( |
| 38 | + array: RLEArray, |
| 39 | + cache: &ConversionCache, |
| 40 | + ctx: &mut ExecutionCtx, |
| 41 | + flatten: bool, |
| 42 | +) -> VortexResult<Box<dyn ColumnExporter>> { |
| 43 | + if flatten || array.is_empty() { |
| 44 | + return canonical::new_exporter(array.into_array(), cache, ctx); |
| 45 | + } |
| 46 | + // DuckDB dictionary can't carry validity on codes. |
| 47 | + // Don't execute the validity mask, if there's a chance of NULL, |
| 48 | + // canonicalize |
| 49 | + match array.indices().validity()? { |
| 50 | + Validity::AllInvalid => return Ok(all_invalid::new_exporter()), |
| 51 | + Validity::Array(_) => return canonical::new_exporter(array.into_array(), cache, ctx), |
| 52 | + _ => {} |
| 53 | + } |
| 54 | + |
| 55 | + let indices = array.indices().clone().execute::<PrimitiveArray>(ctx)?; |
| 56 | + let values = array.values().clone(); |
| 57 | + let values_idx_offsets = array |
| 58 | + .values_idx_offsets() |
| 59 | + .clone() |
| 60 | + .execute::<PrimitiveArray>(ctx)?; |
| 61 | + |
| 62 | + let values = cached_values_dict(values, cache, ctx)?; |
| 63 | + match_each_unsigned_integer_ptype!(indices.ptype(), |I| { |
| 64 | + match_each_unsigned_integer_ptype!(values_idx_offsets.ptype(), |O| { |
| 65 | + Ok(Box::new(RLEExporter { |
| 66 | + values, |
| 67 | + indices, |
| 68 | + values_idx_offsets, |
| 69 | + offset: array.offset(), |
| 70 | + indices_type: PhantomData::<I>, |
| 71 | + values_idx_offsets_type: PhantomData::<O>, |
| 72 | + })) |
| 73 | + }) |
| 74 | + }) |
| 75 | +} |
| 76 | + |
| 77 | +impl<I, O> ColumnExporter for RLEExporter<I, O> |
| 78 | +where |
| 79 | + I: IntegerPType + AsPrimitive<u32>, |
| 80 | + O: IntegerPType + AsPrimitive<u32>, |
| 81 | +{ |
| 82 | + fn export( |
| 83 | + &self, |
| 84 | + offset: usize, |
| 85 | + len: usize, |
| 86 | + vector: &mut VectorRef, |
| 87 | + _ctx: &mut ExecutionCtx, |
| 88 | + ) -> VortexResult<()> { |
| 89 | + let mut selection_vec = SelectionVector::with_capacity(len); |
| 90 | + let mut selection = unsafe { selection_vec.as_slice_mut(len) }; |
| 91 | + |
| 92 | + let indices = self.indices.as_slice::<I>(); |
| 93 | + let values_idx_offsets = self.values_idx_offsets.as_slice::<O>(); |
| 94 | + |
| 95 | + let mut pos = self.offset + offset; |
| 96 | + let end = pos + len; |
| 97 | + |
| 98 | + let first_idx_offset = values_idx_offsets[0]; |
| 99 | + while pos < end { |
| 100 | + let chunk_idx = pos / FL_CHUNK_SIZE; |
| 101 | + let base: u32 = (values_idx_offsets[chunk_idx] - first_idx_offset).as_(); |
| 102 | + let take = ((chunk_idx + 1) * FL_CHUNK_SIZE).min(end) - pos; |
| 103 | + |
| 104 | + for (dst, idx) in selection[..take].iter_mut().zip(&indices[pos..pos + take]) { |
| 105 | + let idx: u32 = idx.as_(); |
| 106 | + *dst = base + idx; |
| 107 | + } |
| 108 | + |
| 109 | + selection = &mut selection[take..]; |
| 110 | + pos += take; |
| 111 | + } |
| 112 | + |
| 113 | + vector.reuse_dictionary(&self.values, &selection_vec); |
| 114 | + Ok(()) |
| 115 | + } |
| 116 | +} |
| 117 | + |
| 118 | +#[cfg(test)] |
| 119 | +mod tests { |
| 120 | + use vortex::array::ArrayRef; |
| 121 | + use vortex::array::IntoArray; |
| 122 | + use vortex::array::VortexSessionExecute; |
| 123 | + use vortex::array::arrays::PrimitiveArray; |
| 124 | + use vortex::encodings::fastlanes::RLEArray; |
| 125 | + use vortex::encodings::fastlanes::RLEData; |
| 126 | + use vortex::error::VortexResult; |
| 127 | + |
| 128 | + use crate::SESSION; |
| 129 | + use crate::cpp::duckdb_type::DUCKDB_TYPE_INTEGER; |
| 130 | + use crate::duckdb::DataChunk; |
| 131 | + use crate::duckdb::LogicalType; |
| 132 | + use crate::exporter::ConversionCache; |
| 133 | + use crate::exporter::new_array_exporter; |
| 134 | + |
| 135 | + fn encode_rle(values: Vec<i32>) -> VortexResult<RLEArray> { |
| 136 | + let mut ctx = SESSION.create_execution_ctx(); |
| 137 | + let primitive = PrimitiveArray::from_iter(values); |
| 138 | + RLEData::encode(primitive.as_view(), &mut ctx) |
| 139 | + } |
| 140 | + |
| 141 | + fn export_flat(array: ArrayRef, len: usize) -> VortexResult<Vec<i32>> { |
| 142 | + let mut ctx = SESSION.create_execution_ctx(); |
| 143 | + let mut chunk = DataChunk::new([LogicalType::new(DUCKDB_TYPE_INTEGER)]); |
| 144 | + new_array_exporter(array, &ConversionCache::default(), &mut ctx)?.export( |
| 145 | + 0, |
| 146 | + len, |
| 147 | + chunk.get_vector_mut(0), |
| 148 | + &mut ctx, |
| 149 | + )?; |
| 150 | + chunk.set_len(len); |
| 151 | + let vector = chunk.get_vector(0); |
| 152 | + vector.flatten(len as u64); |
| 153 | + Ok(vector.as_slice_with_len::<i32>(len).to_vec()) |
| 154 | + } |
| 155 | + |
| 156 | + #[test] |
| 157 | + fn test_roundtrip_two_chunks() -> VortexResult<()> { |
| 158 | + let expected: Vec<i32> = (0i32..2048).map(|i| i / 100).collect(); |
| 159 | + let rle = encode_rle(expected.clone())?; |
| 160 | + let exported = export_flat(rle.into_array(), 2048)?; |
| 161 | + assert_eq!(exported, expected); |
| 162 | + Ok(()) |
| 163 | + } |
| 164 | + |
| 165 | + #[test] |
| 166 | + fn test_roundtrip_boundary() -> VortexResult<()> { |
| 167 | + let source: Vec<i32> = (0i32..2048).map(|i| i / 100).collect(); |
| 168 | + let rle = encode_rle(source.clone())?; |
| 169 | + let sliced = rle.into_array().slice(500..1700)?; |
| 170 | + let exported = export_flat(sliced, 1200)?; |
| 171 | + assert_eq!(exported, source[500..1700]); |
| 172 | + Ok(()) |
| 173 | + } |
| 174 | + |
| 175 | + #[test] |
| 176 | + fn test_roundtrip_slice() -> VortexResult<()> { |
| 177 | + let source: Vec<i32> = (0i32..3072).map(|i| i / 100).collect(); |
| 178 | + let rle = encode_rle(source.clone())?; |
| 179 | + let sliced = rle.into_array().slice(1200..2000)?; |
| 180 | + let exported = export_flat(sliced, 800)?; |
| 181 | + assert_eq!(exported, source[1200..2000]); |
| 182 | + Ok(()) |
| 183 | + } |
| 184 | + |
| 185 | + fn chunk_string(array: ArrayRef, offset: usize, len: usize) -> VortexResult<String> { |
| 186 | + let mut ctx = SESSION.create_execution_ctx(); |
| 187 | + let mut chunk = DataChunk::new([LogicalType::new(DUCKDB_TYPE_INTEGER)]); |
| 188 | + new_array_exporter(array, &ConversionCache::default(), &mut ctx)?.export( |
| 189 | + offset, |
| 190 | + len, |
| 191 | + chunk.get_vector_mut(0), |
| 192 | + &mut ctx, |
| 193 | + )?; |
| 194 | + chunk.set_len(len); |
| 195 | + String::try_from(&*chunk) |
| 196 | + } |
| 197 | + |
| 198 | + fn two_chunk_rle() -> VortexResult<RLEArray> { |
| 199 | + let mut ctx = SESSION.create_execution_ctx(); |
| 200 | + let source: Vec<i32> = std::iter::repeat_n(10i32, 1024) |
| 201 | + .chain(std::iter::repeat_n(20, 1024)) |
| 202 | + .collect(); |
| 203 | + RLEData::encode(PrimitiveArray::from_iter(source).as_view(), &mut ctx) |
| 204 | + } |
| 205 | + |
| 206 | + #[test] |
| 207 | + fn test_one_chunk() -> VortexResult<()> { |
| 208 | + let rle = two_chunk_rle()?; |
| 209 | + let chunk_str = chunk_string(rle.into_array(), 0, 5)?; |
| 210 | + assert_eq!( |
| 211 | + chunk_str, |
| 212 | + r#"Chunk - [1 Columns] |
| 213 | +- DICTIONARY INTEGER: 5 = [ 10, 10, 10, 10, 10] |
| 214 | +"# |
| 215 | + ); |
| 216 | + Ok(()) |
| 217 | + } |
| 218 | + |
| 219 | + #[test] |
| 220 | + fn test_one_chunk_nulls() -> VortexResult<()> { |
| 221 | + let mut ctx = SESSION.create_execution_ctx(); |
| 222 | + let source = vec![Some(0u32), Some(1), None, Some(3), None]; |
| 223 | + let rle = RLEData::encode(PrimitiveArray::from_option_iter(source).as_view(), &mut ctx)?; |
| 224 | + let chunk_str = chunk_string(rle.into_array(), 0, 5)?; |
| 225 | + assert_eq!( |
| 226 | + chunk_str, |
| 227 | + r#"Chunk - [1 Columns] |
| 228 | +- FLAT INTEGER: 5 = [ 0, 1, NULL, 3, NULL] |
| 229 | +"# |
| 230 | + ); |
| 231 | + Ok(()) |
| 232 | + } |
| 233 | + |
| 234 | + #[test] |
| 235 | + fn test_chunk_boundary() -> VortexResult<()> { |
| 236 | + let rle = two_chunk_rle()?; |
| 237 | + let chunk_str = chunk_string(rle.into_array(), 1020, 10)?; |
| 238 | + assert_eq!( |
| 239 | + chunk_str, |
| 240 | + r#"Chunk - [1 Columns] |
| 241 | +- DICTIONARY INTEGER: 10 = [ 10, 10, 10, 10, 20, 20, 20, 20, 20, 20] |
| 242 | +"# |
| 243 | + ); |
| 244 | + Ok(()) |
| 245 | + } |
| 246 | + |
| 247 | + #[test] |
| 248 | + fn test_chunk_slice() -> VortexResult<()> { |
| 249 | + let rle = two_chunk_rle()?; |
| 250 | + let sliced = rle.into_array().slice(1500..1510)?; |
| 251 | + let chunk_str = chunk_string(sliced, 0, 10)?; |
| 252 | + assert_eq!( |
| 253 | + chunk_str, |
| 254 | + r#"Chunk - [1 Columns] |
| 255 | +- FLAT INTEGER: 10 = [ 20, 20, 20, 20, 20, 20, 20, 20, 20, 20] |
| 256 | +"# |
| 257 | + ); |
| 258 | + Ok(()) |
| 259 | + } |
| 260 | + |
| 261 | + #[test] |
| 262 | + fn test_roundtrip_with_nulls() -> VortexResult<()> { |
| 263 | + let source: Vec<Option<i32>> = (0i32..1024) |
| 264 | + .map(|i| if i % 7 == 0 { None } else { Some(i / 50) }) |
| 265 | + .collect(); |
| 266 | + let mut ctx = SESSION.create_execution_ctx(); |
| 267 | + let primitive = PrimitiveArray::from_option_iter(source.clone()); |
| 268 | + let rle = RLEData::encode(primitive.as_view(), &mut ctx)?; |
| 269 | + |
| 270 | + let mut chunk = DataChunk::new([LogicalType::new(DUCKDB_TYPE_INTEGER)]); |
| 271 | + new_array_exporter(rle.into_array(), &ConversionCache::default(), &mut ctx)?.export( |
| 272 | + 0, |
| 273 | + 1024, |
| 274 | + chunk.get_vector_mut(0), |
| 275 | + &mut ctx, |
| 276 | + )?; |
| 277 | + chunk.set_len(1024); |
| 278 | + |
| 279 | + let vector = chunk.get_vector(0); |
| 280 | + vector.flatten(1024); |
| 281 | + let slice = vector.as_slice_with_len::<i32>(1024); |
| 282 | + for (i, expected) in source.iter().enumerate() { |
| 283 | + if let Some(v) = expected { |
| 284 | + assert!(!vector.row_is_null(i as u64), "row {i} is null"); |
| 285 | + assert_eq!(slice[i], *v); |
| 286 | + } else { |
| 287 | + assert!(vector.row_is_null(i as u64), "row {i} not null"); |
| 288 | + } |
| 289 | + } |
| 290 | + Ok(()) |
| 291 | + } |
| 292 | +} |
0 commit comments