Skip to content

Commit 417f0f1

Browse files
committed
Add canonical Map arrays and builder
Signed-off-by: Adam Gutglick <adam@spiraldb.com>
1 parent 6889de1 commit 417f0f1

25 files changed

Lines changed: 1112 additions & 19 deletions

File tree

‎fuzz/src/array/fill_null.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ pub fn fill_null_canonical_array(
4747
}
4848
Canonical::Struct(_)
4949
| Canonical::List(_)
50+
| Canonical::Map(_)
5051
| Canonical::FixedSizeList(_)
5152
| Canonical::Extension(_) => canonical.into_array().fill_null(fill_value.clone())?,
5253
Canonical::Union(_) => {

‎fuzz/src/array/mask.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ pub fn mask_canonical_array(
153153
Canonical::Union(_) => {
154154
todo!("TODO(connor)[Union]: support Union arrays in the mask fuzzer")
155155
}
156+
Canonical::Map(_) => unreachable!("Map arrays are not fuzzed"),
156157
Canonical::Variant(_) => unreachable!("Variant arrays are not fuzzed"),
157158
})
158159
}

‎fuzz/src/array/scalar_at.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ pub fn scalar_at_canonical_array(
107107
Canonical::Union(_) => {
108108
todo!("TODO(connor)[Union]: support Union arrays in the scalar_at fuzzer")
109109
}
110+
Canonical::Map(_) => unreachable!("Map arrays are not fuzzed"),
110111
Canonical::Variant(_) => unreachable!("Variant arrays are not fuzzed"),
111112
})
112113
}

