Skip to content

Commit 8318ee1

Browse files
committed
Only support direct JSON inputs for expressions
Signed-off-by: Adam Gutglick <adam@spiraldb.com>
1 parent 17007a1 commit 8318ee1

3 files changed

Lines changed: 103 additions & 102 deletions

File tree

encodings/parquet-variant/src/json_to_variant_tests.rs

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
//! Execution tests for the `vortex.json_to_variant` scalar function.
55
//!
66
//! The function definition lives in `vortex-json`; the JSON->Variant construction is performed
7-
//! by the [`JsonToVariantKernel`](crate::kernel) registered here, so these end-to-end tests live
8-
//! in `vortex-parquet-variant` where that kernel is registered.
7+
//! by the execute-parent kernel registered here, so these end-to-end tests live in
8+
//! `vortex-parquet-variant` where that kernel is registered.
99
1010
use std::sync::LazyLock;
1111

@@ -54,6 +54,10 @@ fn shred_field_as_i64(field: &str) -> VortexResult<ShreddingSpec> {
5454
ShreddingSpec::try_new([(VariantPath::field(field), i64_dtype())])
5555
}
5656

57+
fn json_input(storage: ArrayRef) -> VortexResult<ArrayRef> {
58+
Ok(ExtensionArray::try_new_from_vtable(Json, EmptyMetadata, storage)?.into_array())
59+
}
60+
5761
fn execute_json_to_variant(input: ArrayRef, shredding: ShreddingSpec) -> VortexResult<ArrayRef> {
5862
let expr = json_to_variant(root(), shredding);
5963
input
@@ -82,8 +86,21 @@ fn assert_variant_i64_rows(array: &ArrayRef, expected: &[Option<i64>]) -> Vortex
8286
}
8387

