Skip to main content

vortex_layout/plan/physical/plans/
row_idx.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::fmt::Display;
5use std::fmt::Formatter;
6use std::ops::Range;
7
8use vortex_array::EmptyMetadata;
9use vortex_array::IntoArray;
10use vortex_array::dtype::DType;
11use vortex_array::dtype::FieldName;
12use vortex_array::dtype::Nullability;
13use vortex_array::dtype::PType;
14use vortex_array::dtype::StructFields;
15use vortex_array::expr::BoundExpression;
16use vortex_array::expr::transform::partition_bound;
17use vortex_array::expr::traversal::NodeExt;
18use vortex_array::expr::traversal::Transformed;
19use vortex_array::expr::traversal::TraversalOrder;
20use vortex_array::scalar_fn::fns::pack::Pack as PackFn;
21use vortex_buffer::Buffer;
22use vortex_error::VortexResult;
23use vortex_error::vortex_ensure_eq;
24use vortex_error::vortex_err;
25use vortex_mask::Mask;
26use vortex_session::registry::CachedId;
27
28use crate::layouts::row_idx::RowIdx as RowIdxFn;
29use crate::plan::physical::EvalPlan;
30use crate::plan::physical::PackPlan;
31use crate::plan::physical::Plan;
32use crate::plan::physical::PlanChildren;
33use crate::plan::physical::PlanId;
34use crate::plan::physical::PlanParts;
35use crate::plan::physical::PlanRef;
36use crate::plan::physical::PlanVTable;
37use crate::plan::physical::check_child_count;
38use crate::plan::physical::operators::OnceSource;
39use crate::plan::physical::pipeline::Chain;
40use crate::plan::physical::pipeline::Compiler;
41use crate::plan::physical::pipeline::Reach;
42use crate::plan::physical::plans::pack::rewrite_partition_root;
43use crate::segments::SegmentId;
44
45const ROW_IDX_PARTITION_NAME: &str = "row_idx";
46const CHILD_PARTITION_NAME: &str = "child";
47
48/// Generates global row indices for the current execution row domain.
49#[derive(Clone, Debug)]
50pub struct RowIdx;
51
52/// Operator-specific data for a [`RowIdx`] plan.
53#[derive(Clone, Debug)]
54pub struct RowIdxData;
55
56/// A childless source of global row indices supplied by its execution row domain.
57pub type RowIdxPlan = Plan<RowIdx>;
58
59impl RowIdxPlan {
60    /// Creates a row-index source covering `row_count` rows.
61    pub fn new(row_count: u64) -> Self {
62        PlanParts {
63            vtable: RowIdx,
64            dtype: row_idx_dtype(),
65            row_count,
66            children: PlanChildren::default(),
67            data: RowIdxData,
68        }
69        .into_typed()
70    }
71}
72
73/// Returns the dtype of a generated row index.
74pub fn row_idx_dtype() -> DType {
75    DType::Primitive(PType::U64, Nullability::NonNullable)
76}
77
78impl PlanVTable for RowIdx {
79    type PlanData = RowIdxData;
80    type Metadata = EmptyMetadata;
81
82    fn id(&self) -> PlanId {
83        static ID: CachedId = CachedId::new("vortex.plan.row_idx");
84        *ID
85    }
86
87    fn metadata(_plan: &Plan<Self>) -> Option<Self::Metadata> {
88        Some(EmptyMetadata)
89    }
90
91    fn with_children(
92        _plan: &Plan<Self>,
93        children: &PlanChildren,
94        _data: &mut Self::PlanData,
95    ) -> VortexResult<()> {
96        check_child_count("RowIdx", children, 0)
97    }
98
99    fn compile(
100        plan: &Plan<Self>,
101        rows: Range<u64>,
102        mask: &Mask,
103        compiler: &mut Compiler<'_>,
104    ) -> VortexResult<Option<Chain>> {
105        let _ = plan;
106        if mask.all_false() {
107            return Ok(None);
108        }
109        let offset = compiler.row_offset();
110        let indices = Buffer::from_iter(rows.start + offset..rows.end + offset).into_array();
111        let indices = if mask.all_true() {
112            indices
113        } else {
114            indices.filter(mask.clone())?
115        };
116        Ok(Some(Chain::new(OnceSource::new(indices))))
117    }
118
119    fn reach(
120        plan: &Plan<Self>,
121        rows: Range<u64>,
122        at: &Reach,
123        visit: &mut dyn FnMut(SegmentId, Range<u64>),
124    ) -> VortexResult<()> {
125        let _ = (plan, rows, at, visit);
126        Ok(())
127    }
128}
129
130/// Plans an expression over a data source and its global row-index domain.
131///
132/// Expressions that only use data bypass the row-index source, expressions that only use
133/// `#row_idx` bypass `child`, and mixed expressions combine independently planned branches with a
134/// [`PackPlan`]. The file row offset is supplied by the execution row domain.
135pub fn plan_row_idx_expression(
136    expression: BoundExpression,
137    child: PlanRef,
138) -> VortexResult<PlanRef> {
139    let partitioned = partition_bound(expression.clone(), |node| {
140        if node
141            .as_scalar()
142            .is_some_and(|scalar_fn| scalar_fn.is::<RowIdxFn>())
143        {
144            vec![RowIdxExpressionPartition::RowIdx]
145        } else if node.is_root() {
146            vec![RowIdxExpressionPartition::Child]
147        } else {
148            vec![]
149        }
150    })?;
151
152    if partitioned.partition_annotations.is_empty() {
153        let row_domain = PackPlan::try_new(
154            StructFields::empty(),
155            Nullability::NonNullable,
156            child.row_count(),
157            Vec::new(),
158            None,
159        )?
160        .into_plan();
161        return Ok(EvalPlan::try_new(expression, row_domain)?.into_plan());
162    }
163
164    if partitioned.partition_annotations.len() == 1 {
165        return match partitioned.partition_annotations[0] {
166            RowIdxExpressionPartition::RowIdx => {
167                let expression = replace_row_idx(expression)?;
168                let values = RowIdxPlan::new(child.row_count()).into_plan();
169                Ok(EvalPlan::try_new(expression, values)?.into_plan())
170            }
171            RowIdxExpressionPartition::Child => {
172                Ok(EvalPlan::try_new(expression, child)?.into_plan())
173            }
174        };
175    }
176
177    vortex_ensure_eq!(
178        partitioned.partition_annotations.len(),
179        2,
180        "Row-index expression produced more than two partitions"
181    );
182    let row_idx_index = partitioned
183        .partition_annotations
184        .iter()
185        .position(|partition| *partition == RowIdxExpressionPartition::RowIdx)
186        .ok_or_else(|| vortex_err!("Row-index expression has no row-index partition"))?;
187    let child_index = partitioned
188        .partition_annotations
189        .iter()
190        .position(|partition| *partition == RowIdxExpressionPartition::Child)
191        .ok_or_else(|| vortex_err!("Row-index expression has no data partition"))?;
192
193    let row_idx_partition = &partitioned.partitions[row_idx_index];
194    let child_partition = &partitioned.partitions[child_index];
195    let (Some(row_idx_pack), Some(child_pack)) = (
196        row_idx_partition
197            .as_scalar()
198            .and_then(|scalar_fn| scalar_fn.as_opt::<PackFn>()),
199        child_partition
200            .as_scalar()
201            .and_then(|scalar_fn| scalar_fn.as_opt::<PackFn>()),
202    ) else {
203        return Err(vortex_err!(
204            "Row-index expression partitions must be struct packs"
205        ));
206    };
207    let row_idx_partition_name = partitioned.partition_names[row_idx_index].clone();
208    let child_partition_name = partitioned.partition_names[child_index].clone();
209    let mut collapsed = Vec::with_capacity(2);
210
211    let row_idx_expression = if row_idx_partition.children().len() == 1 {
212        let Some(value_name) = row_idx_pack.names.get(0) else {
213            return Err(vortex_err!("Row-index expression partition is empty"));
214        };
215        collapsed.push((row_idx_partition_name, value_name.clone()));
216        row_idx_partition.children()[0].clone()
217    } else {
218        row_idx_partition.clone()
219    };
220    let child_expression = if child_partition.children().len() == 1 {
221        let Some(value_name) = child_pack.names.get(0) else {
222            return Err(vortex_err!("Data expression partition is empty"));
223        };
224        collapsed.push((child_partition_name, value_name.clone()));
225        child_partition.children()[0].clone()
226    } else {
227        child_partition.clone()
228    };
229
230    let row_count = child.row_count();
231    let row_idx_expression = replace_row_idx(row_idx_expression)?;
232    let row_idx_plan =
233        EvalPlan::try_new(row_idx_expression, RowIdxPlan::new(row_count).into_plan())?.into_plan();
234    let child_plan = EvalPlan::try_new(child_expression, child)?.into_plan();
235    let fields = StructFields::from_iter([
236        (ROW_IDX_PARTITION_NAME, row_idx_plan.dtype().clone()),
237        (CHILD_PARTITION_NAME, child_plan.dtype().clone()),
238    ]);
239    let partitions = PackPlan::try_new(
240        fields,
241        Nullability::NonNullable,
242        row_count,
243        vec![row_idx_plan, child_plan],
244        None,
245    )?;
246    let residual =
247        rewrite_partition_root(partitioned.root, partitions.dtype().clone(), &collapsed)?;
248
249    Ok(EvalPlan::try_new(residual, partitions.into_plan())?.into_plan())
250}
251
252fn replace_row_idx(expression: BoundExpression) -> VortexResult<BoundExpression> {
253    Ok(expression
254        .transform_down(|node| {
255            if node
256                .as_scalar()
257                .is_some_and(|scalar_fn| scalar_fn.is::<RowIdxFn>())
258            {
259                Ok(Transformed {
260                    value: BoundExpression::new_root(row_idx_dtype()),
261                    changed: true,
262                    order: TraversalOrder::Skip,
263                })
264            } else {
265                Ok(Transformed::no(node))
266            }
267        })?
268        .into_inner())
269}
270
271#[derive(Clone, Copy, PartialEq, Eq, Hash)]
272enum RowIdxExpressionPartition {
273    RowIdx,
274    Child,
275}
276
277impl RowIdxExpressionPartition {
278    fn name(self) -> &'static str {
279        match self {
280            Self::RowIdx => ROW_IDX_PARTITION_NAME,
281            Self::Child => CHILD_PARTITION_NAME,
282        }
283    }
284}
285
286impl Display for RowIdxExpressionPartition {
287    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
288        formatter.write_str(self.name())
289    }
290}
291
292impl From<RowIdxExpressionPartition> for FieldName {
293    fn from(partition: RowIdxExpressionPartition) -> Self {
294        FieldName::from(partition.name())
295    }
296}