‎vortex-array/src/aggregate_fn/fns/is_constant/mod.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,9 @@ impl AggregateFnVTable for IsConstant {
402402
Canonical::Struct(s) => check_struct_constant(s, ctx)?,
403403
Canonical::Extension(e) => check_extension_constant(e, ctx)?,
404404
Canonical::List(l) => check_listview_constant(l, ctx)?,
405+
Canonical::Map(_) => {
406+
vortex_bail!("Map arrays don't support IsConstant")
407+
}
405408
Canonical::FixedSizeList(f) => check_fixed_size_list_constant(f, ctx)?,
406409
Canonical::Null(_) => true,
407410
Canonical::Union(_) => {

‎vortex-array/src/aggregate_fn/fns/min_max/mod.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,7 @@ impl AggregateFnVTable for MinMax {
422422
}
423423
Canonical::Struct(_)
424424
| Canonical::List(_)
425+
| Canonical::Map(_)
425426
| Canonical::FixedSizeList(_)
426427
| Canonical::Variant(_) => {
427428
vortex_bail!("Unsupported canonical type for min_max: {}", batch.dtype())

‎vortex-array/src/aggregate_fn/fns/uncompressed_size_in_bytes/mod.rs‎

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ use crate::aggregate_fn::EmptyOptions;
4545
use crate::array::ArrayView;
4646
use crate::arrays::Constant;
4747
use crate::arrays::ConstantArray;
48+
use crate::arrays::map::MapArrayExt;
4849
use crate::arrays::varbinview::BinaryView;
4950
use crate::dtype::DType;
5051
use crate::dtype::DecimalType;
@@ -199,6 +200,9 @@ pub(crate) fn canonical_uncompressed_size_in_bytes(
199200
Canonical::Decimal(array) => decimal_uncompressed_size_in_bytes(array, ctx),
200201
Canonical::VarBinView(array) => varbinview_uncompressed_size_in_bytes(array, ctx),
201202
Canonical::List(array) => list_view_uncompressed_size_in_bytes(array, ctx),
203+
Canonical::Map(array) => {
204+
list_view_uncompressed_size_in_bytes(&array.entries().into_owned(), ctx)
205+
}
202206
Canonical::FixedSizeList(array) => fixed_size_list_uncompressed_size_in_bytes(array, ctx),
203207
Canonical::Struct(array) => struct_uncompressed_size_in_bytes(array, ctx),
204208
Canonical::Union(array) => union_uncompressed_size_in_bytes(array, ctx),
@@ -232,13 +236,14 @@ pub(crate) fn constant_uncompressed_size_in_bytes(
232236
array.len(),
233237
array.scalar().as_binary().value().map(|value| value.len()),
234238
)?,
235-
DType::List(..) | DType::FixedSizeList(..) | DType::Struct(..) | DType::Extension(_) => {
239+
DType::List(..)
240+
| DType::Map(..)
241+
| DType::FixedSizeList(..)
242+
| DType::Struct(..)
243+
| DType::Extension(_) => {
236244
let canonical = array.array().clone().execute::<Canonical>(ctx)?;
237245
return canonical_uncompressed_size_in_bytes(&canonical, ctx);
238246
}
239-
DType::Map(..) => {
240-
vortex_bail!("UncompressedSizeInBytes is not supported for map arrays yet")
241-
}
242247
DType::Union(..) => {
243248
todo!(
244249
"TODO(connor)[Union]: support constant Union size accounting after constant Union \
@@ -297,7 +302,10 @@ fn supports_uncompressed_size_in_bytes(dtype: &DType) -> bool {
297302
DType::List(element_dtype, _) | DType::FixedSizeList(element_dtype, ..) => {
298303
supports_uncompressed_size_in_bytes(element_dtype)
299304
}
300-
DType::Map(..) => false,
305+
DType::Map(map_dtype, _) => {
306+
supports_uncompressed_size_in_bytes(&map_dtype.key_dtype())
307+
&& supports_uncompressed_size_in_bytes(&map_dtype.value_dtype())
308+
}
301309
DType::Struct(fields, _) => fields
302310
.fields()
303311
.all(|field| supports_uncompressed_size_in_bytes(&field)),

‎vortex-array/src/arrays/constant/vtable/canonical.rs‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use crate::arrays::DecimalArray;
2020
use crate::arrays::ExtensionArray;
2121
use crate::arrays::FixedSizeListArray;
2222
use crate::arrays::ListViewArray;
23+
use crate::arrays::MapArray;
2324
use crate::arrays::NullArray;
2425
use crate::arrays::PrimitiveArray;
2526
use crate::arrays::StructArray;
@@ -126,7 +127,16 @@ pub(crate) fn constant_canonicalize(
126127
))
127128
}
128129
DType::List(..) => Canonical::List(constant_canonical_list_array(scalar, array.len())),
129-
DType::Map(..) => vortex_error::vortex_bail!("canonical map arrays are not yet supported"),
130+
DType::Map(map_dtype, nullability) => {
131+
let entries_scalar = Scalar::try_new(
132+
DType::List(Arc::new(map_dtype.entries_dtype()), *nullability),
133+
scalar.value().cloned(),
134+
)?;
135+
Canonical::Map(MapArray::try_new(
136+
map_dtype.clone(),
137+
constant_canonical_list_array(&entries_scalar, array.len()),
138+
)?)
139+
}
130140
DType::FixedSizeList(element_dtype, list_size, _) => {
131141
let value = scalar.as_list();
132142

‎vortex-array/src/arrays/dict/execute.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ pub(crate) fn take_canonical(
4949
Canonical::Decimal(a) => Canonical::Decimal(take_decimal(&a, codes, ctx)),
5050
Canonical::VarBinView(a) => Canonical::VarBinView(take_varbinview(&a, codes, ctx)),
5151
Canonical::List(a) => Canonical::List(take_listview(&a, codes, ctx)),
52+
Canonical::Map(_) => vortex_error::vortex_bail!("Map arrays don't support take"),
5253
Canonical::FixedSizeList(a) => {
5354
Canonical::FixedSizeList(take_fixed_size_list(&a, codes, ctx))
5455
}

‎vortex-array/src/arrays/filter/execute/mod.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use std::sync::Arc;
99

1010
use vortex_error::VortexExpect;
1111
use vortex_error::VortexResult;
12+
use vortex_error::vortex_panic;
1213
use vortex_mask::Mask;
1314
use vortex_mask::MaskValues;
1415

@@ -95,6 +96,7 @@ pub(super) fn execute_filter(canonical: Canonical, mask: &Arc<MaskValues>) -> Ca
9596
Canonical::Decimal(a) => Canonical::Decimal(decimal::filter_decimal(&a, mask)),
9697
Canonical::VarBinView(a) => Canonical::VarBinView(varbinview::filter_varbinview(&a, mask)),
9798
Canonical::List(a) => Canonical::List(listview::filter_listview(&a, mask)),
99+
Canonical::Map(_) => vortex_panic!("Map arrays don't support filter"),
98100
Canonical::FixedSizeList(a) => {
99101
Canonical::FixedSizeList(fixed_size_list::filter_fixed_size_list(&a, mask))
100102
}
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
use std::fmt::Display;
5+
use std::fmt::Formatter;
6+
use std::hash::Hasher;
7+
use std::sync::Arc;
8+
9+
use smallvec::smallvec;
10+
use vortex_error::VortexExpect;
11+
use vortex_error::VortexResult;
12+
use vortex_error::vortex_ensure;
13+
14+
use crate::ArrayEq;
15+
use crate::ArrayHash;
16+
use crate::ArrayRef;
17+
use crate::EqMode;
18+
use crate::IntoArray;
19+
use crate::array::Array;
20+
use crate::array::ArrayParts;
21+
use crate::array::ArrayView;
22+
use crate::array::TypedArrayRef;
23+
use crate::arrays::ListView;
24+
use crate::arrays::ListViewArray;
25+
use crate::arrays::listview::ListViewArrayExt;
26+
use crate::arrays::map::Map;
27+
use crate::dtype::DType;
28+
use crate::dtype::MapDType;
29+
use crate::validity::Validity;
30+
31+
/// The one child slot holding a [`ListViewArray`] of map entries.
32+
pub(super) const ENTRIES_SLOT: usize = 0;
33+
pub(super) const NUM_SLOTS: usize = 1;
34+
pub(super) const SLOT_NAMES: [&str; NUM_SLOTS] = ["entries"];
35+
36+
/// Encoding-specific metadata for [`crate::arrays::MapArray`].
37+
///
38+
/// All map metadata is represented by the outer [`DType::Map`] and the entries child, so this
39+
/// value is intentionally empty.
40+
#[derive(Clone, Debug, Default)]
41+
pub struct MapData;
42+
43+
impl Display for MapData {
44+
fn fmt(&self, _f: &mut Formatter<'_>) -> std::fmt::Result {
45+
Ok(())
46+
}
47+
}
48+
49+
impl ArrayEq for MapData {
50+
fn array_eq(&self, _other: &Self, _accuracy: EqMode) -> bool {
51+
true
52+
}
53+
}
54+
55+
impl ArrayHash for MapData {
56+
fn array_hash<H: Hasher>(&self, _state: &mut H, _accuracy: EqMode) {}
57+
}
58+
59+
/// The logical and physical inputs used to construct a [`crate::arrays::MapArray`].
60+
pub struct MapDataParts {
61+
/// The key/value type and sortedness assertion for the map.
62+
pub map_dtype: MapDType,
63+
/// The physical list-view storage of `{key, value}` entry structs.
64+
pub entries: ListViewArray,
65+
}
66+
67+
/// Accessors for the canonical map representation.
68+
pub trait MapArrayExt: TypedArrayRef<Map> {
69+
/// Returns the list-view storage of map entry structs.
70+
fn entries(&self) -> ArrayView<'_, ListView> {
71+
self.as_ref().slots()[ENTRIES_SLOT]
72+
.as_ref()
73+
.vortex_expect("MapArray entries slot")
74+
.as_::<ListView>()
75+
}
76+
77+
/// Returns the entry structs for one map row.
78+
fn entries_at(&self, index: usize) -> VortexResult<ArrayRef> {
79+
self.entries().list_elements_at(index)
80+
}
81+
82+
/// Returns the number of entries in one map row.
83+
fn entry_count_at(&self, index: usize) -> usize {
84+
self.entries().size_at(index)
85+
}
86+
87+
/// Returns the outer map validity delegated from the entries list-view.
88+
fn map_validity(&self) -> Validity {
89+
self.entries().listview_validity()
90+
}
91+
92+
/// Returns this map's key/value type information.
93+
fn map_dtype(&self) -> &MapDType {
94+
self.as_ref()
95+
.dtype()
96+
.as_map_opt()
97+
.vortex_expect("MapArray requires a map dtype")
98+
}
99+
100+
/// Returns whether producers assert sorted keys within each map value.
101+
fn keys_sorted(&self) -> bool {
102+
self.map_dtype().keys_sorted()
103+
}
104+
}
105+
impl<T: TypedArrayRef<Map>> MapArrayExt for T {}
106+
107+
impl Array<Map> {
108+
/// Creates a canonical map array from its map dtype and list-view entry storage.
109+
///
110+
/// # Panics
111+
///
112+
/// Panics if `entries` is not a list of the map dtype's non-nullable `{key, value}` entry
113+
/// struct with matching outer nullability.
114+
pub fn new(map_dtype: MapDType, entries: ListViewArray) -> Self {
115+
Self::try_new(map_dtype, entries).vortex_expect("MapArray construction failed")
116+
}
117+
118+
/// Constructs a canonical map array from its map dtype and list-view entry storage.
119+
///
120+
/// # Errors
121+
///
122+
/// Returns an error when the entry child is not `ListView<Struct<key, value>>`, has a
123+
/// different outer nullability, or has a different length than the outer map array.
124+
pub fn try_new(map_dtype: MapDType, entries: ListViewArray) -> VortexResult<Self> {
125+
let nullability = entries.nullability();
126+
let dtype = DType::Map(map_dtype, nullability);
127+
let len = entries.len();
128+
let parts = ArrayParts::new(Map, dtype, len, MapData)
129+
.with_slots(smallvec![Some(entries.into_array())]);
130+
Self::try_from_parts(parts)
131+
}
132+
133+
/// Creates a canonical map array without validating its entry storage.
134+
///
135+
/// # Safety
136+
///
137+
/// The caller must ensure that `entries` has dtype
138+
/// `List(Struct { key, value }, entries.nullability())`, where the struct exactly matches
139+
/// `map_dtype.entries_dtype()`.
140+
pub unsafe fn new_unchecked(map_dtype: MapDType, entries: ListViewArray) -> Self {
141+
let nullability = entries.nullability();
142+
let dtype = DType::Map(map_dtype, nullability);
143+
let len = entries.len();
144+
let parts = ArrayParts::new(Map, dtype, len, MapData)
145+
.with_slots(smallvec![Some(entries.into_array())]);
146+
unsafe { Self::from_parts_unchecked(parts) }
147+
}
148+
149+
/// Decomposes this map array into its logical dtype and physical entries child.
150+
pub fn into_data_parts(self) -> MapDataParts {
151+
let map_dtype = self
152+
.dtype()
153+
.as_map_opt()
154+
.vortex_expect("MapArray requires a map dtype")
155+
.clone();
156+
let entries = self.entries().into_owned();
157+
MapDataParts { map_dtype, entries }
158+
}
159+
}
160+
161+
fn expected_entries_dtype(map_dtype: &MapDType, nullability: crate::dtype::Nullability) -> DType {
162+
DType::List(Arc::new(map_dtype.entries_dtype()), nullability)
163+
}
164+
165+
pub(super) fn validate_entries(
166+
map_dtype: &MapDType,
167+
nullability: crate::dtype::Nullability,
168+
len: usize,
169+
entries: &ArrayRef,
170+
) -> VortexResult<()> {
171+
vortex_ensure!(
172+
entries.is::<ListView>(),
173+
"MapArray entries must use vortex.listview encoding, got {}",
174+
entries.encoding_id()
175+
);
176+
vortex_ensure!(
177+
entries.len() == len,
178+
"MapArray entries length {} does not match outer length {len}",
179+
entries.len()
180+
);
181+
182+
let expected_dtype = expected_entries_dtype(map_dtype, nullability);
183+
vortex_ensure!(
184+
entries.dtype() == &expected_dtype,
185+
"MapArray entries dtype {} does not match expected {expected_dtype}",
186+
entries.dtype()
187+
);
188+
189+
Ok(())
190+
}

0 commit comments

Comments
 (0)