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::channel::oneshot;
17use futures::future::BoxFuture;
18use futures::pin_mut;
19use futures::stream::BoxStream;
20use futures::stream::once;
21use futures::try_join;
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_in;
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::LayoutWriterContext;
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: LayoutWriterContext,
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 |_| async move {
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 |_| async move {
216 values.write_stream(
217 ctx2,
218 segment_sink2,
219 SequentialStreamAdapter::new(dtype2, once(values_fut)).sendable(),
220 values_eof,
221 &session2,
222 ).await
223 });
224
225 yield async move {
226 try_join!(codes_fut, values_layout)
227 }.boxed();
228 }
229 };
230
231 let mut child_layouts = child_layouts
232 .buffered(usize::MAX)
233 .map(|result| {
234 let (codes_layout, values_layout) = result?;
235 Ok::<_, VortexError>(DictLayout::new(values_layout, codes_layout).into_layout())
237 })
238 .try_collect::<Vec<_>>()
239 .await?;
240
241 if child_layouts.len() == 1 {
242 return Ok(child_layouts.remove(0));
243 }
244
245 let row_count = child_layouts.iter().map(|child| child.row_count()).sum();
246 Ok(ChunkedLayout::new(
247 row_count,
248 dtype,
249 OwnedLayoutChildren::layout_children(child_layouts),
250 )
251 .into_layout())
252 }
253}
254
255enum DictionaryChunk {
256 Codes {
257 seq_id: SequenceId,
258 codes: ArrayRef,
259 codes_ptype: PType,
260 },
261 Values((SequenceId, ArrayRef)),
262}
263
264type DictionaryStream = BoxStream<'static, VortexResult<DictionaryChunk>>;
265
266fn dict_encode_stream(
267 input: SendableSequentialStream,
268 constraints: DictConstraints,
269 mut exec_ctx: ExecutionCtx,
270) -> DictionaryStream {
271 Box::pin(try_stream! {
272 let mut state = DictStreamState {
273 encoder: None,
274 constraints,
275 };
276
277 let input = input.peekable();
278 pin_mut!(input);
279
280 while let Some(item) = input.next().await {
281 let (sequence_id, chunk) = item?;
282
283 match input.as_mut().peek().await {
287 Some(_) => {
288 let mut labeler = DictChunkLabeler::new(sequence_id);
289 let chunks = state.encode(&mut labeler, chunk, &mut exec_ctx)?;
290 drop(labeler);
291 for dict_chunk in chunks {
292 yield dict_chunk;
293 }
294 }
295 None => {
296 let mut labeler = DictChunkLabeler::new(sequence_id);
298 let encoded = state.encode(&mut labeler, chunk, &mut exec_ctx)?;
299 let drained = state.drain_values(&mut labeler);
300 drop(labeler);
301 for dict_chunk in encoded.into_iter().chain(drained.into_iter()) {
302 yield dict_chunk;
303 }
304 }
305 }
306 }
307 })
308}
309
310struct DictStreamState {
311 encoder: Option<Box<dyn DictEncoder>>,
312 constraints: DictConstraints,
313}
314
315impl DictStreamState {
316 fn encode(
317 &mut self,
318 labeler: &mut DictChunkLabeler,
319 chunk: ArrayRef,
320 exec_ctx: &mut ExecutionCtx,
321 ) -> VortexResult<Vec<DictionaryChunk>> {
322 let mut res = Vec::new();
323 let mut to_be_encoded = Some(chunk);
324 while let Some(remaining) = to_be_encoded.take() {
325 match self.encoder.take() {
326 None => match start_encoding(&self.constraints, &remaining, exec_ctx)? {
327 EncodingState::Continue((encoder, encoded)) => {
328 let ptype = encoder.codes_ptype();
329 res.push(labeler.codes(encoded, ptype));
330 self.encoder = Some(encoder);
331 }
332 EncodingState::Done((values, encoded, unencoded)) => {
333 let ptype = PType::try_from(encoded.dtype())
335 .vortex_expect("codes should be primitive");
336 res.push(labeler.codes(encoded, ptype));
337 res.push(labeler.values(values));
338 to_be_encoded = Some(unencoded);
339 }
340 },
341 Some(encoder) => {
342 let ptype = encoder.codes_ptype();
343 match encode_chunk(encoder, &remaining, exec_ctx)? {
344 EncodingState::Continue((encoder, encoded)) => {
345 res.push(labeler.codes(encoded, ptype));
346 self.encoder = Some(encoder);
347 }
348 EncodingState::Done((values, encoded, unencoded)) => {
349 res.push(labeler.codes(encoded, ptype));
350 res.push(labeler.values(values));
351 to_be_encoded = Some(unencoded);
352 }
353 }
354 }
355 }
356 }
357 Ok(res)
358 }
359
360 fn drain_values(&mut self, labeler: &mut DictChunkLabeler) -> Vec<DictionaryChunk> {
361 match self.encoder.as_mut() {
362 None => Vec::new(),
363 Some(encoder) => vec![labeler.values(encoder.reset())],
364 }
365 }
366}
367
368struct DictChunkLabeler {
369 sequence_pointer: SequencePointer,
370}
371
372impl DictChunkLabeler {
373 fn new(starting_id: SequenceId) -> Self {
374 let sequence_pointer = starting_id.descend();
375 Self { sequence_pointer }
376 }
377
378 fn codes(&mut self, chunk: ArrayRef, ptype: PType) -> DictionaryChunk {
379 DictionaryChunk::Codes {
380 seq_id: self.sequence_pointer.advance(),
381 codes: chunk,
382 codes_ptype: ptype,
383 }
384 }
385
386 fn values(&mut self, chunk: ArrayRef) -> DictionaryChunk {
387 DictionaryChunk::Values((self.sequence_pointer.advance(), chunk))
388 }
389}
390
391type SequencedChunk = VortexResult<(SequenceId, ArrayRef)>;
392
393struct DictionaryTransformer {
394 input: DictionaryStream,
395 active_codes_tx: Option<kanal::AsyncSender<SequencedChunk>>,
396 active_values_tx: Option<oneshot::Sender<SequencedChunk>>,
397 pending_send: Option<BoxFuture<'static, Result<(), kanal::SendError>>>,
398}
399
400impl DictionaryTransformer {
401 fn new(input: DictionaryStream) -> Self {
402 Self {
403 input,
404 active_codes_tx: None,
405 active_values_tx: None,
406 pending_send: None,
407 }
408 }
409}
410
411impl Stream for DictionaryTransformer {
412 type Item = (SendableSequentialStream, BoxFuture<'static, SequencedChunk>);
413
414 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
415 loop {
416 if let Some(mut send_fut) = self.pending_send.take() {
418 match send_fut.poll_unpin(cx) {
419 Poll::Ready(Ok(())) => {
420 }
422 Poll::Ready(Err(_)) => {
423 self.active_codes_tx = None;
425 if let Some(values_tx) = self.active_values_tx.take() {
426 drop(values_tx.send(Err(vortex_err!("values receiver dropped"))));
427 }
428 }
429 Poll::Pending => {
430 self.pending_send = Some(send_fut);
432 return Poll::Pending;
433 }
434 }
435 }
436
437 match self.input.poll_next_unpin(cx) {
438 Poll::Ready(Some(Ok(DictionaryChunk::Codes {
439 seq_id,
440 codes,
441 codes_ptype,
442 }))) => {
443 if self.active_codes_tx.is_none() {
444 let (codes_tx, codes_rx) = kanal::bounded_async::<SequencedChunk>(1);
446 let (values_tx, values_rx) = oneshot::channel();
447
448 self.active_codes_tx = Some(codes_tx.clone());
449 self.active_values_tx = Some(values_tx);
450
451 let codes_dtype = DType::Primitive(codes_ptype, Nullability::NonNullable);
453
454 self.pending_send =
456 Some(Box::pin(
457 async move { codes_tx.send(Ok((seq_id, codes))).await },
458 ));
459
460 let codes_stream = SequentialStreamAdapter::new(
462 codes_dtype,
463 codes_rx.into_stream().boxed(),
464 )
465 .sendable();
466
467 let values_future = async move {
468 values_rx
469 .await
470 .map_err(|e| vortex_err!("values sender dropped: {}", e))
471 .flatten()
472 }
473 .boxed();
474
475 return Poll::Ready(Some((codes_stream, values_future)));
476 }
477
478 if let Some(tx) = &self.active_codes_tx {
480 let tx = tx.clone();
481 self.pending_send =
482 Some(Box::pin(async move { tx.send(Ok((seq_id, codes))).await }));
483 }
484 }
485 Poll::Ready(Some(Ok(DictionaryChunk::Values(values)))) => {
486 if let Some(values_tx) = self.active_values_tx.take() {
488 drop(values_tx.send(Ok(values)));
489 }
490 self.active_codes_tx = None; }
492 Poll::Ready(Some(Err(e))) => {
493 if let Some(values_tx) = self.active_values_tx.take() {
495 drop(values_tx.send(Err(e)));
496 }
497 self.active_codes_tx = None;
498 return Poll::Ready(None);
500 }
501 Poll::Ready(None) => {
502 if let Some(values_tx) = self.active_values_tx.take() {
504 drop(values_tx.send(Err(vortex_err!("Incomplete dictionary group"))));
505 }
506 self.active_codes_tx = None;
507 return Poll::Ready(None);
508 }
509 Poll::Pending => return Poll::Pending,
510 }
511 }
512 }
513}
514
515async fn peek_first_chunk(
516 mut stream: BoxStream<'static, SequencedChunk>,
517) -> VortexResult<(BoxStream<'static, SequencedChunk>, Option<ArrayRef>)> {
518 match stream.next().await {
519 None => Ok((stream.boxed(), None)),
520 Some(Err(e)) => Err(e),
521 Some(Ok((sequence_id, chunk))) => {
522 let chunk_clone = chunk.clone();
523 let reconstructed_stream =
524 once(async move { Ok((sequence_id, chunk_clone)) }).chain(stream);
525 Ok((reconstructed_stream.boxed(), Some(chunk)))
526 }
527 }
528}
529
530pub fn dict_layout_supported(dtype: &DType) -> bool {
531 matches!(
532 dtype,
533 DType::Primitive(..) | DType::Utf8(_) | DType::Binary(_)
534 )
535}
536
537#[derive(prost::Message)]
538pub struct DictLayoutMetadata {
539 #[prost(enumeration = "PType", tag = "1")]
540 codes_ptype: i32,
542}
543
544impl DictLayoutMetadata {
545 pub fn new(codes_ptype: PType) -> Self {
546 let mut metadata = Self::default();
547 metadata.set_codes_ptype(codes_ptype);
548 metadata
549 }
550}
551
552enum EncodingState {
553 Continue((Box<dyn DictEncoder>, ArrayRef)),
554 Done((ArrayRef, ArrayRef, ArrayRef)),
556}
557
558fn start_encoding(
559 constraints: &DictConstraints,
560 chunk: &ArrayRef,
561 ctx: &mut ExecutionCtx,
562) -> VortexResult<EncodingState> {
563 let encoder = dict_encoder_in(chunk, constraints, ctx.allocator().clone());
564 encode_chunk(encoder, chunk, ctx)
565}
566
567fn encode_chunk(
568 mut encoder: Box<dyn DictEncoder>,
569 chunk: &ArrayRef,
570 ctx: &mut ExecutionCtx,
571) -> VortexResult<EncodingState> {
572 let encoded = encoder.encode(chunk, ctx)?.into_array();
573 match remainder(chunk, encoded.len())? {
574 None => Ok(EncodingState::Continue((encoder, encoded))),
575 Some(unencoded) => Ok(EncodingState::Done((encoder.reset(), encoded, unencoded))),
576 }
577}
578
579fn remainder(array: &ArrayRef, encoded_len: usize) -> VortexResult<Option<ArrayRef>> {
580 if encoded_len < array.len() {
581 Ok(Some(array.slice(encoded_len..array.len())?))
582 } else {
583 Ok(None)
584 }
585}
586
587#[cfg(test)]
588mod tests {
589 use std::sync::LazyLock;
590
591 use futures::StreamExt;
592 use vortex_array::IntoArray;
593 use vortex_array::VortexSessionExecute;
594 use vortex_array::arrays::VarBinArray;
595 use vortex_array::builders::dict::DictConstraints;
596 use vortex_array::dtype::DType;
597 use vortex_array::dtype::Nullability::NonNullable;
598 use vortex_array::dtype::PType;
599 use vortex_array::session::ArraySession;
600 use vortex_session::VortexSession;
601
602 use super::DictionaryTransformer;
603 use super::dict_encode_stream;
604 use crate::sequence::SequenceId;
605 use crate::sequence::SequentialStream;
606 use crate::sequence::SequentialStreamAdapter;
607 use crate::sequence::SequentialStreamExt;
608
609 static SESSION: LazyLock<VortexSession> =
610 LazyLock::new(|| VortexSession::empty().with::<ArraySession>());
611
612 #[tokio::test]
617 async fn test_dict_transformer_uses_u8_for_small_dictionaries() {
618 let constraints = DictConstraints {
620 max_bytes: 1024 * 1024,
621 max_len: 100,
622 };
623
624 let arr = VarBinArray::from(vec!["hello", "world", "hello", "world"]).into_array();
626
627 let mut pointer = SequenceId::root();
629 let input_stream = SequentialStreamAdapter::new(
630 arr.dtype().clone(),
631 futures::stream::once(async move { Ok((pointer.advance(), arr)) }),
632 )
633 .sendable();
634
635 let dict_stream =
637 dict_encode_stream(input_stream, constraints, SESSION.create_execution_ctx());
638
639 let mut transformer = DictionaryTransformer::new(dict_stream);
641
642 let (codes_stream, _values_fut) = transformer
644 .next()
645 .await
646 .expect("expected at least one dictionary run");
647
648 assert_eq!(
650 codes_stream.dtype(),
651 &DType::Primitive(PType::U8, NonNullable),
652 "codes stream should use U8 dtype for small dictionaries, not U16"
653 );
654 }
655
656 #[tokio::test]
658 async fn test_dict_transformer_uses_u16_for_large_dictionaries() {
659 let constraints = DictConstraints {
661 max_bytes: 1024 * 1024,
662 max_len: 1000,
663 };
664
665 let values: Vec<String> = (0..300).map(|i| format!("value_{i}")).collect();
667 let arr =
668 VarBinArray::from(values.iter().map(|s| s.as_str()).collect::<Vec<_>>()).into_array();
669
670 let mut pointer = SequenceId::root();
672 let input_stream = SequentialStreamAdapter::new(
673 arr.dtype().clone(),
674 futures::stream::once(async move { Ok((pointer.advance(), arr)) }),
675 )
676 .sendable();
677
678 let dict_stream =
680 dict_encode_stream(input_stream, constraints, SESSION.create_execution_ctx());
681
682 let mut transformer = DictionaryTransformer::new(dict_stream);
684
685 let (codes_stream, _values_fut) = transformer
687 .next()
688 .await
689 .expect("expected at least one dictionary run");
690
691 assert_eq!(
693 codes_stream.dtype(),
694 &DType::Primitive(PType::U16, NonNullable),
695 "codes stream should use U16 dtype for dictionaries with >255 entries"
696 );
697 }
698}