Skip to content

Commit 1c79e68

Browse files
authored
feat: FieldPath::resolve, ::exists, and AccesPath::new (#3491)
For Spiral, mainly. --------- Signed-off-by: Daniel King <[email protected]>
1 parent d5a7560 commit 1c79e68

File tree

2 files changed

+206
-6
lines changed

2 files changed

+206
-6
lines changed

vortex-dtype/src/field.rs

Lines changed: 205 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
1-
//! Selectors for fields in (possibly nested) `StructDType`s
1+
//! Selectors for fields or elements in (possibly nested) `DType`s
22
//!
3-
//! A `Field` can either be a direct child field of the top-level struct (selected by name or index),
4-
//! or a nested field (selected by a sequence of such selectors)
3+
//! A `Field` indexes a single layer of `DType`, for example: a name in a struct or the element of a
4+
//! list. A `FieldPath` indexes zero or more layers, for example: the field "child" which is within
5+
//! the struct field "parent" which is within the struct field "grandparent".
56
67
use core::fmt;
78
use std::fmt::{Display, Formatter};
89
use std::sync::Arc;
910

1011
use itertools::Itertools;
12+
use vortex_error::{VortexResult, vortex_bail};
1113

12-
/// A selector for a field in a struct
14+
use crate::DType;
15+
16+
/// Selects a nested type within either a struct or a list.
1317
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1418
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1519
pub enum Field {
@@ -59,7 +63,19 @@ impl Field {
5963
}
6064
}
6165

62-
/// A path through a (possibly nested) struct, composed of a sequence of field selectors
66+
/// A sequence of field selectors representing a path through zero or more layers of `DType`.
67+
///
68+
/// # Examples
69+
///
70+
/// The empty path references the root:
71+
///
72+
/// ```
73+
/// use vortex_dtype::*;
74+
///
75+
/// let dtype_i32 = DType::Primitive(PType::I32, Nullability::NonNullable);
76+
/// assert_eq!(dtype_i32, FieldPath::root().resolve(dtype_i32.clone()).unwrap());
77+
/// ```
78+
///
6379
// TODO(ngates): we should probably reverse the path. Or better yet, store a Arc<[Field]> along
6480
// with a positional index to allow cheap step_into.
6581
#[derive(Clone, Default, Debug, PartialEq, Eq, Hash)]
@@ -110,6 +126,62 @@ impl FieldPath {
110126
self.0 = self.0.iter().skip(1).cloned().collect();
111127
Some(self)
112128
}
129+
130+
/// The dtype, within the given type, to which this field path refers.
131+
///
132+
/// Note that a nullable DType may contain a non-nullable DType. This function returns the
133+
/// literal nullability of the child.
134+
///
135+
/// # Examples
136+
///
137+
/// Extract the type of the "b" field from `struct{a: list(struct{b: u32})?}`:
138+
///
139+
/// ```
140+
/// use std::sync::Arc;
141+
///
142+
/// use vortex_dtype::*;
143+
/// use vortex_dtype::Nullability::*;
144+
///
145+
/// let dtype = DType::Struct(
146+
/// Arc::new(StructFields::from_iter([(
147+
/// "a",
148+
/// DType::List(
149+
/// Arc::new(DType::Struct(
150+
/// Arc::new(StructFields::from_iter([(
151+
/// "b",
152+
/// DType::Primitive(PType::U32, NonNullable),
153+
/// )])),
154+
/// NonNullable,
155+
/// )),
156+
/// Nullable,
157+
/// ),
158+
/// )])),
159+
/// NonNullable,
160+
/// );
161+
///
162+
/// let path = FieldPath::from(vec![Field::from("a"), Field::ElementType, Field::from("b")]);
163+
/// let resolved = path.resolve(dtype).unwrap();
164+
/// assert_eq!(resolved, DType::Primitive(PType::U32, NonNullable));
165+
/// ```
166+
pub fn resolve(&self, mut dtype: DType) -> VortexResult<DType> {
167+
for field in &self.0 {
168+
dtype = match (dtype, field) {
169+
(DType::Struct(fields, _), Field::Name(name)) => fields.field(name)?,
170+
(DType::List(element_dtype, _), Field::ElementType) => DType::clone(&element_dtype),
171+
(other, f) => {
172+
vortex_bail!("FieldPath: invalid index {:?} for DType {:?}", f, other)
173+
}
174+
}
175+
}
176+
177+
Ok(dtype)
178+
}
179+
180+
/// Does the field referenced by the field path exist in the given dtype?
181+
pub fn exists(&self, dtype: DType) -> bool {
182+
// Indexing a struct type always allocates anyway.
183+
self.resolve(dtype).is_ok()
184+
}
113185
}
114186

