1use std::fmt;
5use std::fmt::Display;
6use std::fmt::Formatter;
7
8use prost::Message;
9use vortex_error::VortexResult;
10use vortex_error::vortex_ensure;
11use vortex_error::vortex_err;
12use vortex_proto::expr as pb;
13use vortex_proto::expr::variant_path_element;
14use vortex_session::VortexSession;
15use vortex_session::registry::CachedId;
16use vortex_utils::aliases::StringEscape;
17
18use crate::ArrayRef;
19use crate::ExecutionCtx;
20use crate::IntoArray;
21use crate::arrays::ChunkedArray;
22use crate::arrays::ConstantArray;
23use crate::arrays::VariantArray;
24use crate::builders::builder_with_capacity_in;
25use crate::dtype::DType;
26use crate::dtype::FieldName;
27use crate::dtype::Nullability;
28use crate::expr::display::ExprDisplay;
29use crate::scalar::Scalar;
30use crate::scalar_fn::Arity;
31use crate::scalar_fn::ChildName;
32use crate::scalar_fn::ExecutionArgs;
33use crate::scalar_fn::ScalarFnId;
34use crate::scalar_fn::ScalarFnVTable;
35
36#[derive(Clone)]
43pub struct VariantGet;
44
45impl ScalarFnVTable for VariantGet {
46 type Options = VariantGetOptions;
47
48 fn id(&self) -> ScalarFnId {
49 static ID: CachedId = CachedId::new("vortex.variant_get");
50 *ID
51 }
52
53 fn serialize(&self, options: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
54 let path = options
55 .path()
56 .elements()
57 .iter()
58 .map(VariantPathElement::to_proto)
59 .collect();
60 let dtype = options.dtype().map(TryInto::try_into).transpose()?;
61
62 Ok(Some(pb::VariantGetOpts { path, dtype }.encode_to_vec()))
63 }
64
65 fn deserialize(&self, metadata: &[u8], session: &VortexSession) -> VortexResult<Self::Options> {
66 let opts = pb::VariantGetOpts::decode(metadata)?;
67 let path = opts
68 .path
69 .into_iter()
70 .map(VariantPathElement::from_proto)
71 .collect::<VortexResult<_>>()?;
72 let dtype = opts
73 .dtype
74 .as_ref()
75 .map(|dtype| DType::from_proto(dtype, session))
76 .transpose()?;
77
78 Ok(VariantGetOptions::new(path, dtype))
79 }
80
81 fn arity(&self, _options: &Self::Options) -> Arity {
82 Arity::Exact(1)
83 }
84
85 fn child_name(&self, _options: &Self::Options, child_idx: usize) -> ChildName {
86 match child_idx {
87 0 => ChildName::from("input"),
88 _ => unreachable!("Invalid child index {child_idx} for VariantGet expression"),
89 }
90 }
91
92 fn fmt_sql(
93 &self,
94 options: &Self::Options,
95 expr: &dyn ExprDisplay,
96 f: &mut Formatter<'_>,
97 ) -> fmt::Result {
98 write!(f, "variant_get(")?;
99 Display::fmt(expr.display_child(0), f)?;
100 let path = options.path().to_string();
101 write!(f, ", \"{}\"", StringEscape(&path))?;
102 if let Some(dtype) = options.dtype() {
103 write!(f, ", {dtype}")?;
104 }
105 write!(f, ")")
106 }
107
108 fn return_dtype(&self, options: &Self::Options, arg_dtypes: &[DType]) -> VortexResult<DType> {
109 let input_dtype = &arg_dtypes[0];
110 vortex_ensure!(
111 matches!(input_dtype, DType::Variant(_)),
112 "VariantGet input must be Variant, found {input_dtype}"
113 );
114
115 Ok(options
117 .dtype()
118 .map_or(DType::Variant(Nullability::Nullable), DType::as_nullable))
119 }
120
121 fn execute(
122 &self,
123 options: &Self::Options,
124 args: &dyn ExecutionArgs,
125 ctx: &mut ExecutionCtx,
126 ) -> VortexResult<ArrayRef> {
127 let input = args.get(0)?;
128 let dtype = options
130 .dtype()
131 .map_or(DType::Variant(Nullability::Nullable), DType::as_nullable);
132
133 if !dtype.is_variant() {
134 let mut builder = builder_with_capacity_in(ctx.allocator(), &dtype, input.len());
135 for idx in 0..input.len() {
136 let scalar = input.execute_scalar(idx, ctx)?;
137 let output = variant_get_scalar(&scalar, options, &dtype)?;
138 builder.append_scalar(&output)?;
139 }
140
141 return Ok(builder.finish());
142 }
143
144 let mut chunks = Vec::with_capacity(input.len());
147
148 for idx in 0..input.len() {
149 let scalar = input.execute_scalar(idx, ctx)?;
150 let output = variant_get_scalar(&scalar, options, &dtype)?;
151 chunks.push(ConstantArray::new(output, 1).into_array());
152 }
153
154 let array = ChunkedArray::try_new(chunks, dtype)?.into_array();
155 VariantArray::try_new(array, None).map(|array| array.into_array())
156 }
157
158 fn is_strict(&self, _options: &Self::Options) -> bool {
159 true
160 }
161}
162
163fn variant_get_scalar(
164 scalar: &Scalar,
165 options: &VariantGetOptions,
166 output_dtype: &DType,
167) -> VortexResult<Scalar> {
168 let Some(value) = variant_path_scalar(scalar, options.path().elements())? else {
169 return Ok(Scalar::null(output_dtype.clone()));
170 };
171
172 if options.dtype().is_none_or(DType::is_variant) {
173 return Scalar::variant(value).cast(output_dtype);
174 }
175
176 if value.is_null() {
177 return Ok(Scalar::null(output_dtype.clone()));
178 }
179
180 value
181 .cast(output_dtype)
182 .or_else(|_| Ok(Scalar::null(output_dtype.clone())))
183}
184
185fn variant_path_scalar(
186 scalar: &Scalar,
187 path: &[VariantPathElement],
188) -> VortexResult<Option<Scalar>> {
189 let mut current = match variant_payload(scalar.clone()) {
190 Some(value) => value,
191 None => return Ok(None),
192 };
193
194 for element in path {
195 current = match variant_payload(current) {
196 Some(value) => value,
197 None => return Ok(None),
198 };
199
200 if current.is_null() {
201 return Ok(None);
202 }
203
204 current = match element {
205 VariantPathElement::Field(name) => {
206 let Some(struct_scalar) = current.as_struct_opt() else {
207 return Ok(None);
208 };
209 if struct_scalar.is_null() {
210 return Ok(None);
211 }
212 let Some(field) = struct_scalar.field(name.as_ref()) else {
213 return Ok(None);
214 };
215 field
216 }
217 VariantPathElement::Index(index) => {
218 let Ok(index) = usize::try_from(*index) else {
219 return Ok(None);
220 };
221 let Some(list_scalar) = current.as_list_opt() else {
222 return Ok(None);
223 };
224 let Some(element) = list_scalar.element(index) else {
225 return Ok(None);
226 };
227 element
228 }
229 };
230 }
231
232 Ok(variant_payload(current))
233}
234
235fn variant_payload(scalar: Scalar) -> Option<Scalar> {
236 if scalar.dtype().is_variant() {
237 scalar.as_variant().value().cloned()
238 } else {
239 Some(scalar)
240 }
241}
242
243#[derive(Clone, Debug, PartialEq, Eq, Hash)]
245pub struct VariantGetOptions {
246 path: VariantPath,
247 dtype: Option<DType>,
248}
249
250impl VariantGetOptions {
251 pub fn new(path: VariantPath, dtype: Option<DType>) -> Self {
253 Self { path, dtype }
254 }
255
256 pub fn path(&self) -> &VariantPath {
258 &self.path
259 }
260
261 pub fn dtype(&self) -> Option<&DType> {
263 self.dtype.as_ref()
264 }
265}
266
267impl Display for VariantGetOptions {
268 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
269 write!(f, "{}", self.path)?;
270 if let Some(dtype) = &self.dtype {
271 write!(f, " as {dtype}")?;
272 }
273 Ok(())
274 }
275}
276
277#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
279pub struct VariantPath(Vec<VariantPathElement>);
280
281impl VariantPath {
282 pub fn new(elements: impl IntoIterator<Item = VariantPathElement>) -> Self {
284 Self(elements.into_iter().collect())
285 }
286
287 pub fn root() -> Self {
289 Self::default()
290 }
291
292 pub fn field(field: impl Into<FieldName>) -> Self {
294 Self(vec![VariantPathElement::field(field)])
295 }
296
297 pub fn elements(&self) -> &[VariantPathElement] {
299 &self.0
300 }
301
302 pub fn is_root(&self) -> bool {
304 self.0.is_empty()
305 }
306}
307
308impl FromIterator<VariantPathElement> for VariantPath {
309 fn from_iter<T: IntoIterator<Item = VariantPathElement>>(iter: T) -> Self {
310 Self(iter.into_iter().collect())
311 }
312}
313
314impl From<VariantPathElement> for VariantPath {
315 fn from(value: VariantPathElement) -> Self {
316 Self(vec![value])
317 }
318}
319
320impl Display for VariantPath {
321 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
322 write!(f, "$")?;
323 for element in self.elements() {
324 match element {
325 VariantPathElement::Field(name) => write!(f, ".{name}")?,
326 VariantPathElement::Index(index) => write!(f, "[{index}]")?,
327 }
328 }
329 Ok(())
330 }
331}
332
333#[derive(Clone, Debug, PartialEq, Eq, Hash)]
335pub enum VariantPathElement {
336 Field(FieldName),
338 Index(u64),
340}
341
342impl VariantPathElement {
343 pub fn field(field: impl Into<FieldName>) -> Self {
345 Self::Field(field.into())
346 }
347
348 pub fn index(index: u64) -> Self {
350 Self::Index(index)
351 }
352
353 pub fn from_proto(value: pb::VariantPathElement) -> VortexResult<Self> {
355 match value
356 .element
357 .ok_or_else(|| vortex_err!("Variant path element missing value"))?
358 {
359 variant_path_element::Element::Field(field) => Ok(Self::field(field)),
360 variant_path_element::Element::Index(index) => Ok(Self::index(index)),
361 }
362 }
363
364 pub fn to_proto(&self) -> pb::VariantPathElement {
366 match self {
367 VariantPathElement::Field(name) => pb::VariantPathElement {
368 element: Some(variant_path_element::Element::Field(
369 name.as_ref().to_string(),
370 )),
371 },
372 VariantPathElement::Index(index) => pb::VariantPathElement {
373 element: Some(variant_path_element::Element::Index(*index)),
374 },
375 }
376 }
377}
378
379impl From<FieldName> for VariantPathElement {
380 fn from(value: FieldName) -> Self {
381 Self::field(value)
382 }
383}
384
385impl From<&str> for VariantPathElement {
386 fn from(value: &str) -> Self {
387 Self::field(value)
388 }
389}
390
391impl From<u64> for VariantPathElement {
392 fn from(value: u64) -> Self {
393 Self::index(value)
394 }
395}
396
397#[cfg(test)]
398mod tests {
399 use vortex_error::VortexResult;
400 use vortex_error::vortex_bail;
401 use vortex_error::vortex_ensure;
402 use vortex_error::vortex_err;
403 use vortex_session::VortexSession;
404
405 use crate::ArrayRef;
406 use crate::Canonical;
407 use crate::IntoArray;
408 use crate::VortexSessionExecute;
409 use crate::array_session;
410 use crate::arrays::Chunked;
411 use crate::arrays::ChunkedArray;
412 use crate::arrays::ConstantArray;
413 use crate::arrays::PrimitiveArray;
414 use crate::arrays::VariantArray;
415 use crate::arrays::variant::VariantArraySlotsExt;
416 use crate::assert_arrays_eq;
417 use crate::assert_nth_scalar_is_null;
418 use crate::dtype::DType;
419 use crate::dtype::FieldName;
420 use crate::dtype::FieldNames;
421 use crate::dtype::Nullability;
422 use crate::dtype::PType;
423 use crate::dtype::StructFields;
424 use crate::expr::Expression;
425 use crate::expr::proto::ExprSerializeProtoExt;
426 use crate::expr::root;
427 use crate::expr::variant_get;
428 use crate::scalar::Scalar;
429 use crate::scalar::ScalarValue;
430 use crate::scalar_fn::ScalarFnVTable;
431 use crate::scalar_fn::fns::variant_get::VariantGet;
432 use crate::scalar_fn::fns::variant_get::VariantGetOptions;
433 use crate::scalar_fn::fns::variant_get::VariantPath;
434 use crate::scalar_fn::fns::variant_get::VariantPathElement;
435
436 fn variant_object(fields: impl IntoIterator<Item = (&'static str, Scalar)>) -> Scalar {
437 let fields = fields.into_iter().collect::<Vec<_>>();
438 let names = FieldNames::from_iter(fields.iter().map(|(name, _)| FieldName::from(*name)));
439 let dtypes = vec![DType::Variant(Nullability::NonNullable); fields.len()];
440 let values = fields
441 .into_iter()
442 .map(|(_, value)| Scalar::variant(value).into_value())
443 .collect();
444 Scalar::try_new(
445 DType::Struct(StructFields::new(names, dtypes), Nullability::NonNullable),
446 Some(ScalarValue::Tuple(values)),
447 )
448 .unwrap()
449 }
450
451 fn variant_rows(rows: impl IntoIterator<Item = Scalar>) -> VortexResult<ArrayRef> {
452 let dtype = DType::Variant(Nullability::Nullable);
453 let chunks = rows
454 .into_iter()
455 .map(|row| ConstantArray::new(row.cast(&dtype).unwrap(), 1).into_array());
456 ChunkedArray::try_new(chunks, dtype.clone()).map(|array| array.into_array())
457 }
458
459 fn parse_path(path: &str) -> VortexResult<VariantPath> {
462 if path.is_empty() || path == "$" {
463 return Ok(VariantPath::root());
464 }
465
466 let mut elements = Vec::new();
467 let mut pos = usize::from(path.as_bytes().first() == Some(&b'$'));
468 if pos == 1
469 && path
470 .as_bytes()
471 .get(pos)
472 .is_some_and(|byte| !matches!(byte, b'.' | b'['))
473 {
474 vortex_bail!("Invalid Variant path {path:?}: expected '.' or '[' after '$'");
475 }
476
477 while pos < path.len() {
478 match path.as_bytes()[pos] {
479 b'.' => {
480 pos += 1;
481 let (field, next_pos) = parse_field(path, pos)?;
482 elements.push(VariantPathElement::field(field));
483 pos = next_pos;
484 }
485 b'[' => {
486 let (index, next_pos) = parse_index(path, pos + 1)?;
487 elements.push(VariantPathElement::index(index));
488 pos = next_pos;
489 }
490 _ if pos == 0 => {
491 let (field, next_pos) = parse_field(path, pos)?;
492 elements.push(VariantPathElement::field(field));
493 pos = next_pos;
494 }
495 _ => {
496 vortex_bail!("Invalid Variant path {path:?}: expected '.', '[', or end of path")
497 }
498 }
499 }
500
501 Ok(VariantPath::new(elements))
502 }
503
504 fn parse_field(path: &str, start: usize) -> VortexResult<(&str, usize)> {
505 let mut pos = start;
506 while path
507 .as_bytes()
508 .get(pos)
509 .is_some_and(|byte| byte.is_ascii_alphanumeric() || *byte == b'_')
510 {
511 pos += 1;
512 }
513 vortex_ensure!(
514 pos > start,
515 "Invalid Variant path {path:?}: expected field name"
516 );
517 Ok((&path[start..pos], pos))
518 }
519
520 fn parse_index(path: &str, start: usize) -> VortexResult<(u64, usize)> {
521 let mut pos = start;
522 while path
523 .as_bytes()
524 .get(pos)
525 .is_some_and(|byte| byte.is_ascii_digit())
526 {
527 pos += 1;
528 }
529 vortex_ensure!(
530 pos > start,
531 "Invalid Variant path {path:?}: expected list index"
532 );
533 vortex_ensure!(
534 path.as_bytes().get(pos) == Some(&b']'),
535 "Invalid Variant path {path:?}: expected closing ']'"
536 );
537 let index = path[start..pos]
538 .parse()
539 .map_err(|_| vortex_err!("Invalid Variant path {path:?}: list index is too large"))?;
540 Ok((index, pos + 1))
541 }
542
543 fn execute_variant_get(
544 array: ArrayRef,
545 path: &str,
546 dtype: Option<DType>,
547 ) -> VortexResult<ArrayRef> {
548 let expr = variant_get(root(), parse_path(path)?, dtype);
549 array
550 .apply(&expr)?
551 .execute::<ArrayRef>(&mut array_session().create_execution_ctx())
552 }
553
554 #[test]
555 fn variant_get_path_parse_and_display() {
556 let path = parse_path("$.data[1].a").unwrap();
557 assert_eq!(
558 path.elements(),
559 &[
560 VariantPathElement::field("data"),
561 VariantPathElement::index(1),
562 VariantPathElement::field("a")
563 ]
564 );
565 assert_eq!(path.to_string(), "$.data[1].a");
566
567 let bare_path = parse_path("data[2]").unwrap();
568 assert_eq!(bare_path.to_string(), "$.data[2]");
569 assert!(parse_path("$.").is_err());
570 assert!(parse_path("$data").is_err());
571 assert!(parse_path("$.data[-1]").is_err());
572 }
573
574 #[test]
575 fn variant_get_return_dtype_is_nullable_variant_without_requested_dtype() {
576 let expr = variant_get(root(), VariantPath::field("data"), None);
577 let dtype = expr
578 .return_dtype(&DType::Variant(Nullability::NonNullable))
579 .unwrap();
580
581 assert_eq!(dtype, DType::Variant(Nullability::Nullable));
582 }
583
584 #[test]
585 fn variant_get_return_dtype_makes_requested_dtype_nullable() {
586 let requested = DType::Primitive(PType::I64, Nullability::NonNullable);
587 let expr = variant_get(root(), VariantPath::field("data"), Some(requested));
588 let dtype = expr
589 .return_dtype(&DType::Variant(Nullability::NonNullable))
590 .unwrap();
591
592 assert_eq!(dtype, DType::Primitive(PType::I64, Nullability::Nullable));
593 }
594
595 #[test]
596 fn variant_get_rejects_non_variant_input() {
597 let expr = variant_get(root(), VariantPath::field("data"), None);
598 let err = expr
599 .return_dtype(&DType::Utf8(Nullability::NonNullable))
600 .unwrap_err();
601
602 assert!(err.to_string().contains("VariantGet input must be Variant"));
603 }
604
605 #[test]
606 fn variant_get_formats_sql() {
607 let expr = variant_get(
608 root(),
609 parse_path("$.data[1].a").unwrap(),
610 Some(DType::Utf8(Nullability::NonNullable)),
611 );
612
613 assert_eq!(expr.to_string(), "variant_get($, \"$.data[1].a\", utf8)");
614 }
615
616 #[test]
617 fn variant_get_options_roundtrip_serialization() {
618 let options = VariantGetOptions::new(
619 VariantPath::new([
620 VariantPathElement::field("data"),
621 VariantPathElement::index(1),
622 VariantPathElement::field("a"),
623 ]),
624 Some(DType::Primitive(PType::I32, Nullability::NonNullable)),
625 );
626 let metadata = VariantGet.serialize(&options).unwrap().unwrap();
627 let actual = VariantGet
628 .deserialize(&metadata, &VortexSession::empty())
629 .unwrap();
630
631 assert_eq!(actual, options);
632 }
633
634 #[test]
635 fn variant_get_expression_roundtrip_serialization() {
636 let expr: Expression = variant_get(
637 root(),
638 parse_path("$.data[1].a").unwrap(),
639 Some(DType::Primitive(PType::I32, Nullability::NonNullable)),
640 );
641 let proto = expr.serialize_proto().unwrap();
642 let actual = Expression::from_proto(&proto, &array_session()).unwrap();
643
644 assert_eq!(actual, expr);
645 }
646
647 #[test]
648 fn variant_get_generic_fallback_extracts_field_and_list_index() -> VortexResult<()> {
649 let items = Scalar::list(
650 DType::Variant(Nullability::NonNullable),
651 vec![
652 Scalar::variant(Scalar::primitive(10i32, Nullability::NonNullable)),
653 Scalar::variant(Scalar::primitive(20i32, Nullability::NonNullable)),
654 ],
655 Nullability::NonNullable,
656 );
657 let array = variant_rows([
658 Scalar::variant(variant_object([("items", items)])),
659 Scalar::variant(variant_object([(
660 "items",
661 Scalar::list_empty(
662 DType::Variant(Nullability::NonNullable).into(),
663 Nullability::NonNullable,
664 ),
665 )])),
666 Scalar::variant(variant_object([(
667 "items",
668 Scalar::list(
669 DType::Variant(Nullability::NonNullable),
670 vec![
671 Scalar::variant(Scalar::utf8("x", Nullability::NonNullable)),
672 Scalar::variant(Scalar::utf8("wrong", Nullability::NonNullable)),
673 ],
674 Nullability::NonNullable,
675 ),
676 )])),
677 ])?;
678
679 let result = execute_variant_get(
680 array,
681 "$.items[1]",
682 Some(DType::Primitive(PType::I32, Nullability::NonNullable)),
683 )?;
684 let mut ctx = array_session().create_execution_ctx();
685
686 assert_arrays_eq!(
687 result,
688 PrimitiveArray::from_option_iter([Some(20i32), None, None]),
689 &mut ctx
690 );
691 Ok(())
692 }
693
694 #[test]
695 fn variant_get_reads_chunked_variant_input() -> VortexResult<()> {
696 let array = variant_rows([
697 Scalar::variant(variant_object([(
698 "a",
699 Scalar::primitive(10i32, Nullability::NonNullable),
700 )])),
701 Scalar::variant(variant_object([(
702 "b",
703 Scalar::primitive(20i32, Nullability::NonNullable),
704 )])),
705 Scalar::variant(variant_object([(
706 "a",
707 Scalar::primitive(30i32, Nullability::NonNullable),
708 )])),
709 Scalar::null(DType::Variant(Nullability::Nullable)),
710 ])?;
711 assert!(array.is::<Chunked>());
712
713 let result = execute_variant_get(
714 array,
715 "$.a",
716 Some(DType::Primitive(PType::I32, Nullability::NonNullable)),
717 )?;
718 let mut ctx = array_session().create_execution_ctx();
719
720 assert_arrays_eq!(
721 result,
722 PrimitiveArray::from_option_iter([Some(10i32), None, Some(30), None]),
723 &mut ctx
724 );
725 Ok(())
726 }
727
728 #[test]
729 fn variant_get_fallback_typed_output_is_contiguous() -> VortexResult<()> {
730 let array = variant_rows([
731 Scalar::variant(variant_object([(
732 "a",
733 Scalar::primitive(10i32, Nullability::NonNullable),
734 )])),
735 Scalar::variant(variant_object([(
736 "a",
737 Scalar::primitive(20i32, Nullability::NonNullable),
738 )])),
739 Scalar::variant(variant_object([(
740 "b",
741 Scalar::primitive(30i32, Nullability::NonNullable),
742 )])),
743 ])?;
744
745 let result = execute_variant_get(
746 array,
747 "$.a",
748 Some(DType::Primitive(PType::I32, Nullability::NonNullable)),
749 )?;
750
751 assert!(!result.is::<Chunked>());
752 let mut ctx = array_session().create_execution_ctx();
753 assert_arrays_eq!(
754 result,
755 PrimitiveArray::from_option_iter([Some(10i32), Some(20), None]),
756 &mut ctx
757 );
758 Ok(())
759 }
760
761 #[test]
762 fn variant_get_generic_fallback_preserves_variant_null() -> VortexResult<()> {
763 let array = variant_rows([
764 Scalar::variant(variant_object([(
765 "a",
766 Scalar::utf8("ok", Nullability::NonNullable),
767 )])),
768 Scalar::null(DType::Variant(Nullability::Nullable)),
769 Scalar::variant(variant_object([("a", Scalar::null(DType::Null))])),
770 Scalar::variant(variant_object([(
771 "b",
772 Scalar::primitive(2i32, Nullability::NonNullable),
773 )])),
774 ])?;
775
776 let result = execute_variant_get(array, "$.a", None)?;
777
778 let mut ctx = array_session().create_execution_ctx();
779 let row0 = result.execute_scalar(0, &mut ctx)?;
780 assert_eq!(
781 row0.as_variant()
782 .value()
783 .and_then(|value| value.as_utf8().value())
784 .map(|value| value.as_str()),
785 Some("ok")
786 );
787 assert_nth_scalar_is_null!(result, 1, &mut ctx);
788 assert_eq!(
789 result
790 .execute_scalar(2, &mut ctx)?
791 .as_variant()
792 .is_variant_null(),
793 Some(true)
794 );
795 assert_nth_scalar_is_null!(result, 3, &mut ctx);
796 Ok(())
797 }
798
799 #[test]
800 fn variant_get_fallback_variant_output_canonicalizes() -> VortexResult<()> {
801 let array = variant_rows([
802 Scalar::variant(variant_object([(
803 "a",
804 Scalar::primitive(10i32, Nullability::NonNullable),
805 )])),
806 Scalar::variant(variant_object([(
807 "a",
808 Scalar::primitive(20i32, Nullability::NonNullable),
809 )])),
810 ])?;
811
812 let result = execute_variant_get(array, "$.a", None)?;
813 let variant = result
814 .clone()
815 .execute::<VariantArray>(&mut array_session().create_execution_ctx())?;
816 let canonical = result.execute::<Canonical>(&mut array_session().create_execution_ctx())?;
817 let Canonical::Variant(canonical_variant) = canonical else {
818 vortex_bail!("expected Variant canonical array");
819 };
820
821 assert_eq!(variant.len(), 2);
822 assert_eq!(canonical_variant.len(), 2);
823 assert_eq!(variant.core_storage().dtype(), variant.dtype());
824 assert_eq!(variant.core_storage().len(), variant.len());
825
826 let mut ctx = array_session().create_execution_ctx();
827 for (idx, expected) in [10i32, 20].into_iter().enumerate() {
828 let scalar = variant.execute_scalar(idx, &mut ctx)?;
829 let actual = scalar
830 .as_variant()
831 .value()
832 .and_then(|value| value.as_primitive().as_::<i32>());
833 assert_eq!(actual, Some(expected), "row {idx}");
834 }
835 Ok(())
836 }
837}