use vortex_error::VortexResult;
use crate::ArrayEq;
use crate::ArrayRef;
use crate::EqMode;
use crate::IntoArray;
use crate::array::ArrayView;
use crate::array::VTable;
use crate::arrays::Chunked;
use crate::arrays::ChunkedArray;
use crate::arrays::Constant;
use crate::arrays::ConstantArray;
use crate::arrays::Dict;
use crate::arrays::DictArray;
use crate::arrays::ScalarFn;
use crate::arrays::ScalarFnArray;
use crate::arrays::chunked::ChunkedArrayExt;
use crate::arrays::dict::DictArrayExt;
use crate::arrays::dict::DictArraySlotsExt;
use crate::arrays::filter::FilterReduceAdaptor;
use crate::arrays::scalar_fn::AnyScalarFn;
use crate::arrays::scalar_fn::ScalarFnArrayExt;
use crate::arrays::slice::SliceReduceAdaptor;
use crate::builtins::ArrayBuiltins;
use crate::optimizer::ArrayOptimizer;
use crate::optimizer::rules::ArrayParentReduceRule;
use crate::optimizer::rules::ParentRuleSet;
use crate::scalar_fn::fns::cast::Cast;
use crate::scalar_fn::fns::cast::CastReduceAdaptor;
use crate::scalar_fn::fns::like::LikeReduceAdaptor;
use crate::scalar_fn::fns::mask::MaskReduceAdaptor;
use crate::scalar_fn::fns::pack::Pack;
use crate::validity::Validity;
pub(crate) const PARENT_RULES: ParentRuleSet<Dict> = ParentRuleSet::new(&[
ParentRuleSet::lift(&FilterReduceAdaptor(Dict)),
ParentRuleSet::lift(&CastReduceAdaptor(Dict)),
ParentRuleSet::lift(&MaskReduceAdaptor(Dict)),
ParentRuleSet::lift(&LikeReduceAdaptor(Dict)),
ParentRuleSet::lift(&DictionaryChunkedValuesPullUpRule),
ParentRuleSet::lift(&DictionaryScalarFnValuesPushDownRule),
ParentRuleSet::lift(&DictionaryScalarFnCodesPullUpRule),
ParentRuleSet::lift(&SliceReduceAdaptor(Dict)),
]);
#[derive(Debug)]
struct DictionaryChunkedValuesPullUpRule;
impl ArrayParentReduceRule<Dict> for DictionaryChunkedValuesPullUpRule {
type Parent = Chunked;
fn reduce_parent(
&self,
array: ArrayView<'_, Dict>,
parent: ArrayView<'_, Chunked>,
_child_idx: usize,
) -> VortexResult<Option<ArrayRef>> {
let values = array.values();
let codes_dtype = array.codes().dtype().clone();
let mut code_chunks = Vec::with_capacity(parent.nchunks());
let mut all_values_referenced = array.has_all_values_referenced();
for chunk in parent.iter_chunks() {
let Some(dict) = chunk.as_opt::<Dict>() else {
return Ok(None);
};
if dict.codes().dtype() != &codes_dtype {
return Ok(None);
}
if !ArrayRef::ptr_eq(dict.values(), values) {
return Ok(None);
}
all_values_referenced |= dict.has_all_values_referenced();
code_chunks.push(dict.codes().clone());
}
let codes = ChunkedArray::try_new(code_chunks, codes_dtype)?.into_array();
let dict = DictArray::try_new(codes, values.clone())?;
let dict = if all_values_referenced {
unsafe { dict.set_all_values_referenced(true) }
} else {
dict
};
Ok(Some(dict.into_array()))
}
}
#[derive(Debug)]
struct DictionaryScalarFnValuesPushDownRule;
impl ArrayParentReduceRule<Dict> for DictionaryScalarFnValuesPushDownRule {
type Parent = AnyScalarFn;
fn reduce_parent(
&self,
array: ArrayView<'_, Dict>,
parent: ArrayView<'_, ScalarFn>,
child_idx: usize,
) -> VortexResult<Option<ArrayRef>> {
let sig = parent.scalar_fn().signature();
if parent.scalar_fn().is::<Pack>() {
return Ok(None);
}
if parent.scalar_fn().is::<Cast>() {
return Ok(None);
}
if array.values().len() > array.codes().len() {
return Ok(None);
}
if !array.all_values_referenced && sig.is_fallible() {
tracing::trace!(
"Not pushing down fallible scalar function {} over dictionary with sparse codes {}",
parent.scalar_fn(),
Dict.id(),
);
return Ok(None);
}
if !parent
.iter_children()
.enumerate()
.all(|(idx, c)| idx == child_idx || c.is::<Constant>())
{
return Ok(None);
}
if array.codes().dtype().is_nullable()
&& !matches!(
array.codes().validity()?,
Validity::NonNullable | Validity::AllValid
)
&& sig.is_null_sensitive()
{
tracing::trace!(
"Not pushing down null-sensitive scalar function {} over dictionary with null codes {}",
parent.scalar_fn(),
Dict.id(),
);
return Ok(None);
}
let values_len = array.values().len();
let mut new_children = Vec::with_capacity(parent.nchildren());
for (idx, child) in parent.iter_children().enumerate() {
if idx == child_idx {
new_children.push(array.values().clone());
} else {
let scalar = child.as_::<Constant>().scalar().clone();
new_children.push(ConstantArray::new(scalar, values_len).into_array());
}
}
let new_values = ScalarFnArray::try_new(parent.scalar_fn().clone(), new_children)?
.into_array()
.optimize()?;
if sig.is_null_sensitive() && array.codes().dtype().is_nullable() {
let new_codes = array.codes().cast(array.codes().dtype().as_nonnullable())?;
let new_dict = unsafe {
DictArray::new_unchecked(new_codes, new_values)
.set_all_values_referenced(array.has_all_values_referenced())
}
.into_array();
return Ok(Some(new_dict.cast(parent.dtype().clone())?));
}
Ok(Some(unsafe {
DictArray::new_unchecked(array.codes().clone(), new_values)
.set_all_values_referenced(array.has_all_values_referenced())
.into_array()
}))
}
}
#[derive(Debug)]
struct DictionaryScalarFnCodesPullUpRule;
impl ArrayParentReduceRule<Dict> for DictionaryScalarFnCodesPullUpRule {
type Parent = AnyScalarFn;
fn reduce_parent(
&self,
array: ArrayView<'_, Dict>,
parent: ArrayView<'_, ScalarFn>,
child_idx: usize,
) -> VortexResult<Option<ArrayRef>> {
if parent.nchildren() < 2 {
return Ok(None);
}
if !parent.iter_children().enumerate().all(|(idx, c)| {
idx == child_idx
|| c.as_opt::<Dict>()
.is_some_and(|c| c.values().len() == array.values().len())
}) {
return Ok(None);
}
if !parent.iter_children().enumerate().all(|(idx, c)| {
idx == child_idx
|| c.as_opt::<Dict>()
.is_some_and(|c| c.codes().array_eq(array.codes(), EqMode::Value))
}) {
return Ok(None);
}
let mut new_children = Vec::with_capacity(parent.nchildren());
for (idx, child) in parent.iter_children().enumerate() {
if idx == child_idx {
new_children.push(array.values().clone());
} else {
new_children.push(child.as_::<Dict>().values().clone());
}
}
let new_values = ScalarFnArray::try_new(parent.scalar_fn().clone(), new_children)?
.into_array()
.optimize()?;
let new_dict =
unsafe { DictArray::new_unchecked(array.codes().clone(), new_values) }.into_array();
Ok(Some(new_dict))
}
}
#[cfg(test)]
mod tests {
use vortex_buffer::buffer;
use vortex_error::VortexResult;
use crate::ArrayRef;
use crate::IntoArray;
use crate::arrays::BoolArray;
use crate::arrays::Chunked;
use crate::arrays::ChunkedArray;
use crate::arrays::Dict;
use crate::arrays::DictArray;
use crate::arrays::PrimitiveArray;
use crate::arrays::chunked::ChunkedArrayExt;
use crate::arrays::dict::DictArrayExt;
use crate::arrays::dict::DictArraySlotsExt;
use crate::arrays::scalar_fn::ScalarFnFactoryExt;
use crate::assert_arrays_eq;
use crate::executor::VortexSessionExecute;
use crate::optimizer::ArrayOptimizer;
use crate::scalar_fn::EmptyOptions;
use crate::scalar_fn::fns::not::Not;
#[test]
fn chunked_dict_with_shared_values_pulls_values_up() -> VortexResult<()> {
let values = buffer![10u32, 20, 30].into_array();
let chunk0 = DictArray::try_new(buffer![0u8, 1].into_array(), values.clone())?.into_array();
let chunk1 =
DictArray::try_new(buffer![2u8, 0, 1].into_array(), values.clone())?.into_array();
let array =
ChunkedArray::try_new(vec![chunk0, chunk1], values.dtype().clone())?.into_array();
let optimized = array.optimize()?;
let dict = optimized.as_::<Dict>();
let codes = dict.codes().as_::<Chunked>();
assert!(ArrayRef::ptr_eq(dict.values(), &values));
assert_eq!(codes.nchunks(), 2);
let mut ctx = crate::LEGACY_SESSION.create_execution_ctx();
assert_arrays_eq!(
optimized,
PrimitiveArray::from_iter([10u32, 20, 30, 10, 20]),
&mut ctx
);
Ok(())
}
#[test]
fn chunked_dict_with_distinct_values_stays_chunked() -> VortexResult<()> {
let values0 = buffer![10u32, 20, 30].into_array();
let values1 = buffer![10u32, 20, 30].into_array();
let chunk0 =
DictArray::try_new(buffer![0u8, 1].into_array(), values0.clone())?.into_array();
let chunk1 = DictArray::try_new(buffer![2u8, 0, 1].into_array(), values1)?.into_array();
let array =
ChunkedArray::try_new(vec![chunk0, chunk1], values0.dtype().clone())?.into_array();
let optimized = array.optimize()?;
assert!(optimized.is::<Chunked>());
let mut ctx = crate::LEGACY_SESSION.create_execution_ctx();
assert_arrays_eq!(
optimized,
PrimitiveArray::from_iter([10u32, 20, 30, 10, 20]),
&mut ctx
);
Ok(())
}
#[test]
fn scalar_fn_values_pushdown_preserves_all_values_referenced() -> VortexResult<()> {
let dict = unsafe {
DictArray::try_new(
buffer![0u8, 1, 0, 1].into_array(),
BoolArray::from_iter([true, false]).into_array(),
)?
.set_all_values_referenced(true)
}
.into_array();
let result = Not
.try_new_array(dict.len(), EmptyOptions, [dict])?
.optimize()?;
let result = result.as_::<Dict>();
assert!(result.has_all_values_referenced());
Ok(())
}
}