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