115187
impl FromIterator<Field> for FieldPath {
@@ -139,6 +211,8 @@ impl Display for FieldPath {
139211
#[cfg(test)]
140212
mod tests {
141213
use super::*;
214+
use crate::Nullability::*;
215+
use crate::{DType, PType, StructFields};
142216

143217
#[test]
144218
fn test_field_path() {
@@ -155,4 +229,130 @@ mod tests {
155229
assert_eq!(vec_path.to_string(), "$A.$B.$C");
156230
assert_eq!(path, vec_path);
157231
}
232+
233+
#[test]
234+
fn nested_field_single_level() {
235+
let a_type = DType::Primitive(PType::I32, NonNullable);
236+
let dtype = DType::Struct(
237+
Arc::from(StructFields::from_iter([
238+
("a", a_type.clone()),
239+
("b", DType::Bool(Nullable)),
240+
])),
241+
NonNullable,
242+
);
243+
let path = FieldPath::from_name("a");
244+
assert_eq!(a_type, path.resolve(dtype.clone()).unwrap());
245+
assert!(path.exists(dtype));
246+
}
247+
248+
#[test]
249+
fn nested_field_two_level() {
250+
let inner = DType::Struct(
251+
Arc::new(StructFields::from_iter([
252+
("inner_a", DType::Primitive(PType::U8, NonNullable)),
253+
("inner_b", DType::Bool(Nullable)),
254+
])),
255+
NonNullable,
256+
);
257+
258+
let outer = DType::Struct(
259+
Arc::from(StructFields::from_iter([
260+
("outer_a", DType::Bool(NonNullable)),
261+
("outer_b", inner),
262+
])),
263+
NonNullable,
264+
);
265+
266+
let path = FieldPath::from_name("outer_b").push("inner_a");
267+
let dtype = path.resolve(outer.clone()).unwrap();
268+
269+
assert_eq!(dtype, DType::Primitive(PType::U8, NonNullable));
270+
assert!(path.exists(outer));
271+
}
272+
273+
#[test]
274+
fn nested_field_deep_nested() {
275+
let level4 = DType::Struct(
276+
Arc::new(StructFields::from_iter([(
277+
"c",
278+
DType::Primitive(PType::F64, Nullable),
279+
)])),
280+
NonNullable,
281+
);
282+
283+
let level3 = DType::List(Arc::from(level4), Nullable);
284+
285+
let level2 = DType::Struct(
286+
Arc::new(StructFields::from_iter([("b", level3)])),
287+
NonNullable,
288+
);
289+
290+
let level1 = DType::Struct(
291+
Arc::from(StructFields::from_iter([("a", level2)])),
292+
NonNullable,
293+
);
294+
295+
let path = FieldPath::from_name("a")
296+
.push("b")
297+
.push(Field::ElementType)
298+
.push("c");
299+
let dtype = path.resolve(level1.clone()).unwrap();
300+
301+
assert_eq!(dtype, DType::Primitive(PType::F64, Nullable));
302+
assert!(path.exists(level1.clone()));
303+
304+
let path = FieldPath::from_name("a")
305+
.push("b")
306+
.push("c")
307+
.push(Field::ElementType);
308+
assert!(path.resolve(level1.clone()).is_err());
309+
assert!(!path.exists(level1.clone()));
310+
311+
let path = FieldPath::from_name("a")
312+
.push(Field::ElementType)
313+
.push("b")
314+
.push("c");
315+
assert!(path.resolve(level1.clone()).is_err());
316+
assert!(!path.exists(level1.clone()));
317+
318+
let path = FieldPath::from_name(Field::ElementType)
319+
.push("a")
320+
.push("b")
321+
.push("c");
322+
assert!(path.resolve(level1.clone()).is_err());
323+
assert!(!path.exists(level1));
324+
}
325+
326+
#[test]
327+
fn nested_field_not_found() {
328+
let dtype = DType::Struct(
329+
Arc::from(StructFields::from_iter([("a", DType::Bool(NonNullable))])),
330+
NonNullable,
331+
);
332+
let path = FieldPath::from_name("b");
333+
assert!(path.resolve(dtype.clone()).is_err());
334+
assert!(!path.exists(dtype.clone()));
335+
336+
let path = FieldPath::from(Field::ElementType);
337+
assert!(path.resolve(dtype.clone()).is_err());
338+
assert!(!path.exists(dtype));
339+
}
340+
341+
#[test]
342+
fn nested_field_non_struct_intermediate() {
343+
let dtype = DType::Struct(
344+
Arc::from(StructFields::from_iter([(
345+
"a",
346+
DType::Primitive(PType::I32, NonNullable),
347+
)])),
348+
NonNullable,
349+
);
350+
let path = FieldPath::from_name("a").push("b");
351+
assert!(path.resolve(dtype.clone()).is_err());
352+
assert!(!path.exists(dtype.clone()));
353+
354+
let path = FieldPath::from_name("a").push(Field::ElementType);
355+
assert!(path.resolve(dtype.clone()).is_err());
356+
assert!(!path.exists(dtype));
357+
}
158358
}

vortex-expr/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ impl AccessPath {
169169
}
170170
}
171171

172-
fn new(path: FieldPath, identifier: Identifier) -> Self {
172+
pub fn new(path: FieldPath, identifier: Identifier) -> Self {
173173
Self {
174174
field_path: path,
175175
identifier,

0 commit comments

Comments
 (0)