|
18 | 18 | use std::str::from_utf8_unchecked; |
19 | 19 | use std::sync::Arc; |
20 | 20 |
|
21 | | -use arrow::array::{Array, ArrayRef, StringBuilder}; |
| 21 | +use arrow::array::{Array, ArrayRef, NullBufferBuilder, StringArray, StringBuilder}; |
| 22 | +use arrow::buffer::{Buffer, OffsetBuffer}; |
22 | 23 | use arrow::datatypes::DataType; |
23 | 24 | use arrow::{ |
24 | 25 | array::{as_dictionary_array, as_largestring_array, as_string_array}, |
@@ -166,40 +167,59 @@ where |
166 | 167 | I: Iterator<Item = Option<T>>, |
167 | 168 | T: AsRef<[u8]> + 'a, |
168 | 169 | { |
169 | | - let mut builder = StringBuilder::with_capacity(len, len * 64); |
170 | | - let mut buffer = Vec::with_capacity(64); |
171 | 170 | let lookup = if lowercase { |
172 | 171 | &HEX_LOOKUP_LOWER |
173 | 172 | } else { |
174 | 173 | &HEX_LOOKUP_UPPER |
175 | 174 | }; |
176 | 175 |
|
| 176 | + // Write hex digits directly into one growing value buffer, tracking offsets |
| 177 | + // ourselves. Each input byte becomes exactly two output bytes, so there is |
| 178 | + // no per-row `String`/`StringBuilder` copy — the hex digits are written once |
| 179 | + // into the final buffer. |
| 180 | + let mut values: Vec<u8> = Vec::with_capacity(len * 64); |
| 181 | + let mut offsets: Vec<i32> = Vec::with_capacity(len + 1); |
| 182 | + offsets.push(0); |
| 183 | + let mut nulls = NullBufferBuilder::new(len); |
| 184 | + |
177 | 185 | for v in iter { |
178 | 186 | if let Some(b) = v { |
179 | 187 | let bytes = b.as_ref(); |
180 | | - buffer.clear(); |
181 | 188 | let additional = bytes |
182 | 189 | .len() |
183 | 190 | .checked_mul(2) |
184 | 191 | .ok_or_else(|| exec_datafusion_err!("hex output size overflow"))?; |
185 | | - buffer.try_reserve(additional).map_err(|e| { |
| 192 | + values.try_reserve(additional).map_err(|e| { |
186 | 193 | exec_datafusion_err!( |
187 | 194 | "failed to reserve {additional} bytes for hex output: {e}" |
188 | 195 | ) |
189 | 196 | })?; |
190 | 197 | for &byte in bytes { |
191 | | - buffer.extend_from_slice(&lookup[byte as usize]); |
192 | | - } |
193 | | - // SAFETY: buffer contains only ASCII hex digits, which are valid UTF-8. |
194 | | - unsafe { |
195 | | - builder.append_value(from_utf8_unchecked(&buffer)); |
| 198 | + values.extend_from_slice(&lookup[byte as usize]); |
196 | 199 | } |
| 200 | + nulls.append_non_null(); |
197 | 201 | } else { |
198 | | - builder.append_null(); |
| 202 | + nulls.append_null(); |
199 | 203 | } |
| 204 | + offsets.push( |
| 205 | + i32::try_from(values.len()).map_err(|_| { |
| 206 | + exec_datafusion_err!("hex output exceeds i32 offset range") |
| 207 | + })?, |
| 208 | + ); |
200 | 209 | } |
201 | 210 |
|
202 | | - Ok(Arc::new(builder.finish())) |
| 211 | + // SAFETY: the value buffer contains only ASCII hex digits (valid UTF-8) and |
| 212 | + // the offsets are monotonically increasing and end at `values.len()`, so the |
| 213 | + // array invariants hold. This mirrors the previous `from_utf8_unchecked` |
| 214 | + // path and avoids a redundant UTF-8 validation pass over the whole buffer. |
| 215 | + let array = unsafe { |
| 216 | + StringArray::new_unchecked( |
| 217 | + OffsetBuffer::new(offsets.into()), |
| 218 | + Buffer::from_vec(values), |
| 219 | + nulls.finish(), |
| 220 | + ) |
| 221 | + }; |
| 222 | + Ok(Arc::new(array)) |
203 | 223 | } |
204 | 224 |
|
205 | 225 | /// Generic hex encoding for int64 type |
|
0 commit comments