dora_node_api/node/
arrow_utils.rs1pub mod ipc_encode;
4
5use arrow::array::ArrayData;
6use eyre::Context;
7
8const MAX_IPC_BYTES: usize = 256 * 1024 * 1024;
10
11pub(crate) const ARROW_BUFFER_ALIGNMENT: usize = 64;
17pub(crate) const ARROW_BUFFER_ALIGNMENT_EXPONENT: u8 =
18 ARROW_BUFFER_ALIGNMENT.trailing_zeros() as u8;
19const _: () = assert!(ARROW_BUFFER_ALIGNMENT.is_power_of_two());
20
21pub fn encode_arrow_ipc(arrow_array: &ArrayData) -> eyre::Result<Vec<u8>> {
27 use arrow::ipc::writer::StreamWriter;
28 use arrow::record_batch::RecordBatch;
29 use arrow_schema::{Field, Schema};
30 use std::sync::Arc;
31
32 let schema = Schema::new(vec![Field::new(
33 "data",
34 arrow_array.data_type().clone(),
35 true,
36 )]);
37 let schema_ref = Arc::new(schema);
38
39 let array_ref = arrow::array::make_array(arrow_array.clone());
40 let batch = RecordBatch::try_new(schema_ref.clone(), vec![array_ref])
41 .context("failed to create RecordBatch for IPC encoding")?;
42
43 let mut buf = Vec::new();
44 {
45 let mut writer = StreamWriter::try_new(&mut buf, &schema_ref)
46 .context("failed to create Arrow IPC StreamWriter")?;
47 writer
48 .write(&batch)
49 .context("failed to write RecordBatch to IPC stream")?;
50 writer
51 .finish()
52 .context("failed to finish Arrow IPC stream")?;
53 }
54
55 if buf.len() > MAX_IPC_BYTES {
64 eyre::bail!(
65 "Arrow IPC payload too large: {} bytes (max {MAX_IPC_BYTES}); \
66 split the output into smaller batches",
67 buf.len()
68 );
69 }
70 Ok(buf)
71}
72
73pub fn decode_arrow_ipc(ipc_buf: &[u8]) -> eyre::Result<ArrayData> {
78 use arrow::ipc::reader::StreamReader;
79 use std::io::Cursor;
80
81 if ipc_buf.len() > MAX_IPC_BYTES {
82 eyre::bail!(
83 "Arrow IPC payload too large: {} bytes (max {MAX_IPC_BYTES})",
84 ipc_buf.len()
85 );
86 }
87
88 let cursor = Cursor::new(ipc_buf);
89 let mut reader =
90 StreamReader::try_new(cursor, None).context("failed to open Arrow IPC stream")?;
91
92 let batch = reader
93 .next()
94 .ok_or_else(|| eyre::eyre!("Arrow IPC stream contained no record batches"))?
95 .context("failed to read RecordBatch from IPC stream")?;
96
97 if batch.num_columns() != 1 {
98 eyre::bail!(
99 "expected 1 column in IPC record batch, got {}",
100 batch.num_columns()
101 );
102 }
103
104 Ok(batch.column(0).to_data())
105}
106
107pub fn decode_arrow_ipc_zero_copy(
124 mut buffer: arrow::buffer::Buffer,
125) -> eyre::Result<arrow::array::ArrayData> {
126 use arrow::ipc::reader::StreamDecoder;
127
128 if buffer.len() > MAX_IPC_BYTES {
129 eyre::bail!(
130 "Arrow IPC payload too large: {} bytes (max {MAX_IPC_BYTES})",
131 buffer.len()
132 );
133 }
134
135 let mut decoder = StreamDecoder::new();
136 let mut batch = None;
137 while !buffer.is_empty() {
141 let before = buffer.len();
142 if let Some(b) = decoder
143 .decode(&mut buffer)
144 .context("failed to decode Arrow IPC stream")?
145 {
146 batch = Some(b);
147 break;
148 }
149 if buffer.len() == before {
153 eyre::bail!("Arrow IPC decoder made no progress on a partial/corrupt stream");
154 }
155 }
156
157 let batch = batch.ok_or_else(|| eyre::eyre!("Arrow IPC stream contained no record batches"))?;
158
159 if batch.num_columns() != 1 {
160 eyre::bail!(
161 "expected 1 column in IPC record batch, got {}",
162 batch.num_columns()
163 );
164 }
165
166 Ok(batch.column(0).to_data())
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use arrow::array::{Array, StringArray, UInt64Array};
173
174 #[test]
175 fn ipc_roundtrip_primitive() {
176 let array = UInt64Array::from(vec![1, 2, 3, 4, 5]);
177 let data = array.into_data();
178 let encoded = encode_arrow_ipc(&data).unwrap();
179 let decoded = decode_arrow_ipc(&encoded).unwrap();
180 assert_eq!(data, decoded);
181 }
182
183 #[test]
187 fn ipc_encode_rejects_oversized_payload() {
188 use arrow::array::UInt8Array;
189
190 let array = UInt8Array::from(vec![0u8; MAX_IPC_BYTES + 1]);
192 let data = array.into_data();
193 let err =
194 encode_arrow_ipc(&data).expect_err("oversized payload must be rejected by the encoder");
195 assert!(
196 err.to_string().contains("too large"),
197 "unexpected error: {err}"
198 );
199 }
200
201 fn aligned_buffer_from(bytes: &[u8]) -> (arrow::buffer::Buffer, usize, usize) {
206 use aligned_vec::{AVec, ConstAlign};
207 use std::ptr::NonNull;
208
209 let mut aligned: AVec<u8, ConstAlign<128>> = AVec::__from_elem(128, 0, bytes.len());
210 aligned.copy_from_slice(bytes);
211 let base = aligned.as_ptr() as usize;
212 let len = aligned.len();
213 let ptr = NonNull::new(aligned.as_ptr() as *mut u8).unwrap();
214 let buffer = unsafe {
217 arrow::buffer::Buffer::from_custom_allocation(ptr, len, std::sync::Arc::new(aligned))
218 };
219 (buffer, base, len)
220 }
221
222 #[test]
223 fn ipc_zero_copy_roundtrip_primitive() {
224 let array = UInt64Array::from((0..1000u64).collect::<Vec<_>>());
225 let data = array.into_data();
226 let encoded = encode_arrow_ipc(&data).unwrap();
227 let (buffer, _, _) = aligned_buffer_from(&encoded);
228 let decoded = decode_arrow_ipc_zero_copy(buffer).unwrap();
229 assert_eq!(data, decoded);
230 }
231
232 #[test]
237 fn ipc_decode_is_zero_copy_for_aligned_buffer() {
238 use arrow::ipc::reader::StreamDecoder;
239
240 let array = UInt64Array::from((0..100_000u64).collect::<Vec<_>>());
243 let data = array.into_data();
244 let encoded = encode_arrow_ipc(&data).unwrap();
245
246 {
250 let (mut buffer, _, _) = aligned_buffer_from(&encoded);
251 let mut decoder = StreamDecoder::new().with_require_alignment(true);
252 let mut got = None;
253 while !buffer.is_empty() {
254 if let Some(b) = decoder
255 .decode(&mut buffer)
256 .expect("aligned IPC buffer must decode without realignment")
257 {
258 got = Some(b);
259 break;
260 }
261 }
262 assert_eq!(got.unwrap().column(0).to_data(), data);
263 }
264
265 {
268 let (buffer, base, len) = aligned_buffer_from(&encoded);
269 let decoded = decode_arrow_ipc_zero_copy(buffer).unwrap();
270 let data_ptr = decoded.buffers()[0].as_ptr() as usize;
271 assert!(
272 data_ptr >= base && data_ptr < base + len,
273 "decoded data buffer at {data_ptr:#x} is outside input \
274 [{base:#x}, {:#x}) — a copy happened (not zero-copy)",
275 base + len
276 );
277 }
278 }
279
280 #[test]
284 fn ipc_zero_copy_decoder_handles_misaligned_input() {
285 let array = UInt64Array::from(vec![1, 2, 3, 4, 5, 6, 7, 8]);
286 let data = array.into_data();
287 let encoded = encode_arrow_ipc(&data).unwrap();
288
289 let mut shifted = Vec::with_capacity(encoded.len() + 1);
291 shifted.push(0u8);
292 shifted.extend_from_slice(&encoded);
293 let buffer = arrow::buffer::Buffer::from_vec(shifted).slice(1);
294
295 let decoded = decode_arrow_ipc_zero_copy(buffer).unwrap();
296 assert_eq!(data, decoded);
297 }
298
299 #[test]
300 fn ipc_roundtrip_string() {
301 let array = StringArray::from(vec!["hello", "world"]);
302 let data = array.into_data();
303 let encoded = encode_arrow_ipc(&data).unwrap();
304 let decoded = decode_arrow_ipc(&encoded).unwrap();
305 assert_eq!(data, decoded);
306 }
307
308 #[test]
309 fn ipc_roundtrip_empty_array() {
310 let array = UInt64Array::from(Vec::<u64>::new());
311 let data = array.into_data();
312 let encoded = encode_arrow_ipc(&data).unwrap();
313 let decoded = decode_arrow_ipc(&encoded).unwrap();
314 assert_eq!(data.len(), decoded.len());
315 }
316
317 #[test]
323 fn ipc_roundtrip_empty_typed_array_preserves_type() {
324 use arrow::array::Float32Array;
325 let data = Float32Array::from(Vec::<f32>::new()).into_data();
326 let encoded = encode_arrow_ipc(&data).unwrap();
327 let decoded = decode_arrow_ipc(&encoded).unwrap();
328 assert_eq!(decoded.data_type(), &arrow_schema::DataType::Float32);
329 assert_eq!(decoded.len(), 0);
330 }
331
332 #[test]
333 fn ipc_roundtrip_with_nulls() {
334 let array = UInt64Array::from(vec![Some(1), None, Some(3)]);
335 let data = array.into_data();
336 let encoded = encode_arrow_ipc(&data).unwrap();
337 let decoded = decode_arrow_ipc(&encoded).unwrap();
338 assert_eq!(data, decoded);
339 }
340}