8488
#[test]
85-
fn converts_utf8_json_rows() -> VortexResult<()> {
86-
let input = VarBinViewArray::from_iter_str([r#"{"a": 1}"#, "2", r#"{"a": 3}"#]).into_array();
89+
fn rejects_bare_utf8_input() {
90+
let input = VarBinViewArray::from_iter_str(["1", "2"]).into_array();
91+
92+
let err = execute_json_to_variant(input, ShreddingSpec::empty()).unwrap_err();
93+
assert!(
94+
err.to_string().contains("Json extension"),
95+
"unexpected error: {err}"
96+
);
97+
}
98+
99+
#[test]
100+
fn converts_json_extension_rows() -> VortexResult<()> {
101+
let input = json_input(
102+
VarBinViewArray::from_iter_str([r#"{"a": 1}"#, "2", r#"{"a": 3}"#]).into_array(),
103+
)?;
87104

88105
let result = execute_json_to_variant(input, ShreddingSpec::empty())?;
89106

@@ -117,7 +134,7 @@ fn converts_utf8_json_rows() -> VortexResult<()> {
117134
#[test]
118135
fn converts_json_extension_input() -> VortexResult<()> {
119136
let storage = VarBinViewArray::from_iter_str(["1", "2"]).into_array();
120-
let input = ExtensionArray::try_new_from_vtable(Json, EmptyMetadata, storage)?.into_array();
137+
let input = json_input(storage)?;
121138

122139
let result = execute_json_to_variant(input, ShreddingSpec::empty())?;
123140

@@ -127,10 +144,10 @@ fn converts_json_extension_input() -> VortexResult<()> {
127144

128145
#[test]
129146
fn dict_encoded_input_converts_each_row() -> VortexResult<()> {
130-
// A dictionary-encoded string column exercises the dict-pushdown / canonicalization path:
147+
// A dictionary-encoded JSON column exercises the dict-pushdown / canonicalization path:
131148
// `json_to_variant` is not null-sensitive, so it pushes into the dict values (canonical
132-
// strings) where the kernel fires; either way every row must convert correctly.
133-
let values = VarBinViewArray::from_iter_str(["1", "2"]).into_array();
149+
// JSON extension values) where the kernel fires; either way every row must convert correctly.
150+
let values = json_input(VarBinViewArray::from_iter_str(["1", "2"]).into_array())?;
134151
let codes = PrimitiveArray::from_iter([0u8, 1, 0, 1, 0]).into_array();
135152
let input = DictArray::try_new(codes, values)?.into_array();
136153

@@ -142,8 +159,9 @@ fn dict_encoded_input_converts_each_row() -> VortexResult<()> {
142159

143160
#[test]
144161
fn null_rows_stay_null_and_json_null_becomes_variant_null() -> VortexResult<()> {
145-
let input =
146-
VarBinViewArray::from_iter_nullable_str([Some("1"), None, Some("null")]).into_array();
162+
let input = json_input(
163+
VarBinViewArray::from_iter_nullable_str([Some("1"), None, Some("null")]).into_array(),
164+
)?;
147165

148166
let result = execute_json_to_variant(input, ShreddingSpec::empty())?;
149167

@@ -158,22 +176,26 @@ fn null_rows_stay_null_and_json_null_becomes_variant_null() -> VortexResult<()>
158176
}
159177

160178
#[test]
161-
fn invalid_json_errors() {
162-
let input = VarBinViewArray::from_iter_str([r#"{"a": 1}"#, r#"{"a":"#]).into_array();
179+
fn invalid_json_errors() -> VortexResult<()> {
180+
let input =
181+
json_input(VarBinViewArray::from_iter_str([r#"{"a": 1}"#, r#"{"a":"#]).into_array())?;
163182

164183
let err = execute_json_to_variant(input, ShreddingSpec::empty()).unwrap_err();
165184
assert!(!err.to_string().is_empty());
185+
Ok(())
166186
}
167187

168188
#[test]
169189
fn shredding_produces_typed_value_child() -> VortexResult<()> {
170-
let input = VarBinViewArray::from_iter_str([
171-
r#"{"a": 1, "b": "x"}"#,
172-
r#"{"a": 2, "b": "y"}"#,
173-
r#"{"a": "not-a-number", "b": "z"}"#,
174-
r#"{"b": "missing-a"}"#,
175-
])
176-
.into_array();
190+
let input = json_input(
191+
VarBinViewArray::from_iter_str([
192+
r#"{"a": 1, "b": "x"}"#,
193+
r#"{"a": 2, "b": "y"}"#,
194+
r#"{"a": "not-a-number", "b": "z"}"#,
195+
r#"{"b": "missing-a"}"#,
196+
])
197+
.into_array(),
198+
)?;
177199

178200
let result = execute_json_to_variant(input, shred_field_as_i64("a")?)?;
179201

@@ -227,9 +249,10 @@ fn shredding_produces_typed_value_child() -> VortexResult<()> {
227249

228250
#[test]
229251
fn shredding_preserves_null_rows() -> VortexResult<()> {
230-
let input =
252+
let input = json_input(
231253
VarBinViewArray::from_iter_nullable_str([Some(r#"{"a": 1}"#), None, Some(r#"{"a": 3}"#)])
232-
.into_array();
254+
.into_array(),
255+
)?;
233256

234257
let result = execute_json_to_variant(input, shred_field_as_i64("a")?)?;
235258

@@ -253,7 +276,8 @@ fn shredding_preserves_null_rows() -> VortexResult<()> {
253276

254277
#[test]
255278
fn shredding_root_path_shreds_top_level_values() -> VortexResult<()> {
256-
let input = VarBinViewArray::from_iter_str(["1", "2", r#""not-a-number""#]).into_array();
279+
let input =
280+
json_input(VarBinViewArray::from_iter_str(["1", "2", r#""not-a-number""#]).into_array())?;
257281
let spec = ShreddingSpec::try_new([(VariantPath::root(), i64_dtype())])?;
258282

259283
let result = execute_json_to_variant(input, spec)?;

encodings/parquet-variant/src/kernel.rs

Lines changed: 6 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ use vortex_array::arrays::Dict;
2424
use vortex_array::arrays::Extension;
2525
use vortex_array::arrays::Filter;
2626
use vortex_array::arrays::Slice;
27-
use vortex_array::arrays::VarBinView;
2827
use vortex_array::arrays::dict::TakeExecute;
2928
use vortex_array::arrays::dict::TakeExecuteAdaptor;
3029
use vortex_array::arrays::extension::ExtensionArrayExt;
@@ -72,7 +71,6 @@ pub(crate) fn initialize(session: &VortexSession) {
7271
TakeExecuteAdaptor(ParquetVariant),
7372
);
7473
kernels.register_execute_parent_kernel(VariantGet.id(), ParquetVariant, VariantGetKernel);
75-
kernels.register_execute_parent_kernel(JsonToVariant.id(), VarBinView, JsonToVariantKernel);
7674
kernels.register_execute_parent_kernel(
7775
JsonToVariant.id(),
7876
Extension,
@@ -120,13 +118,13 @@ impl ExecuteParentKernel<ParquetVariant> for VariantGetKernel {
120118
}
121119
}
122120

123-
/// Performs the [`JsonToVariant`] conversion (and optional shredding) over a JSON string array.
121+
/// Performs the [`JsonToVariant`] conversion (and optional shredding) over JSON string storage.
124122
///
125123
/// `JsonToVariant`'s definition lives in `vortex-json`; the registered `execute_parent` kernels
126124
/// delegate here to do the actual JSON parsing and optional shredding using
127-
/// `parquet_variant_compute`, producing a [`ParquetVariant`] array. `strings` is the input JSON
128-
/// string array (any string encoding); it is executed to Arrow and parsed. Nullability of the
129-
/// result follows `parent.dtype()`, which equals the input's nullability.
125+
/// `parquet_variant_compute`, producing a [`ParquetVariant`] array. `strings` is the JSON
126+
/// extension's storage array; it is executed to Arrow and parsed. Nullability of the result follows
127+
/// `parent.dtype()`, which equals the input's nullability.
130128
fn json_strings_to_variant(
131129
strings: ArrayRef,
132130
parent: ScalarFnArrayView<'_, JsonToVariant>,
@@ -156,32 +154,10 @@ fn json_strings_to_variant(
156154
}
157155
}
158156

159-
/// Builds Parquet Variant arrays for [`JsonToVariant`] over canonical `VarBinView` string input.
160-
#[derive(Default, Debug)]
161-
struct JsonToVariantKernel;
162-
163-
impl ExecuteParentKernel<VarBinView> for JsonToVariantKernel {
164-
type Parent = ExactScalarFn<JsonToVariant>;
165-
166-
fn execute_parent(
167-
&self,
168-
array: ArrayView<'_, VarBinView>,
169-
parent: ScalarFnArrayView<'_, JsonToVariant>,
170-
child_idx: usize,
171-
ctx: &mut ExecutionCtx,
172-
) -> VortexResult<Option<ArrayRef>> {
173-
if child_idx != 0 {
174-
return Ok(None);
175-
}
176-
json_strings_to_variant(array.as_ref().clone(), parent, ctx).map(Some)
177-
}
178-
}
179-
180157
/// Builds Parquet Variant arrays for [`JsonToVariant`] over a [`Json`] extension input.
181158
///
182-
/// `JsonToVariant` also accepts `Json` extension values directly. This kernel unwraps the
183-
/// extension's string storage and runs the same conversion as [`JsonToVariantKernel`]. It is keyed
184-
/// on the shared extension encoding, so it declines any non-`Json` extension.
159+
/// This kernel unwraps the extension's string storage and runs the JSON conversion. It is keyed on
160+
/// the shared extension encoding, so it declines any non-`Json` extension.
185161
#[derive(Default, Debug)]
186162
struct JsonExtensionToVariantKernel;
187163

vortex-json/src/json_to_variant.rs

Lines changed: 51 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ use vortex_array::ExecutionCtx;
1313
use vortex_array::IntoArray;
1414
use vortex_array::arrays::Extension;
1515
use vortex_array::arrays::ExtensionArray;
16-
use vortex_array::arrays::VarBinView;
17-
use vortex_array::arrays::VarBinViewArray;
1816
use vortex_array::arrays::scalar_fn::ScalarFnFactoryExt;
1917
use vortex_array::dtype::DType;
2018
use vortex_array::expr::Expression;
@@ -38,9 +36,9 @@ use crate::Json;
3836

3937
/// Parses JSON strings into Variant values, optionally shredding fields.
4038
///
41-
/// Accepts `Utf8` inputs or [`Json`] extension inputs and returns `Variant` values with the
42-
/// input's nullability. Null rows stay null, the JSON literal `null` becomes a variant-null
43-
/// value, and any row that fails to parse as JSON fails the whole expression.
39+
/// Accepts [`Json`] extension inputs and returns `Variant` values with the input's nullability.
40+
/// Null rows stay null, the JSON literal `null` becomes a variant-null value, and any row that
41+
/// fails to parse as JSON fails the whole expression.
4442
///
4543
/// A non-empty [`ShreddingSpec`] additionally shreds the selected paths into a typed shredded
4644
/// child, following the [Parquet Variant shredding] rules: rows whose value does not match the
@@ -50,10 +48,10 @@ use crate::Json;
5048
///
5149
/// Building a Variant requires a concrete Variant encoding, so this function does not perform the
5250
/// conversion itself. The Variant encoding registered with the session supplies it as an
53-
/// `execute_parent` kernel keyed on the canonical string encodings (`VarBinView` for `Utf8`, and
54-
/// the extension encoding for a [`Json`] input). The fallback [`execute`](ScalarFnVTable::execute)
55-
/// here only canonicalizes its input to one of those encodings and re-dispatches so that kernel
56-
/// runs; it errors if no Variant encoding is registered with the session.
51+
/// `execute_parent` kernel keyed on the extension encoding for a [`Json`] input. The fallback
52+
/// [`execute`](ScalarFnVTable::execute) here only canonicalizes the input to that encoding and
53+
/// re-dispatches so that kernel runs; it errors if no Variant encoding is registered with the
54+
/// session.
5755
///
5856
/// # Normalization
5957
///
@@ -135,11 +133,10 @@ impl ScalarFnVTable for JsonToVariant {
135133
fn return_dtype(&self, _options: &Self::Options, arg_dtypes: &[DType]) -> VortexResult<DType> {
136134
let input_dtype = &arg_dtypes[0];
137135
vortex_ensure!(
138-
input_dtype.is_utf8()
139-
|| input_dtype
140-
.as_extension_opt()
141-
.is_some_and(|ext_dtype| ext_dtype.is::<Json>()),
142-
"JsonToVariant input must be Utf8 or a Json extension, found {input_dtype}"
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}"
143140
);
144141

145142
Ok(DType::Variant(input_dtype.nullability()))
@@ -154,24 +151,19 @@ impl ScalarFnVTable for JsonToVariant {
154151
let input = args.get(0)?;
155152

156153
// This function does not build Variants itself: the Variant encoding registered with the
157-
// session supplies the conversion as an `execute_parent` kernel keyed on the canonical
158-
// string encodings (`VarBinView` for `Utf8`, the extension encoding for a `Json` input).
159-
// Reaching this fallback means no such kernel fired, so canonicalize the input to one of
160-
// those encodings and re-dispatch. If the input is already canonical here, no kernel is
161-
// registered for it; bail with a clear error rather than looping.
154+
// session supplies the conversion as an `execute_parent` kernel keyed on the extension
155+
// encoding for a `Json` input. Reaching this fallback means no such kernel fired, so
156+
// canonicalize the input to that encoding and re-dispatch. If the input is already
157+
// canonical here, no kernel is registered for it; bail with a clear error rather than
158+
// looping.
162159
let no_kernel = || {
163160
vortex_err!(
164161
"json_to_variant requires a registered Variant encoding to build Variant values \
165162
from JSON, but none is registered with this session"
166163
)
167164
};
168165

169-
let canonical = if input.dtype().is_utf8() {
170-
if input.is::<VarBinView>() {
171-
return Err(no_kernel());
172-
}
173-
input.execute::<VarBinViewArray>(ctx)?.into_array()
174-
} else if input
166+
let canonical = if input
175167
.dtype()
176168
.as_extension_opt()
177169
.is_some_and(|ext_dtype| ext_dtype.is::<Json>())
@@ -182,7 +174,7 @@ impl ScalarFnVTable for JsonToVariant {
182174
input.execute::<ExtensionArray>(ctx)?.into_array()
183175
} else {
184176
vortex_bail!(
185-
"JsonToVariant input must be Utf8 or a Json extension, found {}",
177+
"JsonToVariant input must be a Json extension, found {}",
186178
input.dtype()
187179
);
188180
};
@@ -195,16 +187,16 @@ impl ScalarFnVTable for JsonToVariant {
195187
// `json_to_variant` maps null rows to null rows and the JSON literal `null` to a
196188
// variant-null value independently of which other rows are null, so it commutes with
197189
// validity masking. Marking it not null-sensitive also lets it push through dictionaries
198-
// into their (canonical-string) values, where the kernel fires directly.
190+
// into their JSON extension values, where the kernel fires directly.
199191
false
200192
}
201193
}
202194

203195
/// Creates a [`JsonToVariant`] expression that parses `child`'s JSON strings into Variant
204196
/// values, shredding the paths selected by `shredding`.
205197
///
206-
/// `child` must produce `Utf8` or [`Json`] extension values; the result is `Variant` with the
207-
/// input's nullability. Rows containing invalid JSON fail the expression.
198+
/// `child` must produce [`Json`] extension values; the result is `Variant` with the input's
199+
/// nullability. Rows containing invalid JSON fail the expression.
208200
///
209201
/// Note that this is a lossy, normalizing conversion. See [`JsonToVariant`] for the full list of
210202
/// caveats.
@@ -360,36 +352,45 @@ mod tests {
360352
Ok(())
361353
}
362354

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+
363368
#[test]
364369
fn execute_without_variant_kernel_errors() -> VortexResult<()> {
365370
// With no Variant encoding registered, executing over an already-canonical input must
366-
// surface a clear error rather than looping. Both canonical input forms are covered: a
367-
// bare `Utf8`/`VarBinView`, and a `Json` extension.
368-
let utf8 = VarBinViewArray::from_iter_str(["1", "2"]).into_array();
369-
let json_extension = ExtensionArray::try_new_from_vtable(
371+
// surface a clear error rather than looping.
372+
let input = ExtensionArray::try_new_from_vtable(
370373
Json,
371374
EmptyMetadata,
372375
VarBinViewArray::from_iter_str(["1", "2"]).into_array(),
373376
)?
374377
.into_array();
375378

376-
for input in [utf8, json_extension] {
377-
let dtype = input.dtype().clone();
378-
let array = JsonToVariant.try_new_array(
379-
input.len(),
380-
JsonToVariantOptions::unshredded(),
381-
[input],
382-
)?;
383-
384-
let err = array
385-
.execute::<ArrayRef>(&mut session().create_execution_ctx())
386-
.unwrap_err();
387-
388-
assert!(
389-
err.to_string().contains("Variant encoding"),
390-
"unexpected error for {dtype}: {err}"
391-
);
392-
}
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+
);
393394
Ok(())
394395
}
395396
}

0 commit comments

Comments
 (0)