1use arrow::array::ArrayData;
38use arrow::buffer::Buffer as ArrowBuffer;
39use arrow::ipc::writer::{DictionaryTracker, IpcDataGenerator, IpcWriteOptions};
40use arrow::ipc::{Buffer as IpcBuffer, FieldNode, MessageHeader, MetadataVersion};
41use arrow_schema::{DataType, Field, Schema};
42use eyre::{Context, bail, eyre};
43
44use super::ARROW_BUFFER_ALIGNMENT as ALIGN;
45
46const CONTINUATION_MARKER: [u8; 4] = [0xff, 0xff, 0xff, 0xff];
48const PREFIX_LEN: usize = 8;
50
51#[inline]
52fn round_up(n: usize, align: usize) -> usize {
53 debug_assert!(align.is_power_of_two());
54 (n + align - 1) & !(align - 1)
55}
56
57fn is_fast_path_type(data_type: &DataType) -> bool {
61 use DataType::*;
62 matches!(
63 data_type,
64 Null | Boolean
65 | Int8
66 | Int16
67 | Int32
68 | Int64
69 | UInt8
70 | UInt16
71 | UInt32
72 | UInt64
73 | Float16
74 | Float32
75 | Float64
76 | Timestamp(_, _)
77 | Date32
78 | Date64
79 | Time32(_)
80 | Time64(_)
81 | Duration(_)
82 | Interval(_)
83 | Decimal128(_, _)
84 | Decimal256(_, _)
85 | FixedSizeBinary(_)
86 | Binary
87 | LargeBinary
88 | Utf8
89 | LargeUtf8
90 | List(_)
91 | LargeList(_)
92 | FixedSizeList(_, _)
93 | Struct(_)
94 )
95}
96
97struct Desc {
100 offset: usize,
101 len: usize,
102 src: BufferSrc,
103}
104
105enum BufferSrc {
106 Bytes(ArrowBuffer, usize),
108 AllOnes,
110}
111
112#[derive(Default)]
113struct Layout {
114 nodes: Vec<FieldNode>,
115 ipc_buffers: Vec<IpcBuffer>,
116 descs: Vec<Desc>,
117 body_len: usize,
118}
119
120impl Layout {
121 fn push_buffer(&mut self, off: &mut usize, len: usize, src: BufferSrc) -> Option<()> {
126 let padded = len.checked_add(ALIGN - 1).map(|n| n & !(ALIGN - 1))?;
128 let next = off.checked_add(padded)?;
129 if next > super::MAX_IPC_BYTES {
130 return None;
131 }
132 self.ipc_buffers
133 .push(IpcBuffer::new(*off as i64, len as i64));
134 self.descs.push(Desc {
135 offset: *off,
136 len,
137 src,
138 });
139 *off = next;
140 Some(())
141 }
142}
143
144fn build_layout(array: &ArrayData) -> Option<Layout> {
148 let mut layout = Layout::default();
149 let mut off = 0usize;
150 build_layout_rec(array, &mut layout, &mut off)?;
151 layout.body_len = off;
152 Some(layout)
153}
154
155fn build_layout_rec(array: &ArrayData, layout: &mut Layout, off: &mut usize) -> Option<()> {
156 let data_type = array.data_type();
157 if !is_fast_path_type(data_type) || array.offset() != 0 {
158 return None;
159 }
160
161 let len = array.len();
162 let null_count = if matches!(data_type, DataType::Null) {
165 len
166 } else {
167 array.null_count()
168 };
169 layout
170 .nodes
171 .push(FieldNode::new(len as i64, null_count as i64));
172
173 if !matches!(data_type, DataType::Null) {
176 match array.nulls() {
177 Some(nulls) => {
178 if nulls.inner().offset() != 0 {
179 return None;
180 }
181 let sliced = nulls.inner().sliced();
182 let bytes = sliced.len();
183 layout.push_buffer(off, bytes, BufferSrc::Bytes(sliced, 0))?;
184 }
185 None => {
186 let bytes = len.div_ceil(8);
187 layout.push_buffer(off, bytes, BufferSrc::AllOnes)?;
188 }
189 }
190 }
191
192 for buffer in array.buffers() {
194 layout.push_buffer(off, buffer.len(), BufferSrc::Bytes(buffer.clone(), 0))?;
195 }
196
197 match data_type {
199 DataType::FixedSizeList(_, value_size) => {
200 let n = len.checked_mul(*value_size as usize)?;
203 let child = array.child_data().first()?;
204 if child.len() < n {
205 return None;
206 }
207 build_layout_rec(&child.slice(0, n), layout, off)?;
208 }
209 DataType::Struct(_) => {
210 for child in array.child_data() {
216 if child.len() < len {
217 return None;
218 }
219 build_layout_rec(&child.slice(0, len), layout, off)?;
220 }
221 }
222 _ => {
223 for child in array.child_data() {
227 build_layout_rec(child, layout, off)?;
228 }
229 }
230 }
231
232 Some(())
233}
234
235struct Prepared {
237 layout: Layout,
238 schema_message: Vec<u8>,
239 record_batch_message: Vec<u8>,
240 schema_block: usize,
241 record_batch_block: usize,
242 total: usize,
243}
244
245fn ipc_write_options() -> eyre::Result<IpcWriteOptions> {
246 IpcWriteOptions::try_new(ALIGN, false, MetadataVersion::V5)
247 .map_err(|e| eyre!("failed to build Arrow IPC write options: {e}"))
248}
249
250fn build_schema_message(data_type: &DataType) -> eyre::Result<Vec<u8>> {
253 let schema = Schema::new(vec![Field::new("data", data_type.clone(), true)]);
254 let options = ipc_write_options()?;
255 let mut tracker = DictionaryTracker::new(false);
256 let encoded = IpcDataGenerator {}.schema_to_bytes_with_dictionary_tracker(
257 &schema,
258 &mut tracker,
259 &options,
260 );
261 Ok(encoded.ipc_message)
262}
263
264fn build_record_batch_message(
267 num_rows: usize,
268 nodes: &[FieldNode],
269 buffers: &[IpcBuffer],
270 body_len: usize,
271) -> Vec<u8> {
272 use flatbuffers::FlatBufferBuilder;
273
274 let mut fbb = FlatBufferBuilder::new();
275 let buffers_fb = fbb.create_vector(buffers);
276 let nodes_fb = fbb.create_vector(nodes);
277
278 let record_batch = {
279 let mut builder = arrow::ipc::RecordBatchBuilder::new(&mut fbb);
280 builder.add_length(num_rows as i64);
281 builder.add_nodes(nodes_fb);
282 builder.add_buffers(buffers_fb);
283 builder.finish()
284 };
285
286 let message = {
287 let mut builder = arrow::ipc::MessageBuilder::new(&mut fbb);
288 builder.add_version(MetadataVersion::V5);
289 builder.add_header_type(MessageHeader::RecordBatch);
290 builder.add_bodyLength(body_len as i64);
291 builder.add_header(record_batch.as_union_value());
292 builder.finish()
293 };
294
295 fbb.finish(message, None);
296 fbb.finished_data().to_vec()
297}
298
299fn prepare(array: &ArrayData) -> Option<Prepared> {
300 let layout = build_layout(array)?;
301 let schema_message = build_schema_message(array.data_type()).ok()?;
302 let record_batch_message = build_record_batch_message(
303 array.len(),
304 &layout.nodes,
305 &layout.ipc_buffers,
306 layout.body_len,
307 );
308 let schema_block = round_up(PREFIX_LEN + schema_message.len(), ALIGN);
309 let record_batch_block = round_up(PREFIX_LEN + record_batch_message.len(), ALIGN);
310 let total = schema_block + record_batch_block + layout.body_len + PREFIX_LEN;
312 if total > super::MAX_IPC_BYTES {
319 return None;
320 }
321 Some(Prepared {
322 layout,
323 schema_message,
324 record_batch_message,
325 schema_block,
326 record_batch_block,
327 total,
328 })
329}
330
331pub fn ipc_fast_path_len(array: &ArrayData) -> Option<usize> {
336 prepare(array).map(|p| p.total)
337}
338
339fn write_framed_message(dst: &mut [u8], at: usize, flatbuffer: &[u8]) -> usize {
344 let block = round_up(PREFIX_LEN + flatbuffer.len(), ALIGN);
345 let metadata_len = (block - PREFIX_LEN) as i32;
346 dst[at..at + 4].copy_from_slice(&CONTINUATION_MARKER);
347 dst[at + 4..at + 8].copy_from_slice(&metadata_len.to_le_bytes());
348 dst[at + 8..at + 8 + flatbuffer.len()].copy_from_slice(flatbuffer);
349 dst[at + 8 + flatbuffer.len()..at + block].fill(0);
351 block
352}
353
354pub fn encode_ipc_into(array: &ArrayData, dst: &mut [u8]) -> eyre::Result<()> {
361 let prepared =
362 prepare(array).ok_or_else(|| eyre!("array is not Arrow IPC fast-path eligible"))?;
363 if dst.len() != prepared.total {
364 bail!(
365 "destination size {} does not match required IPC length {}",
366 dst.len(),
367 prepared.total
368 );
369 }
370
371 let mut at = 0;
372 at += write_framed_message(dst, at, &prepared.schema_message);
373 debug_assert_eq!(at, prepared.schema_block);
374 at += write_framed_message(dst, at, &prepared.record_batch_message);
375 debug_assert_eq!(at, prepared.schema_block + prepared.record_batch_block);
376
377 let body_start = at;
378 write_body(dst, body_start, &prepared.layout);
379 at = body_start + prepared.layout.body_len;
380
381 dst[at..at + 4].copy_from_slice(&CONTINUATION_MARKER);
383 dst[at + 4..at + 8].copy_from_slice(&0i32.to_le_bytes());
384 debug_assert_eq!(at + PREFIX_LEN, prepared.total);
385
386 Ok(())
387}
388
389fn write_body(dst: &mut [u8], body_start: usize, layout: &Layout) {
393 for desc in &layout.descs {
394 let start = body_start + desc.offset;
395 let end = start + desc.len;
396 match &desc.src {
397 BufferSrc::Bytes(buffer, src_off) => {
398 dst[start..end].copy_from_slice(&buffer.as_slice()[*src_off..*src_off + desc.len]);
399 }
400 BufferSrc::AllOnes => dst[start..end].fill(0xff),
401 }
402 let padded_end = body_start + desc.offset + round_up(desc.len, ALIGN);
403 dst[end..padded_end].fill(0);
404 }
405}
406
407pub fn encode_schema_message(data_type: &DataType) -> eyre::Result<Vec<u8>> {
415 let schema_message = build_schema_message(data_type)?;
416 let block = round_up(PREFIX_LEN + schema_message.len(), ALIGN);
417 let mut dst = vec![0u8; block];
418 write_framed_message(&mut dst, 0, &schema_message);
419 Ok(dst)
420}
421
422pub fn batch_fast_path_len(array: &ArrayData) -> Option<usize> {
426 let prepared = prepare(array)?;
427 Some(prepared.record_batch_block + prepared.layout.body_len + PREFIX_LEN)
428}
429
430pub fn encode_batch_into(array: &ArrayData, dst: &mut [u8]) -> eyre::Result<()> {
437 let prepared =
438 prepare(array).ok_or_else(|| eyre!("array is not Arrow IPC fast-path eligible"))?;
439 let expected = prepared.record_batch_block + prepared.layout.body_len + PREFIX_LEN;
440 if dst.len() != expected {
441 bail!(
442 "destination size {} does not match required batch length {expected}",
443 dst.len(),
444 );
445 }
446 let at = write_framed_message(dst, 0, &prepared.record_batch_message);
447 debug_assert_eq!(at, prepared.record_batch_block);
448 write_body(dst, at, &prepared.layout);
449 let body_end = at + prepared.layout.body_len;
452 dst[body_end..body_end + 4].copy_from_slice(&CONTINUATION_MARKER);
453 dst[body_end + 4..body_end + PREFIX_LEN].copy_from_slice(&0i32.to_le_bytes());
454 Ok(())
455}
456
457pub fn encode_ipc_to_vec(array: &ArrayData) -> eyre::Result<Vec<u8>> {
461 super::encode_arrow_ipc(array).context("Arrow IPC fallback encode")
462}
463
464struct Uint8Layout {
468 schema_message: Vec<u8>,
469 record_batch_message: Vec<u8>,
470 validity_len: usize,
471 validity_padded: usize,
472 body_len: usize,
473 total: usize,
474 data_offset: usize,
475}
476
477fn uint8_layout(data_len: usize) -> eyre::Result<Uint8Layout> {
478 if data_len > super::MAX_IPC_BYTES {
483 bail!(
484 "UInt8 payload too large: {data_len} bytes (max {})",
485 super::MAX_IPC_BYTES
486 );
487 }
488 let validity_len = data_len.div_ceil(8);
489 let validity_padded = round_up(validity_len, ALIGN);
490 let body_len = validity_padded + round_up(data_len, ALIGN);
491 let nodes = [FieldNode::new(data_len as i64, 0)];
494 let buffers = [
495 IpcBuffer::new(0, validity_len as i64),
496 IpcBuffer::new(validity_padded as i64, data_len as i64),
497 ];
498 let record_batch_message = build_record_batch_message(data_len, &nodes, &buffers, body_len);
499 let schema_message = build_schema_message(&DataType::UInt8)?;
500 let schema_block = round_up(PREFIX_LEN + schema_message.len(), ALIGN);
501 let record_batch_block = round_up(PREFIX_LEN + record_batch_message.len(), ALIGN);
502 let total = schema_block + record_batch_block + body_len + PREFIX_LEN;
503 if total > super::MAX_IPC_BYTES {
510 bail!(
511 "UInt8 payload too large: {data_len} bytes encodes to a {total}-byte \
512 Arrow IPC stream (max {})",
513 super::MAX_IPC_BYTES
514 );
515 }
516 let data_offset = schema_block + record_batch_block + validity_padded;
517 Ok(Uint8Layout {
518 schema_message,
519 record_batch_message,
520 validity_len,
521 validity_padded,
522 body_len,
523 total,
524 data_offset,
525 })
526}
527
528pub fn uint8_ipc_len(data_len: usize) -> eyre::Result<usize> {
532 Ok(uint8_layout(data_len)?.total)
533}
534
535pub fn encode_uint8_ipc_header(dst: &mut [u8], data_len: usize) -> eyre::Result<usize> {
544 let layout = uint8_layout(data_len)?;
545 if dst.len() != layout.total {
546 bail!(
547 "destination size {} does not match required UInt8 IPC length {}",
548 dst.len(),
549 layout.total
550 );
551 }
552 let mut at = 0;
553 at += write_framed_message(dst, at, &layout.schema_message);
554 at += write_framed_message(dst, at, &layout.record_batch_message);
555 let body_start = at;
556
557 dst[body_start..body_start + layout.validity_len].fill(0xff);
559 dst[body_start + layout.validity_len..body_start + layout.validity_padded].fill(0);
560
561 let data_end = layout.data_offset + data_len;
564 let body_end = body_start + layout.body_len;
565 dst[data_end..body_end].fill(0);
566
567 dst[body_end..body_end + 4].copy_from_slice(&CONTINUATION_MARKER);
569 dst[body_end + 4..body_end + 8].copy_from_slice(&0i32.to_le_bytes());
570 debug_assert_eq!(body_end + PREFIX_LEN, layout.total);
571
572 Ok(layout.data_offset)
573}
574
575pub fn schema_block_len(stream: &[u8]) -> Option<usize> {
579 if stream.len() < PREFIX_LEN || stream[0..4] != CONTINUATION_MARKER {
580 return None;
581 }
582 let metadata_len = i32::from_le_bytes(stream[4..8].try_into().ok()?);
583 let block = PREFIX_LEN.checked_add(usize::try_from(metadata_len).ok()?)?;
584 (block <= stream.len()).then_some(block)
585}
586
587pub fn schema_block_and_hash(stream: &[u8]) -> Option<(u64, &[u8])> {
594 let block = schema_block_len(stream)?;
595 let schema = stream.get(..block)?;
596 Some((dora_message::metadata::fnv1a(schema), schema))
597}
598
599pub fn batch_slice(stream: &[u8]) -> Option<&[u8]> {
605 let block = schema_block_len(stream)?;
606 (stream.len() >= block + PREFIX_LEN).then(|| &stream[block..])
609}
610
611const MAX_RETAINED_SCHEMAS: usize = 8;
616
617pub struct InputDecoder {
623 decoder: arrow::ipc::reader::StreamDecoder,
625 schema_hash: Option<u64>,
627 schemas: Vec<(u64, ArrowBuffer)>,
634}
635
636impl Default for InputDecoder {
637 fn default() -> Self {
638 Self::new()
639 }
640}
641
642impl InputDecoder {
643 pub fn new() -> Self {
647 Self {
648 decoder: arrow::ipc::reader::StreamDecoder::new(),
649 schema_hash: None,
650 schemas: Vec::new(),
651 }
652 }
653
654 pub fn reset(&mut self) {
658 self.decoder = arrow::ipc::reader::StreamDecoder::new();
659 self.schema_hash = None;
660 self.schemas.clear();
661 }
662
663 pub fn knows_schema(&self, hash: u64) -> bool {
671 self.schema_hash == Some(hash) || self.schemas.iter().any(|(h, _)| *h == hash)
672 }
673
674 pub fn set_schema(&mut self, hash: u64, schema: ArrowBuffer) -> eyre::Result<()> {
680 check_ipc_size(schema.len())?;
681 if self.schema_hash == Some(hash) {
682 return Ok(());
683 }
684 self.prime(hash, schema)
685 }
686
687 fn prime(&mut self, hash: u64, schema: ArrowBuffer) -> eyre::Result<()> {
690 let mut decoder = arrow::ipc::reader::StreamDecoder::new();
694 prime_with_schema(&mut decoder, schema.clone())?;
695 self.decoder = decoder;
696 self.schema_hash = Some(hash);
697 self.schemas.retain(|(h, _)| *h != hash);
698 self.schemas.push((hash, schema));
699 if self.schemas.len() > MAX_RETAINED_SCHEMAS {
700 self.schemas.remove(0);
701 }
702 Ok(())
703 }
704
705 pub fn decode_batch(
715 &mut self,
716 buffer: ArrowBuffer,
717 hash: u64,
718 ) -> eyre::Result<Option<arrow::array::ArrayData>> {
719 check_ipc_size(buffer.len())?;
720 if self.schema_hash != Some(hash) {
721 match self.schemas.iter().find(|(h, _)| *h == hash) {
722 Some((_, schema)) => {
723 let schema = schema.clone();
724 self.prime(hash, schema)?;
725 }
726 None => return Ok(None),
728 }
729 }
730 match decode_one_batch(&mut self.decoder, buffer) {
731 Ok(array) => {
732 self.decoder = arrow::ipc::reader::StreamDecoder::new();
738 self.schema_hash = None;
739 Ok(Some(array))
740 }
741 Err(e) => {
742 self.decoder = arrow::ipc::reader::StreamDecoder::new();
750 self.schema_hash = None;
751 Err(e)
752 }
753 }
754 }
755}
756
757fn check_ipc_size(len: usize) -> eyre::Result<()> {
762 if len > super::MAX_IPC_BYTES {
763 bail!(
764 "Arrow IPC payload too large: {len} bytes (max {})",
765 super::MAX_IPC_BYTES
766 );
767 }
768 Ok(())
769}
770
771fn prime_with_schema(
773 decoder: &mut arrow::ipc::reader::StreamDecoder,
774 mut buffer: ArrowBuffer,
775) -> eyre::Result<()> {
776 while !buffer.is_empty() {
777 let before = buffer.len();
778 if decoder
779 .decode(&mut buffer)
780 .map_err(|e| eyre!("failed to decode IPC schema message: {e}"))?
781 .is_some()
782 {
783 bail!("expected a schema message but got a record batch");
784 }
785 if buffer.len() == before {
788 bail!("IPC schema decoder made no progress on a partial/corrupt message");
789 }
790 }
791 Ok(())
792}
793
794fn decode_one_batch(
805 decoder: &mut arrow::ipc::reader::StreamDecoder,
806 mut buffer: ArrowBuffer,
807) -> eyre::Result<arrow::array::ArrayData> {
808 while !buffer.is_empty() {
809 let before = buffer.len();
810 if let Some(batch) = decoder
811 .decode(&mut buffer)
812 .map_err(|e| eyre!("failed to decode IPC record batch: {e}"))?
813 {
814 if batch.num_columns() != 1 {
815 bail!(
816 "expected 1 column in IPC record batch, got {}",
817 batch.num_columns()
818 );
819 }
820 return Ok(batch.column(0).to_data());
821 }
822 if buffer.len() == before {
825 bail!("IPC batch decoder made no progress on a partial/corrupt message");
826 }
827 }
828 bail!("IPC batch message yielded no record batch")
829}
830
831#[cfg(test)]
832mod tests {
833 use super::*;
834 use crate::arrow_utils::decode_arrow_ipc_zero_copy;
835 use arrow::array::{
836 Array, ArrayRef, BooleanArray, FixedSizeBinaryArray, Float32Array, Int32Array,
837 LargeStringArray, ListArray, NullArray, StringArray, StructArray, UInt8Array, UInt64Array,
838 };
839 use arrow::buffer::Buffer;
840 use arrow::ipc::reader::{StreamDecoder, StreamReader};
841 use arrow_schema::{DataType, Field};
842 use std::io::Cursor;
843 use std::sync::Arc;
844
845 fn fast_encode(array: &ArrayData) -> Vec<u8> {
847 let len = ipc_fast_path_len(array).expect("array should be fast-path eligible");
848 let mut buf = vec![0u8; len];
849 encode_ipc_into(array, &mut buf).expect("fast-path encode");
850 buf
851 }
852
853 fn read_official(bytes: &[u8]) -> ArrayData {
856 let mut reader = StreamReader::try_new(Cursor::new(bytes), None).expect("open IPC stream");
857 let batch = reader
858 .next()
859 .expect("one batch")
860 .expect("batch decodes via official reader");
861 assert_eq!(batch.num_columns(), 1);
862 batch.column(0).to_data()
863 }
864
865 fn aligned_buffer(bytes: &[u8]) -> (Buffer, usize, usize) {
868 use aligned_vec::{AVec, ConstAlign};
869 use std::ptr::NonNull;
870 let mut aligned: AVec<u8, ConstAlign<128>> = AVec::__from_elem(128, 0, bytes.len());
871 aligned.copy_from_slice(bytes);
872 let base = aligned.as_ptr() as usize;
873 let len = aligned.len();
874 let ptr = NonNull::new(aligned.as_ptr() as *mut u8).unwrap();
875 let buffer =
877 unsafe { Buffer::from_custom_allocation(ptr, len, std::sync::Arc::new(aligned)) };
878 (buffer, base, len)
879 }
880
881 fn assert_fast_roundtrip(array: &ArrayData) {
885 let encoded = fast_encode(array);
886 let decoded = read_official(&encoded);
887 assert_eq!(array, &decoded, "fast-path stream must decode to the input");
888 let (buffer, _, _) = aligned_buffer(&encoded);
890 let zc = decode_arrow_ipc_zero_copy(buffer).expect("zero-copy decode");
891 assert_eq!(array, &zc, "zero-copy decode must equal the input");
892 }
893
894 fn batch_bytes(array: &ArrayData) -> Vec<u8> {
897 let len = batch_fast_path_len(array).unwrap();
898 let mut buf = vec![0u8; len];
899 encode_batch_into(array, &mut buf).unwrap();
900 buf
901 }
902
903 fn batch_buf(array: &ArrayData) -> Buffer {
904 Buffer::from_vec(batch_bytes(array))
905 }
906
907 #[test]
912 fn input_decoder_schema_then_batches() {
913 let f32_schema = || Buffer::from_vec(encode_schema_message(&DataType::Float32).unwrap());
914
915 let mut dec = InputDecoder::new();
916
917 let early = Float32Array::from(vec![9.0]).into_data();
919 assert!(dec.decode_batch(batch_buf(&early), 7).unwrap().is_none());
920
921 dec.set_schema(7, f32_schema()).unwrap();
923 for vals in [vec![1.0f32, 2.0, 3.0], vec![4.0], vec![5.0, 6.0, 7.0]] {
924 let array = Float32Array::from(vals).into_data();
925 assert_eq!(
926 dec.decode_batch(batch_buf(&array), 7).unwrap().unwrap(),
927 array
928 );
929 }
930
931 let other = Float32Array::from(vec![8.0]).into_data();
933 assert!(dec.decode_batch(batch_buf(&other), 99).unwrap().is_none());
934
935 dec.set_schema(99, f32_schema()).unwrap();
937 let after = Float32Array::from(vec![10.0, 11.0]).into_data();
938 assert_eq!(
939 dec.decode_batch(batch_buf(&after), 99).unwrap().unwrap(),
940 after
941 );
942 }
943
944 #[test]
951 fn input_decoder_retains_multiple_schemas() {
952 let schema_msg = |dt: &DataType| Buffer::from_vec(encode_schema_message(dt).unwrap());
953
954 let mut dec = InputDecoder::new();
955 dec.set_schema(1, schema_msg(&DataType::Float32)).unwrap();
956 dec.set_schema(2, schema_msg(&DataType::Int32)).unwrap();
957
958 let ints = Int32Array::from(vec![1, 2, 3]).into_data();
960 assert_eq!(
961 dec.decode_batch(batch_buf(&ints), 2).unwrap().unwrap(),
962 ints
963 );
964
965 let floats = Float32Array::from(vec![4.0, 5.0]).into_data();
967 assert_eq!(
968 dec.decode_batch(batch_buf(&floats), 1).unwrap().unwrap(),
969 floats,
970 "a schema installed earlier must be retained across later primes"
971 );
972
973 let more_ints = Int32Array::from(vec![6]).into_data();
975 assert_eq!(
976 dec.decode_batch(batch_buf(&more_ints), 2).unwrap().unwrap(),
977 more_ints
978 );
979 }
980
981 #[test]
987 fn input_decoder_handles_sequential_batches_arrow_59_terminal_state() {
988 let f32_schema = || Buffer::from_vec(encode_schema_message(&DataType::Float32).unwrap());
989
990 let mut dec = InputDecoder::new();
991 dec.set_schema(7, f32_schema()).unwrap();
993
994 let batches = [
998 Float32Array::from(vec![1.0, 2.0]).into_data(),
999 Float32Array::from(vec![3.0]).into_data(),
1000 Float32Array::from(vec![4.0, 5.0, 6.0]).into_data(),
1001 Float32Array::from(vec![7.0, 8.0]).into_data(),
1002 Float32Array::from(vec![9.0, 10.0, 11.0, 12.0]).into_data(),
1003 ];
1004
1005 for (i, batch) in batches.iter().enumerate() {
1006 let result = dec.decode_batch(batch_buf(batch), 7);
1007 assert!(
1008 result.is_ok(),
1009 "batch {} decode failed: {:?}",
1010 i,
1011 result.err()
1012 );
1013 let decoded = result.unwrap().unwrap();
1014 assert_eq!(
1015 &decoded, batch,
1016 "batch {} mismatch: expected {:?}, got {:?}",
1017 i, batch, decoded
1018 );
1019 }
1020 }
1021
1022 #[test]
1023 fn input_decoder_evicts_oldest_schema_beyond_cap() {
1024 let f32_schema = || Buffer::from_vec(encode_schema_message(&DataType::Float32).unwrap());
1025
1026 let mut dec = InputDecoder::new();
1027 for hash in 0..=(MAX_RETAINED_SCHEMAS as u64) {
1030 dec.set_schema(hash, f32_schema()).unwrap();
1031 }
1032 let array = Float32Array::from(vec![1.0]).into_data();
1033 assert!(
1034 dec.decode_batch(batch_buf(&array), 0).unwrap().is_none(),
1035 "the oldest schema must be evicted beyond the cap"
1036 );
1037 assert_eq!(
1038 dec.decode_batch(batch_buf(&array), 1).unwrap().unwrap(),
1039 array,
1040 "schemas within the cap must be retained"
1041 );
1042 }
1043
1044 #[test]
1049 fn decode_batch_resets_on_error_then_reprimes() {
1050 let mut dec = InputDecoder::new();
1051 dec.set_schema(
1052 7,
1053 Buffer::from_vec(encode_schema_message(&DataType::Float32).unwrap()),
1054 )
1055 .unwrap();
1056
1057 let good = Float32Array::from(vec![4.0, 5.0]).into_data();
1059 assert_eq!(
1060 dec.decode_batch(batch_buf(&good), 7).unwrap().unwrap(),
1061 good
1062 );
1063
1064 let dropped = Float32Array::from(vec![6.0, 7.0, 8.0, 9.0]).into_data();
1069 let mut truncated = batch_bytes(&dropped);
1070 truncated.truncate(truncated.len() / 2);
1071 assert!(dec.decode_batch(Buffer::from_vec(truncated), 7).is_err());
1072
1073 let after = Float32Array::from(vec![10.0, 11.0]).into_data();
1076 assert_eq!(
1077 dec.decode_batch(batch_buf(&after), 7).unwrap().unwrap(),
1078 after,
1079 "after a failed batch the decoder must re-prime from the retained schema"
1080 );
1081
1082 dec.reset();
1084 let final_batch = Float32Array::from(vec![12.0]).into_data();
1085 assert!(
1086 dec.decode_batch(batch_buf(&final_batch), 7)
1087 .unwrap()
1088 .is_none(),
1089 "after a full reset the decoder must drop until a schema is re-installed"
1090 );
1091 }
1092
1093 #[test]
1103 fn input_decoder_dictionary_fallback_batch_sequence() {
1104 use arrow::array::DictionaryArray;
1105 use arrow::datatypes::Int32Type;
1106
1107 fn dict(words: &[&str]) -> ArrayData {
1110 let mut values: Vec<&str> = Vec::new();
1111 let mut keys: Vec<i32> = Vec::new();
1112 for w in words {
1113 let idx = values.iter().position(|v| v == w).unwrap_or_else(|| {
1114 values.push(*w);
1115 values.len() - 1
1116 });
1117 keys.push(idx as i32);
1118 }
1119 DictionaryArray::<Int32Type>::try_new(
1120 Int32Array::from(keys),
1121 Arc::new(StringArray::from(values)),
1122 )
1123 .unwrap()
1124 .into_data()
1125 }
1126
1127 let first = dict(&["a", "b", "a", "c", "b"]);
1128 assert!(
1130 ipc_fast_path_len(&first).is_none(),
1131 "dictionary must route to the official-writer fallback"
1132 );
1133
1134 let mut dec = InputDecoder::new();
1137 let full0 = encode_ipc_to_vec(&first).unwrap();
1138 let block = schema_block_len(&full0).unwrap();
1139 dec.set_schema(1, Buffer::from(&full0[..block])).unwrap();
1140
1141 let slice0 = batch_slice(&full0).expect("fallback stream is a valid IPC stream");
1146 assert_eq!(
1147 dec.decode_batch(Buffer::from(slice0), 1)
1148 .unwrap()
1149 .expect("first batch decodes against the primed decoder"),
1150 first
1151 );
1152 for words in [
1153 ["x", "y", "x", "z"].as_slice(),
1154 ["b", "b"].as_slice(),
1155 ["new", "values", "entirely"].as_slice(),
1156 ] {
1157 let arr = dict(words);
1158 let full = encode_ipc_to_vec(&arr).unwrap();
1159 let slice = batch_slice(&full).expect("fallback stream is a valid IPC stream");
1160 let got = dec
1161 .decode_batch(Buffer::from(slice), 1)
1162 .unwrap()
1163 .expect("batch must decode against the primed decoder");
1164 assert_eq!(
1165 got, arr,
1166 "replacement-dictionary batch must decode correctly"
1167 );
1168 }
1169 }
1170
1171 #[test]
1176 fn schema_primed_decoder_decodes_batch_sequence() {
1177 let schema = encode_schema_message(&DataType::Float32).unwrap();
1178 let mut decoder = StreamDecoder::new();
1179
1180 let mut sbuf = Buffer::from_vec(schema);
1182 while !sbuf.is_empty() {
1183 assert!(
1184 decoder.decode(&mut sbuf).unwrap().is_none(),
1185 "schema message must not yield a batch"
1186 );
1187 }
1188
1189 for vals in [vec![1.0f32, 2.0, 3.0], vec![4.0, 5.0], vec![6.0]] {
1190 let array = Float32Array::from(vals).into_data();
1191 let len = batch_fast_path_len(&array).unwrap();
1192 let mut buf = vec![0u8; len];
1193 encode_batch_into(&array, &mut buf).unwrap();
1194
1195 let mut bbuf = Buffer::from_vec(buf);
1196 let mut got = None;
1197 while !bbuf.is_empty() {
1198 if let Some(b) = decoder.decode(&mut bbuf).unwrap() {
1199 got = Some(b);
1200 break;
1201 }
1202 }
1203 assert_eq!(
1204 got.expect("batch message must decode against the primed decoder")
1205 .column(0)
1206 .to_data(),
1207 array
1208 );
1209 }
1210 }
1211
1212 #[test]
1216 fn uint8_ipc_header_constructs_in_place() {
1217 for data_len in [0usize, 1, 7, 8, 9, 1000] {
1218 let bytes: Vec<u8> = (0..data_len).map(|i| (i % 251) as u8).collect();
1219 let total = uint8_ipc_len(data_len).unwrap();
1220 let mut dst = vec![0u8; total];
1221 let offset = encode_uint8_ipc_header(&mut dst, data_len).unwrap();
1222 dst[offset..offset + data_len].copy_from_slice(&bytes);
1224
1225 let arr = arrow::array::make_array(read_official(&dst));
1227 let u8 = arr.as_any().downcast_ref::<UInt8Array>().unwrap();
1228 assert_eq!(u8.values(), bytes.as_slice(), "len {data_len}");
1229 assert_eq!(u8.null_count(), 0);
1230
1231 let array = UInt8Array::from(bytes).into_data();
1233 assert_eq!(dst, fast_encode(&array), "len {data_len}");
1234 }
1235 }
1236
1237 #[test]
1243 fn uint8_ipc_len_bounds_resulting_stream_not_input() {
1244 let max = super::super::MAX_IPC_BYTES;
1245
1246 let err = uint8_ipc_len(max).unwrap_err().to_string();
1249 assert!(
1250 err.contains("too large"),
1251 "data_len == MAX_IPC_BYTES should be rejected, got: {err}"
1252 );
1253
1254 let mut lo = 0usize;
1257 let mut hi = max;
1258 while lo < hi {
1259 let mid = lo + (hi - lo).div_ceil(2);
1260 match uint8_ipc_len(mid) {
1261 Ok(_) => lo = mid,
1262 Err(_) => hi = mid - 1,
1263 }
1264 }
1265 let largest_ok = lo;
1266 let total = uint8_ipc_len(largest_ok).unwrap();
1267 assert!(
1268 total <= max,
1269 "accepted payload of {largest_ok} bytes encodes to {total} > {max}"
1270 );
1271 assert!(uint8_ipc_len(largest_ok + 1).is_err());
1274 assert!(
1275 largest_ok < max,
1276 "boundary should sit below the input limit"
1277 );
1278 }
1279
1280 #[test]
1281 fn roundtrip_primitive_no_nulls() {
1282 let array = Float32Array::from((0..1000).map(|i| i as f32).collect::<Vec<_>>()).into_data();
1283 assert_fast_roundtrip(&array);
1284 }
1285
1286 #[test]
1287 fn roundtrip_primitive_with_nulls() {
1288 let array = UInt64Array::from(vec![Some(1), None, Some(3), None, Some(5)]).into_data();
1289 assert_fast_roundtrip(&array);
1290 }
1291
1292 #[test]
1293 fn roundtrip_empty_primitive() {
1294 let array = Int32Array::from(Vec::<i32>::new()).into_data();
1295 assert_fast_roundtrip(&array);
1296 }
1297
1298 #[test]
1304 fn uint8_header_zero_len_full_stream_roundtrip() {
1305 let len = uint8_ipc_len(0).unwrap();
1306 let mut buf = vec![0u8; len];
1307 let off = encode_uint8_ipc_header(&mut buf, 0).unwrap();
1308 assert_eq!(off, len - PREFIX_LEN);
1310 assert_eq!(read_official(&buf).len(), 0);
1311 let (buffer, _, _) = aligned_buffer(&buf);
1312 let decoded = decode_arrow_ipc_zero_copy(buffer).unwrap();
1313 assert_eq!(decoded.data_type(), &DataType::UInt8);
1314 assert_eq!(decoded.len(), 0);
1315 }
1316
1317 #[test]
1323 fn schema_once_zero_len_batch_roundtrip() {
1324 let schema = || Buffer::from_vec(encode_schema_message(&DataType::UInt8).unwrap());
1325 let batch = |vals: &[u8]| {
1326 let a = UInt8Array::from(vals.to_vec()).into_data();
1327 let len = batch_fast_path_len(&a).unwrap();
1328 let mut b = vec![0u8; len];
1329 encode_batch_into(&a, &mut b).unwrap();
1330 Buffer::from_vec(b)
1331 };
1332
1333 let mut dec = InputDecoder::new();
1334 dec.set_schema(1, schema()).unwrap();
1335
1336 let decoded = dec
1338 .decode_batch(batch(&[]), 1)
1339 .unwrap()
1340 .expect("0-row batch must decode, not drop");
1341 assert_eq!(decoded.data_type(), &DataType::UInt8);
1342 assert_eq!(decoded.len(), 0);
1343
1344 for vals in [vec![1u8, 2, 3], vec![], vec![9u8], vec![]] {
1347 let d = dec.decode_batch(batch(&vals), 1).unwrap().unwrap();
1348 assert_eq!(d.len(), vals.len());
1349 assert_eq!(d.data_type(), &DataType::UInt8);
1350 }
1351 }
1352
1353 #[test]
1358 fn schema_once_zero_len_via_batch_slice_roundtrip() {
1359 let total = uint8_ipc_len(0).unwrap();
1360 let mut full = vec![0u8; total];
1361 encode_uint8_ipc_header(&mut full, 0).unwrap();
1362
1363 let sblock = schema_block_len(&full).unwrap();
1364 let schema = Buffer::from(&full[..sblock]);
1365 let batch = batch_slice(&full).expect("batch slice of a valid stream");
1366
1367 let mut dec = InputDecoder::new();
1368 dec.set_schema(7, schema).unwrap();
1369 let decoded = dec
1370 .decode_batch(Buffer::from(batch), 7)
1371 .unwrap()
1372 .expect("0-row batch via batch_slice must decode, not drop");
1373 assert_eq!(decoded.data_type(), &DataType::UInt8);
1374 assert_eq!(decoded.len(), 0);
1375 }
1376
1377 #[test]
1383 fn schema_block_plus_batch_slice_reconstructs_full_stream() {
1384 let array = Int32Array::from(vec![1, 2, 3]).into_data();
1385 let full = fast_encode(&array);
1386 let sblock = schema_block_len(&full).unwrap();
1387 let schema = &full[..sblock];
1388 let batch = batch_slice(&full).expect("batch slice of a valid stream");
1389 let rebuilt = [schema, batch].concat();
1390 assert_eq!(
1391 rebuilt, full,
1392 "schema_block ++ batch_slice must equal the original stream"
1393 );
1394 assert_eq!(read_official(&rebuilt), array);
1395 }
1396
1397 #[test]
1398 fn roundtrip_boolean() {
1399 let array =
1400 BooleanArray::from(vec![true, false, true, true, false, false, true]).into_data();
1401 assert_fast_roundtrip(&array);
1402 }
1403
1404 #[test]
1405 fn roundtrip_utf8() {
1406 let array =
1407 StringArray::from(vec![Some("hello"), None, Some(""), Some("world!")]).into_data();
1408 assert_fast_roundtrip(&array);
1409 }
1410
1411 #[test]
1412 fn roundtrip_large_utf8_64bit_offsets() {
1413 let array = LargeStringArray::from(vec!["a", "bb", "ccc"]).into_data();
1414 assert_fast_roundtrip(&array);
1415 }
1416
1417 #[test]
1418 fn roundtrip_fixed_size_binary() {
1419 let values = vec![vec![1u8, 2, 3], vec![4, 5, 6], vec![7, 8, 9]];
1420 let array = FixedSizeBinaryArray::try_from_iter(values.into_iter())
1421 .unwrap()
1422 .into_data();
1423 assert_fast_roundtrip(&array);
1424 }
1425
1426 #[test]
1430 fn roundtrip_fixed_size_list() {
1431 use arrow::array::FixedSizeListArray;
1432 let values = Int32Array::from((0..12).collect::<Vec<_>>());
1433 let field = Arc::new(Field::new("item", DataType::Int32, true));
1434 let array = FixedSizeListArray::try_new(field, 3, Arc::new(values), None)
1435 .unwrap()
1436 .into_data();
1437 assert_fast_roundtrip(&array);
1438 }
1439
1440 #[test]
1443 fn roundtrip_fixed_size_list_with_nulls() {
1444 use arrow::array::FixedSizeListArray;
1445 use arrow::buffer::NullBuffer;
1446 let values = Int32Array::from((0..12).collect::<Vec<_>>());
1447 let field = Arc::new(Field::new("item", DataType::Int32, true));
1448 let nulls = NullBuffer::from(vec![true, false, true, true]);
1449 let array = FixedSizeListArray::try_new(field, 3, Arc::new(values), Some(nulls))
1450 .unwrap()
1451 .into_data();
1452 assert_fast_roundtrip(&array);
1453 }
1454
1455 #[test]
1457 fn roundtrip_decimal128() {
1458 use arrow::array::Decimal128Array;
1459 let array = Decimal128Array::from(vec![Some(12_345i128), None, Some(-9_876), Some(0)])
1460 .with_precision_and_scale(20, 4)
1461 .unwrap()
1462 .into_data();
1463 assert_fast_roundtrip(&array);
1464 }
1465
1466 #[test]
1468 fn roundtrip_timestamp_temporal() {
1469 use arrow::array::TimestampMicrosecondArray;
1470 let array =
1471 TimestampMicrosecondArray::from(vec![Some(1_000_000i64), None, Some(2_500_000)])
1472 .into_data();
1473 assert_fast_roundtrip(&array);
1474 }
1475
1476 #[test]
1477 fn roundtrip_struct_with_multilevel_nulls() {
1478 let array = StructArray::from(vec![
1479 (
1480 Arc::new(Field::new("a", DataType::UInt64, true)),
1481 Arc::new(UInt64Array::from(vec![Some(1), None, Some(3)])) as ArrayRef,
1482 ),
1483 (
1484 Arc::new(Field::new("b", DataType::Utf8, true)),
1485 Arc::new(StringArray::from(vec![Some("x"), Some("yy"), None])) as ArrayRef,
1486 ),
1487 ])
1488 .into_data();
1489 assert_fast_roundtrip(&array);
1490 }
1491
1492 #[test]
1497 fn roundtrip_struct_with_oversized_child() {
1498 use arrow_schema::Fields;
1499 let child = Int32Array::from(vec![10, 20, 30]).into_data(); let fields: Fields = vec![Field::new("v", DataType::Int32, false)].into();
1501 let struct_data = ArrayData::builder(DataType::Struct(fields))
1502 .len(2) .add_child_data(child)
1504 .build()
1505 .unwrap();
1506
1507 assert!(
1508 ipc_fast_path_len(&struct_data).is_some(),
1509 "struct with an oversized child should stay on the fast path"
1510 );
1511 let decoded = read_official(&fast_encode(&struct_data));
1512 assert_eq!(decoded.len(), 2);
1513 let arr = arrow::array::make_array(decoded);
1514 let sa = arr.as_any().downcast_ref::<StructArray>().unwrap();
1515 let col = sa.column(0).as_any().downcast_ref::<Int32Array>().unwrap();
1516 assert_eq!(
1517 col.values(),
1518 &[10, 20],
1519 "child must be truncated to the struct's len"
1520 );
1521 }
1522
1523 #[test]
1524 fn roundtrip_list_of_primitive() {
1525 let data = vec![
1526 Some(vec![Some(0), Some(1), Some(2)]),
1527 None,
1528 Some(vec![Some(3), None, Some(5)]),
1529 Some(vec![]),
1530 ];
1531 let array =
1532 ListArray::from_iter_primitive::<arrow::datatypes::Int32Type, _, _>(data).into_data();
1533 assert_fast_roundtrip(&array);
1534 }
1535
1536 #[test]
1537 fn roundtrip_nullarray_zero_and_n() {
1538 assert_fast_roundtrip(&NullArray::new(0).into_data());
1539 assert_fast_roundtrip(&NullArray::new(7).into_data());
1540 }
1541
1542 #[test]
1546 fn fast_path_decodes_zero_copy() {
1547 let array = UInt64Array::from((0..50_000u64).collect::<Vec<_>>()).into_data();
1548 let encoded = fast_encode(&array);
1549
1550 {
1552 let (mut buffer, _, _) = aligned_buffer(&encoded);
1553 let mut decoder = StreamDecoder::new().with_require_alignment(true);
1554 let mut got = None;
1555 while !buffer.is_empty() {
1556 if let Some(b) = decoder
1557 .decode(&mut buffer)
1558 .expect("aligned fast-path stream must decode without realignment")
1559 {
1560 got = Some(b);
1561 break;
1562 }
1563 }
1564 assert_eq!(got.unwrap().column(0).to_data(), array);
1565 }
1566
1567 {
1569 let (buffer, base, len) = aligned_buffer(&encoded);
1570 let decoded = decode_arrow_ipc_zero_copy(buffer).unwrap();
1571 let ptr = decoded.buffers()[0].as_ptr() as usize;
1572 assert!(
1573 ptr >= base && ptr < base + len,
1574 "decoded data buffer at {ptr:#x} is outside input [{base:#x}, {:#x}) — a copy happened",
1575 base + len
1576 );
1577 }
1578 }
1579
1580 #[test]
1581 fn fast_path_len_matches_official_decode() {
1582 let array = Float32Array::from(vec![1.0, 2.0, 3.0, 4.0]).into_data();
1585 let encoded = fast_encode(&array);
1586 let mut reader = StreamReader::try_new(Cursor::new(&encoded[..]), None).unwrap();
1587 let _ = reader.next().unwrap().unwrap();
1588 assert!(reader.next().is_none(), "exactly one batch, fully consumed");
1589 }
1590
1591 #[test]
1594 fn sliced_array_routes_to_fallback_and_roundtrips() {
1595 let array = UInt64Array::from(vec![10, 20, 30, 40, 50])
1596 .into_data()
1597 .slice(2, 2); assert_eq!(array.offset(), 2);
1599 assert!(ipc_fast_path_len(&array).is_none());
1600
1601 let encoded = encode_ipc_to_vec(&array).unwrap();
1602 let decoded = read_official(&encoded);
1603 assert_eq!(array.len(), decoded.len());
1604 let dec = arrow::array::make_array(decoded);
1605 let dec = dec.as_any().downcast_ref::<UInt64Array>().unwrap();
1606 assert_eq!(dec.values(), &[30, 40]);
1607 }
1608
1609 #[test]
1611 fn view_type_routes_to_fallback() {
1612 use arrow::array::StringViewArray;
1613 let array = StringViewArray::from(vec!["a", "bb", "ccc"]).into_data();
1614 assert!(
1615 ipc_fast_path_len(&array).is_none(),
1616 "Utf8View is not fast-path eligible"
1617 );
1618 let encoded = encode_ipc_to_vec(&array).unwrap();
1619 let decoded = read_official(&encoded);
1620 assert_eq!(array, decoded);
1621 }
1622
1623 #[test]
1625 fn fuzz_roundtrip_many_shapes() {
1626 let mut state: u64 = 0x1234_5678_9abc_def0;
1628 let mut next = || {
1629 state = state
1630 .wrapping_mul(6364136223846793005)
1631 .wrapping_add(1442695040888963407);
1632 state
1633 };
1634
1635 for _ in 0..200 {
1636 let len = (next() % 64) as usize;
1637 let kind = next() % 6;
1638 let array: ArrayData = match kind {
1639 0 => UInt8Array::from(
1640 (0..len)
1641 .map(|i| {
1642 if next().is_multiple_of(4) {
1643 None
1644 } else {
1645 Some((i as u8).wrapping_add(1))
1646 }
1647 })
1648 .collect::<Vec<_>>(),
1649 )
1650 .into_data(),
1651 1 => Float32Array::from((0..len).map(|i| i as f32 * 0.5).collect::<Vec<_>>())
1652 .into_data(),
1653 2 => BooleanArray::from(
1654 (0..len)
1655 .map(|i| (i + next() as usize).is_multiple_of(2))
1656 .collect::<Vec<_>>(),
1657 )
1658 .into_data(),
1659 3 => StringArray::from(
1660 (0..len)
1661 .map(|i| {
1662 if next().is_multiple_of(5) {
1663 None
1664 } else {
1665 Some("x".repeat(i % 7))
1666 }
1667 })
1668 .collect::<Vec<_>>(),
1669 )
1670 .into_data(),
1671 4 => Int32Array::from(
1672 (0..len)
1673 .map(|i| {
1674 if next().is_multiple_of(3) {
1675 None
1676 } else {
1677 Some(i as i32 - 10)
1678 }
1679 })
1680 .collect::<Vec<_>>(),
1681 )
1682 .into_data(),
1683 _ => StructArray::from(vec![(
1684 Arc::new(Field::new("v", DataType::Int32, true)),
1685 Arc::new(Int32Array::from(
1686 (0..len).map(|i| Some(i as i32)).collect::<Vec<_>>(),
1687 )) as ArrayRef,
1688 )])
1689 .into_data(),
1690 };
1691 assert_fast_roundtrip(&array);
1692 }
1693 }
1694}