1use std::pin::Pin;
5use std::sync::Arc;
6use std::task::Context;
7use std::task::Poll;
8
9use async_stream::stream;
10use async_stream::try_stream;
11use async_trait::async_trait;
12use futures::FutureExt;
13use futures::Stream;
14use futures::StreamExt;
15use futures::TryStreamExt;
16use futures::future::BoxFuture;
17use futures::pin_mut;
18use futures::stream::BoxStream;
19use futures::stream::once;
20use futures::try_join;
21use vortex_array::ArrayContext;
22use vortex_array::ArrayRef;
23use vortex_array::ExecutionCtx;
24use vortex_array::IntoArray;
25use vortex_array::VortexSessionExecute;
26use vortex_array::arrays::Dict;
27use vortex_array::builders::dict::DictConstraints;
28use vortex_array::builders::dict::DictEncoder;
29use vortex_array::builders::dict::dict_encoder;
30use vortex_array::dtype::DType;
31use vortex_array::dtype::Nullability;
32use vortex_array::dtype::PType;
33use vortex_error::VortexError;
34use vortex_error::VortexExpect;
35use vortex_error::VortexResult;
36use vortex_error::vortex_err;
37use vortex_io::kanal_ext::KanalExt;
38use vortex_io::session::RuntimeSessionExt;
39use vortex_session::VortexSession;
40
41use crate::LayoutRef;
42use crate::LayoutStrategy;
43use crate::OwnedLayoutChildren;
44use crate::layouts::chunked::ChunkedLayout;
45use crate::layouts::compressed::CompressorPlugin;
46use crate::layouts::dict::DictLayout;
47use crate::segments::SegmentSinkRef;
48use crate::sequence::SendableSequentialStream;
49use crate::sequence::SequenceId;
50use crate::sequence::SequencePointer;
51use crate::sequence::SequentialStream;
52use crate::sequence::SequentialStreamAdapter;
53use crate::sequence::SequentialStreamExt;
54
55#[derive(Clone)]
62pub struct DictLayoutConstraints {
63 pub max_bytes: usize,
65 pub max_len: u16,
74}
75
76impl From<DictLayoutConstraints> for DictConstraints {
77 fn from(value: DictLayoutConstraints) -> Self {
78 DictConstraints {
79 max_bytes: value.max_bytes,
80 max_len: value.max_len as usize,
81 }
82 }
83}
84
85impl Default for DictLayoutConstraints {
86 fn default() -> Self {
87 Self {
88 max_bytes: 1024 * 1024,
89 max_len: u16::MAX,
90 }
91 }
92}
93
94#[derive(Clone, Default)]
95pub struct DictLayoutOptions {
96 pub constraints: DictLayoutConstraints,
97}
98
99#[derive(Clone)]
105pub struct DictStrategy {
106 codes: Arc<dyn LayoutStrategy>,
107 values: Arc<dyn LayoutStrategy>,
108 fallback: Arc<dyn LayoutStrategy>,
109 options: DictLayoutOptions,
110 probe_compressor: Arc<dyn CompressorPlugin>,
111}
112
113impl DictStrategy {
114 pub fn new<Codes: LayoutStrategy, Values: LayoutStrategy, Fallback: LayoutStrategy>(
115 codes: Codes,
116 values: Values,
117 fallback: Fallback,
118 options: DictLayoutOptions,
119 probe_compressor: Arc<dyn CompressorPlugin>,
120 ) -> Self {
121 Self {
122 codes: Arc::new(codes),
123 values: Arc::new(values),
124 fallback: Arc::new(fallback),
125 options,
126 probe_compressor,
127 }
128 }
129}
130
131#[async_trait]
132impl LayoutStrategy for DictStrategy {
133 async fn write_stream(
134 &self,
135 ctx: ArrayContext,
136 segment_sink: SegmentSinkRef,
137 stream: SendableSequentialStream,
138 mut eof: SequencePointer,
139 session: &VortexSession,
140 ) -> VortexResult<LayoutRef> {
141 if !dict_layout_supported(stream.dtype()) {
143 return self
144 .fallback
145 .write_stream(ctx, segment_sink, stream, eof, session)
146 .await;
147 }
148
149 let options = self.options.clone();
150 let dtype = stream.dtype().clone();
151
152 let (stream, first_chunk) = peek_first_chunk(stream).await?;
154 let stream = SequentialStreamAdapter::new(dtype.clone(), stream).sendable();
155
156 let should_fallback = match first_chunk {
157 None => true, Some(chunk) => {
159 let mut exec_ctx = session.create_execution_ctx();
160 let compressed = self
161 .probe_compressor
162 .compress_chunk(&chunk, &mut exec_ctx)?;
163 !compressed.is::<Dict>()
164 }
165 };
166 if should_fallback {
167 return self
169 .fallback
170 .write_stream(ctx, segment_sink, stream, eof, session)
171 .await;
172 }
173
174 let dict_stream = dict_encode_stream(
178 stream,
179 options.constraints.into(),
180 session.create_execution_ctx(),
181 );
182
183 let runs = DictionaryTransformer::new(dict_stream);
186
187 let handle = session.handle();
188 let dtype2 = dtype.clone();
189 let child_layouts = stream! {
190 pin_mut!(runs);
191
192 while let Some((codes_stream, values_fut)) = runs.next().await {
193 let codes = Arc::clone(&self.codes);
194 let codes_eof = eof.split_off();
195 let ctx2 = ctx.clone();
196 let segment_sink2 = Arc::clone(&segment_sink);
197 let session2 = session.clone();
198 let codes_fut = handle.spawn_nested(move |h| async move {
199 let session2 = session2.with_handle(h);
200 codes.write_stream(
201 ctx2,
202 segment_sink2,
203 codes_stream.sendable(),
204 codes_eof,
205 &session2,
206 ).await
207 });
208
209 let values = Arc::clone(&self.values);
210 let values_eof = eof.split_off();
211 let ctx2 = ctx.clone();
212 let segment_sink2 = Arc::clone(&segment_sink);
213 let dtype2 = dtype2.clone();
214 let session2 = session.clone();
215 let values_layout = handle.spawn_nested(move |h| async move {
216 let session2 = session2.with_handle(h);
217 values.write_stream(
218 ctx2,
219 segment_sink2,
220 SequentialStreamAdapter::new(dtype2, once(values_fut)).sendable(),
221 values_eof,
222 &session2,
223 ).await
224 });
225
226 yield async move {
227 try_join!(codes_fut, values_layout)
228 }.boxed();
229 }
230 };
231
232 let mut child_layouts = child_layouts
233 .buffered(usize::MAX)
234 .map(|result| {
235 let (codes_layout, values_layout) = result?;
236 Ok::<_, VortexError>(DictLayout::new(values_layout, codes_layout).into_layout())
238 })
239 .try_collect::<Vec<_>>()
240 .await?;
241
242 if child_layouts.len() == 1 {
243 return Ok(child_layouts.remove(0));
244 }
245
246 let row_count = child_layouts.iter().map(|child| child.row_count()).sum();
247 Ok(ChunkedLayout::new(
248 row_count,
249 dtype,
250 OwnedLayoutChildren::layout_children(child_layouts),
251 )
252 .into_layout())
253 }
254
255 fn buffered_bytes(&self) -> u64 {
256 self.codes.buffered_bytes() + self.values.buffered_bytes() + self.fallback.buffered_bytes()
257 }
258}
259
260enum DictionaryChunk {
261 Codes {
262 seq_id: SequenceId,
263 codes: ArrayRef,
264 codes_ptype: PType,
265 },
266 Values((SequenceId, ArrayRef)),
267}
268
269type DictionaryStream = BoxStream<'static, VortexResult<DictionaryChunk>>;
270
271fn dict_encode_stream(
272 input: SendableSequentialStream,
273 constraints: DictConstraints,
274 mut exec_ctx: ExecutionCtx,
275) -> DictionaryStream {
276 Box::pin(try_stream! {
277 let mut state = DictStreamState {
278 encoder: None,
279 constraints,
280 };
281
282 let input = input.peekable();
283 pin_mut!(input);
284
285 while let Some(item) = input.next().await {
286 let (sequence_id, chunk) = item?;
287
288 match input.as_mut().peek().await {
292 Some(_) => {
293 let mut labeler = DictChunkLabeler::new(sequence_id);
294 let chunks = state.encode(&mut labeler, chunk, &mut exec_ctx)?;
295 drop(labeler);
296 for dict_chunk in chunks {
297 yield dict_chunk;
298 }
299 }
300 None => {
301 let mut labeler = DictChunkLabeler::new(sequence_id);
303 let encoded = state.encode(&mut labeler, chunk, &mut exec_ctx)?;
304 let drained = state.drain_values(&mut labeler);
305 drop(labeler);
306 for dict_chunk in encoded.into_iter().chain(drained.into_iter()) {
307 yield dict_chunk;
308 }
309 }
310 }
311 }
312 })
313}
314
315struct DictStreamState {
316 encoder: Option<Box<dyn DictEncoder>>,
317 constraints: DictConstraints,
318}
319
320impl DictStreamState {
321 fn encode(
322 &mut self,
323 labeler: &mut DictChunkLabeler,
324 chunk: ArrayRef,
325 exec_ctx: &mut ExecutionCtx,
326 ) -> VortexResult<Vec<DictionaryChunk>> {
327 let mut res = Vec::new();
328 let mut to_be_encoded = Some(chunk);
329 while let Some(remaining) = to_be_encoded.take() {
330 match self.encoder.take() {
331 None => match start_encoding(&self.constraints, &remaining, exec_ctx)? {
332 EncodingState::Continue((encoder, encoded)) => {
333 let ptype = encoder.codes_ptype();
334 res.push(labeler.codes(encoded, ptype));
335 self.encoder = Some(encoder);
336 }
337 EncodingState::Done((values, encoded, unencoded)) => {
338 let ptype = PType::try_from(encoded.dtype())
340 .vortex_expect("codes should be primitive");
341 res.push(labeler.codes(encoded, ptype));
342 res.push(labeler.values(values));
343 to_be_encoded = Some(unencoded);
344 }
345 },
346 Some(encoder) => {
347 let ptype = encoder.codes_ptype();
348 match encode_chunk(encoder, &remaining, exec_ctx)? {
349 EncodingState::Continue((encoder, encoded)) => {
350 res.push(labeler.codes(encoded, ptype));
351 self.encoder = Some(encoder);
352 }
353 EncodingState::Done((values, encoded, unencoded)) => {
354 res.push(labeler.codes(encoded, ptype));
355 res.push(labeler.values(values));
356 to_be_encoded = Some(unencoded);
357 }
358 }
359 }
360 }
361 }
362 Ok(res)
363 }
364
365 fn drain_values(&mut self, labeler: &mut DictChunkLabeler) -> Vec<DictionaryChunk> {
366 match self.encoder.as_mut() {
367 None => Vec::new(),
368 Some(encoder) => vec![labeler.values(encoder.reset())],
369 }
370 }
371}
372
373struct DictChunkLabeler {
374 sequence_pointer: SequencePointer,
375}
376
377impl DictChunkLabeler {
378 fn new(starting_id: SequenceId) -> Self {
379 let sequence_pointer = starting_id.descend();
380 Self { sequence_pointer }
381 }
382
383 fn codes(&mut self, chunk: ArrayRef, ptype: PType) -> DictionaryChunk {
384 DictionaryChunk::Codes {
385 seq_id: self.sequence_pointer.advance(),
386 codes: chunk,
387 codes_ptype: ptype,
388 }
389 }
390
391 fn values(&mut self, chunk: ArrayRef) -> DictionaryChunk {
392 DictionaryChunk::Values((self.sequence_pointer.advance(), chunk))
393 }
394}
395
396type SequencedChunk = VortexResult<(SequenceId, ArrayRef)>;
397
398struct DictionaryTransformer {
399 input: DictionaryStream,
400 active_codes_tx: Option<kanal::AsyncSender<SequencedChunk>>,
401 active_values_tx: Option<oneshot::Sender<SequencedChunk>>,
402 pending_send: Option<BoxFuture<'static, Result<(), kanal::SendError>>>,
403}
404
405impl DictionaryTransformer {
406 fn new(input: DictionaryStream) -> Self {
407 Self {
408 input,
409 active_codes_tx: None,
410 active_values_tx: None,
411 pending_send: None,
412 }
413 }
414}
415
416impl Stream for DictionaryTransformer {
417 type Item = (SendableSequentialStream, BoxFuture<'static, SequencedChunk>);
418
419 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
420 loop {
421 if let Some(mut send_fut) = self.pending_send.take() {
423 match send_fut.poll_unpin(cx) {
424 Poll::Ready(Ok(())) => {
425 }
427 Poll::Ready(Err(_)) => {
428 self.active_codes_tx = None;
430 if let Some(values_tx) = self.active_values_tx.take() {
431 drop(values_tx.send(Err(vortex_err!("values receiver dropped"))));
432 }
433 }
434 Poll::Pending => {
435 self.pending_send = Some(send_fut);
437 return Poll::Pending;
438 }
439 }
440 }
441
442 match self.input.poll_next_unpin(cx) {
443 Poll::Ready(Some(Ok(DictionaryChunk::Codes {
444 seq_id,
445 codes,
446 codes_ptype,
447 }))) => {
448 if self.active_codes_tx.is_none() {
449 let (codes_tx, codes_rx) = kanal::bounded_async::<SequencedChunk>(1);
451 let (values_tx, values_rx) = oneshot::channel();
452
453 self.active_codes_tx = Some(codes_tx.clone());
454 self.active_values_tx = Some(values_tx);
455
456 let codes_dtype = DType::Primitive(codes_ptype, Nullability::NonNullable);
458
459 self.pending_send =
461 Some(Box::pin(
462 async move { codes_tx.send(Ok((seq_id, codes))).await },
463 ));
464
465 let codes_stream = SequentialStreamAdapter::new(
467 codes_dtype,
468 codes_rx.into_stream().boxed(),
469 )
470 .sendable();
471
472 let values_future = async move {
473 values_rx
474 .await
475 .map_err(|e| vortex_err!("values sender dropped: {}", e))
476 .flatten()
477 }
478 .boxed();
479
480 return Poll::Ready(Some((codes_stream, values_future)));
481 }
482
483 if let Some(tx) = &self.active_codes_tx {
485 let tx = tx.clone();
486 self.pending_send =
487 Some(Box::pin(async move { tx.send(Ok((seq_id, codes))).await }));
488 }
489 }
490 Poll::Ready(Some(Ok(DictionaryChunk::Values(values)))) => {
491 if let Some(values_tx) = self.active_values_tx.take() {
493 drop(values_tx.send(Ok(values)));
494 }
495 self.active_codes_tx = None; }
497 Poll::Ready(Some(Err(e))) => {
498 if let Some(values_tx) = self.active_values_tx.take() {
500 drop(values_tx.send(Err(e)));
501 }
502 self.active_codes_tx = None;
503 return Poll::Ready(None);
505 }
506 Poll::Ready(None) => {
507 if let Some(values_tx) = self.active_values_tx.take() {
509 drop(values_tx.send(Err(vortex_err!("Incomplete dictionary group"))));
510 }
511 self.active_codes_tx = None;
512 return Poll::Ready(None);
513 }
514 Poll::Pending => return Poll::Pending,
515 }
516 }
517 }
518}
519
520async fn peek_first_chunk(
521 mut stream: BoxStream<'static, SequencedChunk>,
522) -> VortexResult<(BoxStream<'static, SequencedChunk>, Option<ArrayRef>)> {
523 match stream.next().await {
524 None => Ok((stream.boxed(), None)),
525 Some(Err(e)) => Err(e),
526 Some(Ok((sequence_id, chunk))) => {
527 let chunk_clone = chunk.clone();
528 let reconstructed_stream =
529 once(async move { Ok((sequence_id, chunk_clone)) }).chain(stream);
530 Ok((reconstructed_stream.boxed(), Some(chunk)))
531 }
532 }
533}
534
535pub fn dict_layout_supported(dtype: &DType) -> bool {
536 matches!(
537 dtype,
538 DType::Primitive(..) | DType::Utf8(_) | DType::Binary(_)
539 )
540}
541
542#[derive(prost::Message)]
543pub struct DictLayoutMetadata {
544 #[prost(enumeration = "PType", tag = "1")]
545 codes_ptype: i32,
547}
548
549impl DictLayoutMetadata {
550 pub fn new(codes_ptype: PType) -> Self {
551 let mut metadata = Self::default();
552 metadata.set_codes_ptype(codes_ptype);
553 metadata
554 }
555}
556
557enum EncodingState {
558 Continue((Box<dyn DictEncoder>, ArrayRef)),
559 Done((ArrayRef, ArrayRef, ArrayRef)),
561}
562
563fn start_encoding(
564 constraints: &DictConstraints,
565 chunk: &ArrayRef,
566 ctx: &mut ExecutionCtx,
567) -> VortexResult<EncodingState> {
568 let encoder = dict_encoder(chunk, constraints);
569 encode_chunk(encoder, chunk, ctx)
570}
571
572fn encode_chunk(
573 mut encoder: Box<dyn DictEncoder>,
574 chunk: &ArrayRef,
575 ctx: &mut ExecutionCtx,
576) -> VortexResult<EncodingState> {
577 let encoded = encoder.encode(chunk, ctx)?.into_array();
578 match remainder(chunk, encoded.len())? {
579 None => Ok(EncodingState::Continue((encoder, encoded))),
580 Some(unencoded) => Ok(EncodingState::Done((encoder.reset(), encoded, unencoded))),
581 }
582}
583
584fn remainder(array: &ArrayRef, encoded_len: usize) -> VortexResult<Option<ArrayRef>> {
585 if encoded_len < array.len() {
586 Ok(Some(array.slice(encoded_len..array.len())?))
587 } else {
588 Ok(None)
589 }
590}
591
592#[cfg(test)]
593mod tests {
594 use std::sync::LazyLock;
595
596 use futures::StreamExt;
597 use vortex_array::IntoArray;
598 use vortex_array::VortexSessionExecute;
599 use vortex_array::arrays::VarBinArray;
600 use vortex_array::builders::dict::DictConstraints;
601 use vortex_array::dtype::DType;
602 use vortex_array::dtype::Nullability::NonNullable;
603 use vortex_array::dtype::PType;
604 use vortex_array::session::ArraySession;
605 use vortex_session::VortexSession;
606
607 use super::DictionaryTransformer;
608 use super::dict_encode_stream;
609 use crate::sequence::SequenceId;
610 use crate::sequence::SequentialStream;
611 use crate::sequence::SequentialStreamAdapter;
612 use crate::sequence::SequentialStreamExt;
613
614 static SESSION: LazyLock<VortexSession> =
615 LazyLock::new(|| VortexSession::empty().with::<ArraySession>());
616
617 #[tokio::test]
622 async fn test_dict_transformer_uses_u8_for_small_dictionaries() {
623 let constraints = DictConstraints {
625 max_bytes: 1024 * 1024,
626 max_len: 100,
627 };
628
629 let arr = VarBinArray::from(vec!["hello", "world", "hello", "world"]).into_array();
631
632 let mut pointer = SequenceId::root();
634 let input_stream = SequentialStreamAdapter::new(
635 arr.dtype().clone(),
636 futures::stream::once(async move { Ok((pointer.advance(), arr)) }),
637 )
638 .sendable();
639
640 let dict_stream =
642 dict_encode_stream(input_stream, constraints, SESSION.create_execution_ctx());
643
644 let mut transformer = DictionaryTransformer::new(dict_stream);
646
647 let (codes_stream, _values_fut) = transformer
649 .next()
650 .await
651 .expect("expected at least one dictionary run");
652
653 assert_eq!(
655 codes_stream.dtype(),
656 &DType::Primitive(PType::U8, NonNullable),
657 "codes stream should use U8 dtype for small dictionaries, not U16"
658 );
659 }
660
661 #[tokio::test]
663 async fn test_dict_transformer_uses_u16_for_large_dictionaries() {
664 let constraints = DictConstraints {
666 max_bytes: 1024 * 1024,
667 max_len: 1000,
668 };
669
670 let values: Vec<String> = (0..300).map(|i| format!("value_{i}")).collect();
672 let arr =
673 VarBinArray::from(values.iter().map(|s| s.as_str()).collect::<Vec<_>>()).into_array();
674
675 let mut pointer = SequenceId::root();
677 let input_stream = SequentialStreamAdapter::new(
678 arr.dtype().clone(),
679 futures::stream::once(async move { Ok((pointer.advance(), arr)) }),
680 )
681 .sendable();
682
683 let dict_stream =
685 dict_encode_stream(input_stream, constraints, SESSION.create_execution_ctx());
686
687 let mut transformer = DictionaryTransformer::new(dict_stream);
689
690 let (codes_stream, _values_fut) = transformer
692 .next()
693 .await
694 .expect("expected at least one dictionary run");
695
696 assert_eq!(
698 codes_stream.dtype(),
699 &DType::Primitive(PType::U16, NonNullable),
700 "codes stream should use U16 dtype for dictionaries with >255 entries"
701 );
702 }
703}