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