vortex_json/
json_to_variant.rs1use std::fmt;
7use std::fmt::Display;
8use std::fmt::Formatter;
9
10use prost::Message;
11use vortex_array::ArrayRef;
12use vortex_array::ExecutionCtx;
13use vortex_array::IntoArray;
14use vortex_array::arrays::Extension;
15use vortex_array::arrays::ExtensionArray;
16use vortex_array::arrays::scalar_fn::ScalarFnFactoryExt;
17use vortex_array::dtype::DType;
18use vortex_array::expr::Expression;
19use vortex_array::scalar_fn::Arity;
20use vortex_array::scalar_fn::ChildName;
21use vortex_array::scalar_fn::ExecutionArgs;
22use vortex_array::scalar_fn::ScalarFnId;
23use vortex_array::scalar_fn::ScalarFnVTable;
24use vortex_array::scalar_fn::ScalarFnVTableExt;
25use vortex_array::scalar_fn::fns::variant_get::VariantPath;
26use vortex_array::scalar_fn::fns::variant_get::VariantPathElement;
27use vortex_error::VortexResult;
28use vortex_error::vortex_bail;
29use vortex_error::vortex_ensure;
30use vortex_error::vortex_err;
31use vortex_proto::expr as pb;
32use vortex_session::VortexSession;
33use vortex_session::registry::CachedId;
34
35use crate::Json;
36
37#[derive(Clone)]
69pub struct JsonToVariant;
70
71impl ScalarFnVTable for JsonToVariant {
72 type Options = JsonToVariantOptions;
73
74 fn id(&self) -> ScalarFnId {
75 static ID: CachedId = CachedId::new("vortex.json_to_variant");
76 *ID
77 }
78
79 fn serialize(&self, options: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
80 let shredding = options
81 .shredding()
82 .fields()
83 .iter()
84 .map(|(path, dtype)| {
85 Ok(pb::ShreddingSpecField {
86 path: path
87 .elements()
88 .iter()
89 .map(VariantPathElement::to_proto)
90 .collect(),
91 dtype: Some(dtype.try_into()?),
92 })
93 })
94 .collect::<VortexResult<_>>()?;
95
96 Ok(Some(pb::JsonToVariantOpts { shredding }.encode_to_vec()))
97 }
98
99 fn deserialize(&self, metadata: &[u8], session: &VortexSession) -> VortexResult<Self::Options> {
100 let opts = pb::JsonToVariantOpts::decode(metadata)?;
101 let fields = opts
102 .shredding
103 .into_iter()
104 .map(|field| {
105 let path = field
106 .path
107 .into_iter()
108 .map(VariantPathElement::from_proto)
109 .collect::<VortexResult<VariantPath>>()?;
110 let dtype = field
111 .dtype
112 .as_ref()
113 .ok_or_else(|| vortex_err!("ShreddingSpecField missing dtype"))
114 .and_then(|dtype| DType::from_proto(dtype, session))?;
115 Ok((path, dtype))
116 })
117 .collect::<VortexResult<Vec<_>>>()?;
118
119 Ok(JsonToVariantOptions::new(ShreddingSpec::try_new(fields)?))
120 }
121
122 fn arity(&self, _options: &Self::Options) -> Arity {
123 Arity::Exact(1)
124 }
125
126 fn child_name(&self, _options: &Self::Options, child_idx: usize) -> ChildName {
127 match child_idx {
128 0 => ChildName::from("input"),
129 _ => unreachable!("Invalid child index {child_idx} for JsonToVariant expression"),
130 }
131 }
132
133 fn return_dtype(&self, _options: &Self::Options, arg_dtypes: &[DType]) -> VortexResult<DType> {
134 let input_dtype = &arg_dtypes[0];
135 vortex_ensure!(
136 input_dtype
137 .as_extension_opt()
138 .is_some_and(|ext_dtype| ext_dtype.is::<Json>()),
139 "JsonToVariant input must be a Json extension, found {input_dtype}"
140 );
141
142 Ok(DType::Variant(input_dtype.nullability()))
143 }
144
145 fn execute(
146 &self,
147 options: &Self::Options,
148 args: &dyn ExecutionArgs,
149 ctx: &mut ExecutionCtx,
150 ) -> VortexResult<ArrayRef> {
151 let input = args.get(0)?;
152
153 let no_kernel = || {
160 vortex_err!(
161 "json_to_variant requires a registered Variant encoding to build Variant values \
162 from JSON, but none is registered with this session"
163 )
164 };
165
166 let canonical = if input
167 .dtype()
168 .as_extension_opt()
169 .is_some_and(|ext_dtype| ext_dtype.is::<Json>())
170 {
171 if input.is::<Extension>() {
172 return Err(no_kernel());
173 }
174 input.execute::<ExtensionArray>(ctx)?.into_array()
175 } else {
176 vortex_bail!(
177 "JsonToVariant input must be a Json extension, found {}",
178 input.dtype()
179 );
180 };
181
182 self.try_new_array(canonical.len(), options.clone(), [canonical])?
183 .execute::<ArrayRef>(ctx)
184 }
185
186 fn is_null_sensitive(&self, _options: &Self::Options) -> bool {
187 false
192 }
193}
194
195pub fn json_to_variant(child: Expression, shredding: ShreddingSpec) -> Expression {
204 JsonToVariant.new_expr(JsonToVariantOptions::new(shredding), [child])
205}
206
207#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
215pub struct ShreddingSpec(Vec<(VariantPath, DType)>);
216
217impl ShreddingSpec {
218 pub fn empty() -> Self {
220 Self::default()
221 }
222
223 pub fn try_new(fields: impl IntoIterator<Item = (VariantPath, DType)>) -> VortexResult<Self> {
229 let fields = fields.into_iter().collect::<Vec<_>>();
230 for (path, _) in &fields {
231 vortex_ensure!(
232 path.elements()
233 .iter()
234 .all(|element| matches!(element, VariantPathElement::Field(_))),
235 "ShreddingSpec paths must only contain object fields, found list index in {path}"
236 );
237 }
238 Ok(Self(fields))
239 }
240
241 pub fn fields(&self) -> &[(VariantPath, DType)] {
243 &self.0
244 }
245
246 pub fn is_empty(&self) -> bool {
248 self.0.is_empty()
249 }
250}
251
252impl Display for ShreddingSpec {
253 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
254 for (idx, (path, dtype)) in self.0.iter().enumerate() {
255 if idx > 0 {
256 write!(f, ", ")?;
257 }
258 write!(f, "{path} as {dtype}")?;
259 }
260 Ok(())
261 }
262}
263
264#[derive(Clone, Debug, PartialEq, Eq, Hash)]
266pub struct JsonToVariantOptions {
267 shredding: ShreddingSpec,
269}
270
271impl JsonToVariantOptions {
272 pub fn new(shredding: ShreddingSpec) -> Self {
274 Self { shredding }
275 }
276
277 pub fn unshredded() -> Self {
279 Self::new(ShreddingSpec::empty())
280 }
281
282 pub fn shredding(&self) -> &ShreddingSpec {
284 &self.shredding
285 }
286}
287
288impl Display for JsonToVariantOptions {
289 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
290 self.shredding.fmt(f)
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 #![allow(clippy::missing_docs_in_private_items)]
297
298 use vortex_array::ArrayRef;
299 use vortex_array::EmptyMetadata;
300 use vortex_array::IntoArray;
301 use vortex_array::VortexSessionExecute;
302 use vortex_array::arrays::VarBinViewArray;
303 use vortex_array::dtype::Nullability;
304 use vortex_array::dtype::PType;
305 use vortex_array::dtype::session::DTypeSession;
306 use vortex_array::expr::proto::ExprSerializeProtoExt;
307 use vortex_array::expr::root;
308 use vortex_array::scalar_fn::session::ScalarFnSession;
309 use vortex_array::scalar_fn::session::ScalarFnSessionExt;
310 use vortex_array::session::ArraySession;
311
312 use super::*;
313
314 fn i64_dtype() -> DType {
315 DType::Primitive(PType::I64, Nullability::Nullable)
316 }
317
318 fn session() -> VortexSession {
321 let session = VortexSession::empty()
322 .with::<ArraySession>()
323 .with::<DTypeSession>()
324 .with::<ScalarFnSession>();
325 session.scalar_fns().register(JsonToVariant);
326 session
327 }
328
329 #[test]
330 fn shredding_spec_rejects_index_paths() {
331 let err = ShreddingSpec::try_new([(
332 VariantPath::new([VariantPathElement::field("a"), VariantPathElement::index(0)]),
333 i64_dtype(),
334 )])
335 .unwrap_err();
336
337 assert!(
338 err.to_string()
339 .contains("ShreddingSpec paths must only contain object fields"),
340 "unexpected error: {err}"
341 );
342 }
343
344 #[test]
345 fn expression_roundtrip_serialization() -> VortexResult<()> {
346 let spec = ShreddingSpec::try_new([(VariantPath::field("a"), i64_dtype())])?;
347 let expr: Expression = json_to_variant(root(), spec);
348 let proto = expr.serialize_proto()?;
349 let actual = Expression::from_proto(&proto, &session())?;
350
351 assert_eq!(actual, expr);
352 Ok(())
353 }
354
355 #[test]
356 fn utf8_input_is_rejected() {
357 let input = VarBinViewArray::from_iter_str(["1", "2"]).into_array();
358 let err = JsonToVariant
359 .try_new_array(input.len(), JsonToVariantOptions::unshredded(), [input])
360 .unwrap_err();
361
362 assert!(
363 err.to_string().contains("Json extension"),
364 "unexpected error: {err}"
365 );
366 }
367
368 #[test]
369 fn execute_without_variant_kernel_errors() -> VortexResult<()> {
370 let input = ExtensionArray::try_new_from_vtable(
373 Json,
374 EmptyMetadata,
375 VarBinViewArray::from_iter_str(["1", "2"]).into_array(),
376 )?
377 .into_array();
378
379 let dtype = input.dtype().clone();
380 let array = JsonToVariant.try_new_array(
381 input.len(),
382 JsonToVariantOptions::unshredded(),
383 [input],
384 )?;
385
386 let err = array
387 .execute::<ArrayRef>(&mut session().create_execution_ctx())
388 .unwrap_err();
389
390 assert!(
391 err.to_string().contains("Variant encoding"),
392 "unexpected error for {dtype}: {err}"
393 );
394 Ok(())
395 }
396}