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