Skip to main content

vortex_layout/layouts/row_idx/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4mod expr;
5
6use std::fmt::Display;
7use std::fmt::Formatter;
8use std::ops::BitAnd;
9use std::ops::Range;
10use std::sync::Arc;
11use std::sync::OnceLock;
12
13use Nullability::NonNullable;
14pub use expr::*;
15use futures::FutureExt;
16use futures::future::BoxFuture;
17use vortex_array::ArrayRef;
18use vortex_array::IntoArray;
19use vortex_array::MaskFuture;
20use vortex_array::VortexSessionExecute;
21use vortex_array::builtins::ArrayBuiltins;
22use vortex_array::dtype::DType;
23use vortex_array::dtype::FieldMask;
24use vortex_array::dtype::FieldName;
25use vortex_array::dtype::Nullability;
26use vortex_array::dtype::PType;
27use vortex_array::expr::BoundExpression;
28use vortex_array::expr::ExactBoundExpr;
29use vortex_array::expr::transform::BoundPartitionedExpr;
30use vortex_array::expr::transform::partition_bound;
31use vortex_array::expr::traversal::NodeExt;
32use vortex_array::expr::traversal::Transformed;
33use vortex_array::expr::traversal::TraversalOrder;
34use vortex_array::scalar::PValue;
35use vortex_error::VortexExpect;
36use vortex_error::VortexResult;
37use vortex_mask::Mask;
38use vortex_sequence::Sequence;
39use vortex_sequence::SequenceArray;
40use vortex_session::VortexSession;
41use vortex_utils::aliases::dash_map::DashMap;
42
43use crate::ArrayFuture;
44use crate::LayoutReader;
45use crate::RowSplits;
46use crate::SplitRange;
47use crate::layouts::partitioned::BoundPartitionedExprEval;
48
49pub struct RowIdxLayoutReader {
50    name: Arc<str>,
51    row_offset: u64,
52    child: Arc<dyn LayoutReader>,
53    partition_cache: DashMap<ExactBoundExpr, Arc<OnceLock<Partitioning>>>,
54    session: VortexSession,
55}
56
57impl RowIdxLayoutReader {
58    pub fn new(row_offset: u64, child: Arc<dyn LayoutReader>, session: VortexSession) -> Self {
59        Self {
60            name: Arc::clone(child.name()),
61            row_offset,
62            child,
63            partition_cache: DashMap::with_hasher_and_shard_amount(Default::default(), 8),
64            session,
65        }
66    }
67
68    fn partition_expr(&self, expr: &BoundExpression) -> VortexResult<Partitioning> {
69        let key = ExactBoundExpr(expr.clone());
70
71        // Check cache first with read-only lock.
72        if let Some(entry) = self.partition_cache.get(&key)
73            && let Some(partitioning) = entry.value().get()
74        {
75            return Ok(partitioning.clone());
76        }
77
78        let result = self.compute_partitioning(expr)?;
79
80        self.partition_cache
81            .entry(key)
82            .or_insert_with(|| Arc::new(OnceLock::new()))
83            .get_or_init(|| result.clone());
84
85        Ok(result)
86    }
87
88    fn compute_partitioning(&self, expr: &BoundExpression) -> VortexResult<Partitioning> {
89        // Partition the expression into row idx and child expressions.
90        let mut partitioned = partition_bound(expr.clone(), |expr: &BoundExpression| {
91            if expr
92                .as_scalar()
93                .is_some_and(|scalar_fn| scalar_fn.is::<RowIdx>())
94            {
95                vec![Partition::RowIdx]
96            } else if expr.is_root() {
97                vec![Partition::Child]
98            } else {
99                vec![]
100            }
101        })?;
102
103        // If there's only a single partition, we can directly return the expression.
104        if partitioned.partitions.len() == 1 {
105            return Ok(match &partitioned.partition_annotations[0] {
106                Partition::RowIdx => Partitioning::RowIdx(replace_row_idx(expr.clone())?),
107                Partition::Child => Partitioning::Child(expr.clone()),
108            });
109        }
110
111        // Replace the row_idx expression with the root expression in the row_idx partition.
112        let partitions = partitioned
113            .partitions
114            .iter()
115            .cloned()
116            .map(replace_row_idx)
117            .collect::<VortexResult<Vec<_>>>()?
118            .into_boxed_slice();
119        partitioned.replace_partitions(partitions)?;
120
121        Ok(Partitioning::Partitioned(Arc::new(partitioned)))
122    }
123}
124
125#[derive(Clone)]
126enum Partitioning {
127    // An expression that only references the row index (e.g., `row_idx == 5`).
128    RowIdx(BoundExpression),
129    // An expression that does not reference the row index.
130    Child(BoundExpression),
131    // Contains both the RowIdx and Child expressions, (e.g., `row_idx < child.some_field`).
132    Partitioned(Arc<BoundPartitionedExpr<Partition>>),
133}
134
135#[derive(Clone, PartialEq, Eq, Hash)]
136enum Partition {
137    RowIdx,
138    Child,
139}
140
141impl Partition {
142    pub fn name(&self) -> &str {
143        match self {
144            Partition::RowIdx => "row_idx",
145            Partition::Child => "child",
146        }
147    }
148}
149
150impl From<Partition> for FieldName {
151    fn from(value: Partition) -> Self {
152        FieldName::from(value.name())
153    }
154}
155
156impl Display for Partition {
157    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
158        write!(f, "{}", self.name())
159    }
160}
161
162impl LayoutReader for RowIdxLayoutReader {
163    fn name(&self) -> &Arc<str> {
164        &self.name
165    }
166
167    fn dtype(&self) -> &DType {
168        self.child.dtype()
169    }
170
171    fn row_count(&self) -> u64 {
172        self.child.row_count()
173    }
174
175    fn register_splits(
176        &self,
177        field_mask: &[FieldMask],
178        split_range: &SplitRange,
179        splits: &mut RowSplits,
180    ) -> VortexResult<()> {
181        self.child.register_splits(field_mask, split_range, splits)
182    }
183
184    fn pruning_evaluation(
185        &self,
186        row_range: &Range<u64>,
187        expr: &BoundExpression,
188        mask: Mask,
189    ) -> VortexResult<MaskFuture> {
190        Ok(match &self.partition_expr(expr)? {
191            Partitioning::RowIdx(expr) => row_idx_mask_future(
192                self.row_offset,
193                row_range,
194                expr.clone(),
195                MaskFuture::ready(mask),
196                self.session.clone(),
197            ),
198            Partitioning::Child(expr) => self.child.pruning_evaluation(row_range, expr, mask)?,
199            Partitioning::Partitioned(..) => MaskFuture::ready(mask),
200        })
201    }
202
203    fn filter_evaluation(
204        &self,
205        row_range: &Range<u64>,
206        expr: &BoundExpression,
207        mask: MaskFuture,
208    ) -> VortexResult<MaskFuture> {
209        match &self.partition_expr(expr)? {
210            // Since this is run during pruning, we skip re-evaluating the row index expression
211            // during the filter evaluation.
212            Partitioning::RowIdx(_) => Ok(mask),
213            Partitioning::Child(expr) => self.child.filter_evaluation(row_range, expr, mask),
214            Partitioning::Partitioned(p) => Arc::clone(p).into_mask_future(
215                mask,
216                |annotation, expr, mask| match annotation {
217                    Partition::RowIdx => Ok(row_idx_mask_future(
218                        self.row_offset,
219                        row_range,
220                        expr.clone(),
221                        mask,
222                        self.session.clone(),
223                    )),
224                    Partition::Child => self.child.filter_evaluation(row_range, expr, mask),
225                },
226                |annotation, expr, mask| match annotation {
227                    Partition::RowIdx => Ok(row_idx_array_future(
228                        self.row_offset,
229                        row_range,
230                        expr.clone(),
231                        mask,
232                    )),
233                    Partition::Child => self.child.projection_evaluation(row_range, expr, mask),
234                },
235                self.session.clone(),
236            ),
237        }
238    }
239
240    fn projection_evaluation(
241        &self,
242        row_range: &Range<u64>,
243        expr: &BoundExpression,
244        mask: MaskFuture,
245    ) -> VortexResult<BoxFuture<'static, VortexResult<ArrayRef>>> {
246        match &self.partition_expr(expr)? {
247            Partitioning::RowIdx(expr) => Ok(row_idx_array_future(
248                self.row_offset,
249                row_range,
250                expr.clone(),
251                mask,
252            )),
253            Partitioning::Child(expr) => self.child.projection_evaluation(row_range, expr, mask),
254            Partitioning::Partitioned(p) => {
255                Arc::clone(p).into_array_future(mask, |annotation, expr, mask| match annotation {
256                    Partition::RowIdx => Ok(row_idx_array_future(
257                        self.row_offset,
258                        row_range,
259                        expr.clone(),
260                        mask,
261                    )),
262                    Partition::Child => self.child.projection_evaluation(row_range, expr, mask),
263                })
264            }
265        }
266    }
267
268    fn as_any(&self) -> &dyn std::any::Any {
269        self
270    }
271}
272
273fn replace_row_idx(expr: BoundExpression) -> VortexResult<BoundExpression> {
274    Ok(expr
275        .transform_down(|node| {
276            if node
277                .as_scalar()
278                .is_some_and(|scalar_fn| scalar_fn.is::<RowIdx>())
279            {
280                Ok(Transformed {
281                    value: BoundExpression::new_root(row_idx_dtype()),
282                    changed: true,
283                    order: TraversalOrder::Skip,
284                })
285            } else {
286                Ok(Transformed::no(node))
287            }
288        })?
289        .into_inner())
290}
291
292fn row_idx_dtype() -> DType {
293    DType::Primitive(PType::U64, NonNullable)
294}
295
296// Returns a SequenceArray representing the row indices for the given row range,
297fn idx_array(row_offset: u64, row_range: &Range<u64>) -> SequenceArray {
298    Sequence::try_new(
299        PValue::U64(row_offset + row_range.start),
300        PValue::U64(1),
301        PType::U64,
302        NonNullable,
303        usize::try_from(row_range.end - row_range.start)
304            .vortex_expect("Row range length must fit in usize"),
305    )
306    .vortex_expect("Failed to create row index array")
307}
308
309fn row_idx_mask_future(
310    row_offset: u64,
311    row_range: &Range<u64>,
312    expr: BoundExpression,
313    mask: MaskFuture,
314    session: VortexSession,
315) -> MaskFuture {
316    let row_range = row_range.clone();
317    MaskFuture::new(mask.len(), async move {
318        let array = idx_array(row_offset, &row_range).into_array();
319
320        let mut ctx = session.create_execution_ctx();
321        let result_mask = array
322            .apply_bound(&expr)?
323            .fill_null(false)?
324            .execute::<Mask>(&mut ctx)?;
325
326        Ok(result_mask.bitand(&mask.await?))
327    })
328}
329
330fn row_idx_array_future(
331    row_offset: u64,
332    row_range: &Range<u64>,
333    expr: BoundExpression,
334    mask: MaskFuture,
335) -> ArrayFuture {
336    let row_range = row_range.clone();
337    async move {
338        // The filter is lazy: trivial masks reduce to the sequence itself or an empty array, and
339        // value masks are left for the consumer to execute alongside the expression.
340        idx_array(row_offset, &row_range)
341            .into_array()
342            .filter(mask.await?)?
343            .apply_bound(&expr)
344    }
345    .boxed()
346}
347
348#[cfg(test)]
349mod tests {
350    use std::sync::Arc;
351
352    use vortex_array::ArrayContext;
353    use vortex_array::IntoArray as _;
354    use vortex_array::MaskFuture;
355    use vortex_array::VortexSessionExecute;
356    use vortex_array::arrays::BoolArray;
357    use vortex_array::assert_arrays_eq;
358    use vortex_array::dtype::DType;
359    use vortex_array::dtype::Nullability;
360    use vortex_array::dtype::PType;
361    use vortex_array::expr::eq;
362    use vortex_array::expr::gt;
363    use vortex_array::expr::lit;
364    use vortex_array::expr::or;
365    use vortex_array::expr::root;
366    use vortex_buffer::buffer;
367    use vortex_io::runtime::single::block_on;
368    use vortex_io::session::RuntimeSessionExt;
369    use vortex_mask::Mask;
370    use vortex_sequence::Sequence;
371
372    use crate::LayoutReader;
373    use crate::LayoutStrategy;
374    use crate::layouts::flat::writer::FlatLayoutStrategy;
375    use crate::layouts::row_idx::RowIdxLayoutReader;
376    use crate::layouts::row_idx::row_idx;
377    use crate::segments::TestSegments;
378    use crate::sequence::SequenceId;
379    use crate::sequence::SequentialArrayStreamExt;
380    use crate::test::new_session;
381
382    #[test]
383    fn flat_expr_no_row_id() {
384        block_on(|handle| async {
385            let session = new_session().with_handle(handle);
386            let mut ctx = session.create_execution_ctx();
387            let array_ctx = ArrayContext::empty();
388            let segments = Arc::new(TestSegments::default());
389            let (ptr, eof) = SequenceId::root().split();
390            let array = buffer![1..=5].into_array();
391            let layout = FlatLayoutStrategy::default()
392                .write_stream(
393                    array_ctx.into(),
394                    Arc::<TestSegments>::clone(&segments),
395                    array.to_array_stream().sequenced(ptr),
396                    eof,
397                    &session,
398                )
399                .await
400                .unwrap();
401
402            let expr = eq(root(), lit(3i32));
403            let reader = RowIdxLayoutReader::new(
404                0,
405                layout
406                    .new_reader("".into(), segments, &session, &Default::default())
407                    .unwrap(),
408                session.clone(),
409            );
410            let expr = expr.bind(reader.dtype()).unwrap();
411            let result = reader
412                .projection_evaluation(
413                    &(0..layout.row_count()),
414                    &expr,
415                    MaskFuture::new_true(layout.row_count().try_into().unwrap()),
416                )
417                .unwrap()
418                .await
419                .unwrap();
420
421            assert_arrays_eq!(
422                result,
423                BoolArray::from_iter([false, false, true, false, false]),
424                &mut ctx
425            );
426        })
427    }
428
429    #[test]
430    fn flat_expr_row_id() {
431        block_on(|handle| async {
432            let session = new_session().with_handle(handle);
433            let mut ctx = session.create_execution_ctx();
434            let array_ctx = ArrayContext::empty();
435            let segments = Arc::new(TestSegments::default());
436            let (ptr, eof) = SequenceId::root().split();
437            let array = buffer![1..=5].into_array();
438            let layout = FlatLayoutStrategy::default()
439                .write_stream(
440                    array_ctx.into(),
441                    Arc::<TestSegments>::clone(&segments),
442                    array.to_array_stream().sequenced(ptr),
443                    eof,
444                    &session,
445                )
446                .await
447                .unwrap();
448
449            let expr = gt(row_idx(), lit(3u64));
450            let reader = RowIdxLayoutReader::new(
451                0,
452                layout
453                    .new_reader("".into(), segments, &session, &Default::default())
454                    .unwrap(),
455                session.clone(),
456            );
457            let expr = expr.bind(reader.dtype()).unwrap();
458            let result = reader
459                .projection_evaluation(
460                    &(0..layout.row_count()),
461                    &expr,
462                    MaskFuture::new_true(layout.row_count().try_into().unwrap()),
463                )
464                .unwrap()
465                .await
466                .unwrap();
467
468            assert_arrays_eq!(
469                result,
470                BoolArray::from_iter([false, false, false, false, true]),
471                &mut ctx
472            );
473        })
474    }
475
476    #[test]
477    fn flat_expr_or() {
478        block_on(|handle| async {
479            let session = new_session().with_handle(handle);
480            let mut ctx = session.create_execution_ctx();
481            let array_ctx = ArrayContext::empty();
482            let segments = Arc::new(TestSegments::default());
483            let (ptr, eof) = SequenceId::root().split();
484            let array = buffer![1..=5].into_array();
485            let layout = FlatLayoutStrategy::default()
486                .write_stream(
487                    array_ctx.into(),
488                    Arc::<TestSegments>::clone(&segments),
489                    array.to_array_stream().sequenced(ptr),
490                    eof,
491                    &session,
492                )
493                .await
494                .unwrap();
495
496            let expr = or(
497                eq(root(), lit(3i32)),
498                or(gt(row_idx(), lit(3u64)), eq(root(), lit(1i32))),
499            );
500
501            let reader = RowIdxLayoutReader::new(
502                0,
503                layout
504                    .new_reader("".into(), segments, &session, &Default::default())
505                    .unwrap(),
506                session.clone(),
507            );
508            let expr = expr.bind(reader.dtype()).unwrap();
509            let result = reader
510                .projection_evaluation(
511                    &(0..layout.row_count()),
512                    &expr,
513                    MaskFuture::new_true(layout.row_count().try_into().unwrap()),
514                )
515                .unwrap()
516                .await
517                .unwrap();
518
519            assert_arrays_eq!(
520                result,
521                BoolArray::from_iter([true, false, true, false, true]),
522                &mut ctx
523            );
524        })
525    }
526
527    #[test]
528    fn row_idx_array_all_true_keeps_sequence() {
529        block_on(|_| async {
530            let expr = root()
531                .bind(&DType::Primitive(PType::U64, Nullability::NonNullable))
532                .unwrap();
533
534            let result = super::row_idx_array_future(
535                10,
536                &(2..7),
537                expr,
538                MaskFuture::ready(Mask::new_true(5)),
539            )
540            .await
541            .unwrap();
542
543            assert!(result.is::<Sequence>());
544            assert_arrays_eq!(
545                result,
546                buffer![12u64, 13, 14, 15, 16].into_array(),
547                &mut new_session().create_execution_ctx()
548            );
549        })
550    }
551
552    #[test]
553    fn row_idx_array_all_false_returns_empty() {
554        block_on(|_| async {
555            let expr = root()
556                .bind(&DType::Primitive(PType::U64, Nullability::NonNullable))
557                .unwrap();
558
559            let result = super::row_idx_array_future(
560                10,
561                &(2..7),
562                expr,
563                MaskFuture::ready(Mask::new_false(5)),
564            )
565            .await
566            .unwrap();
567
568            assert_eq!(
569                result.dtype(),
570                &DType::Primitive(PType::U64, Nullability::NonNullable)
571            );
572            assert_eq!(result.len(), 0);
573        })
574    }
575}