Skip to main content

micromegas_datafusion_extensions/jsonb/
list_udfs.rs

1//! List-returning JSONB scalar UDFs: `jsonb_entries`, `jsonb_elements`, `jsonb_path_elements`.
2//!
3//! Unlike the other scalar JSONB UDFs, these return a plain Arrow `List` (never
4//! `Dictionary<Int32, List<…>>`), because `unnest()` cannot expand a dictionary-wrapped list —
5//! see the plan this module implements. `properties` columns are dictionary-encoded, so a
6//! dictionary fast path builds the list once per unique blob and expands it to row count via
7//! `take`, instead of re-parsing the same bytes on every row.
8
9use crate::binary_column_accessor::{BinaryColumnAccessor, create_binary_accessor};
10use crate::jsonb::extract::{EntryList, array_elements, object_or_array_entries, path_select_all};
11use datafusion::arrow::array::{
12    Array, ArrayBuilder, ArrayRef, BinaryBuilder, DictionaryArray, GenericBinaryArray, ListBuilder,
13    StringArray, StringBuilder, StructBuilder,
14};
15use datafusion::arrow::compute::take;
16use datafusion::arrow::datatypes::{DataType, Field, Fields, Int32Type};
17use datafusion::common::{Result, exec_err};
18use datafusion::error::DataFusionError;
19use datafusion::logical_expr::{
20    ColumnarValue, ScalarFunctionArgs, ScalarUDF, ScalarUDFImpl, Signature, Volatility,
21};
22use jsonb::jsonpath::{JsonPath, parse_json_path};
23use std::collections::{HashMap, HashSet};
24use std::sync::Arc;
25
26// --- shared field/type helpers ---
27
28fn entry_struct_fields() -> Vec<Field> {
29    vec![
30        Field::new("key", DataType::Utf8, false),
31        Field::new("value", DataType::Binary, true),
32    ]
33}
34
35fn entries_list_field() -> Arc<Field> {
36    Arc::new(Field::new_list_field(
37        DataType::Struct(Fields::from(entry_struct_fields())),
38        false,
39    ))
40}
41
42fn entries_return_type() -> DataType {
43    DataType::List(entries_list_field())
44}
45
46fn binary_list_field() -> Arc<Field> {
47    Arc::new(Field::new_list_field(DataType::Binary, true))
48}
49
50fn binary_list_return_type() -> DataType {
51    DataType::List(binary_list_field())
52}
53
54// --- shared dictionary fast path ---
55
56/// The distinct `dict.values()` indices referenced by a non-null key in this batch.
57fn referenced_value_indices(dict_array: &DictionaryArray<Int32Type>) -> HashSet<i32> {
58    let keys = dict_array.keys();
59    (0..keys.len())
60        .filter(|&i| !keys.is_null(i))
61        .map(|i| keys.value(i))
62        .collect()
63}
64
65fn dict_values_binary_array(
66    dict_array: &DictionaryArray<Int32Type>,
67) -> Result<&GenericBinaryArray<i32>> {
68    dict_array
69        .values()
70        .as_any()
71        .downcast_ref::<GenericBinaryArray<i32>>()
72        .ok_or_else(|| DataFusionError::Internal("dictionary values are not a binary array".into()))
73}
74
75/// Returns `array` downcast to `DictionaryArray<Int32Type>` if it is exactly
76/// `Dictionary<Int32, Binary>`, the only dictionary shape the fast path handles; any other
77/// dictionary key/value type (e.g. `Dictionary<Int8, Binary>`) falls through to `None` so the
78/// caller takes the `create_binary_accessor` row-by-row path and gets its actionable
79/// `Unsupported dictionary key type: …` `Execution` error instead of an opaque downcast failure.
80pub(crate) fn binary_dict(array: &ArrayRef) -> Option<&DictionaryArray<Int32Type>> {
81    if let DataType::Dictionary(key, value) = array.data_type()
82        && key.as_ref() == &DataType::Int32
83        && value.as_ref() == &DataType::Binary
84    {
85        array.as_any().downcast_ref::<DictionaryArray<Int32Type>>()
86    } else {
87        None
88    }
89}
90
91/// `create_binary_accessor` with the standard `Invalid input type for {func_name}: …` error
92/// wrapping shared by the three list-returning JSONB UDFs' non-dictionary fallback path.
93pub(crate) fn binary_accessor_or_err(
94    array: &ArrayRef,
95    func_name: &str,
96) -> Result<Box<dyn BinaryColumnAccessor + Send>> {
97    create_binary_accessor(array).map_err(|e| {
98        DataFusionError::Execution(format!(
99            "Invalid input type for {func_name}: {e}. Expected Binary or Dictionary<Int32, Binary>"
100        ))
101    })
102}
103
104/// Build a plain `List` array positionally aligned with `dict_array.values()` — one entry per
105/// value slot, parsing only the slots referenced by a non-null key in this batch (gating on key
106/// nullity and referenced-index membership only, not `values.is_null`, matching
107/// `DictionaryBinaryAccessor` and the row-by-row path) — then expand it to row count via `take`.
108///
109/// `append_slot` is called only for referenced slots; it must call `list_builder.append(true)` or
110/// `list_builder.append(false)` exactly once (via the underlying `ListBuilder`, reached through
111/// its `&mut ListBuilder<T>` argument).
112pub(crate) fn dict_fast_path<T, F>(
113    dict_array: &DictionaryArray<Int32Type>,
114    mut list_builder: ListBuilder<T>,
115    mut append_slot: F,
116) -> Result<ArrayRef>
117where
118    T: ArrayBuilder,
119    F: FnMut(&mut ListBuilder<T>, &[u8]) -> Result<()>,
120{
121    let binary_values = dict_values_binary_array(dict_array)?;
122    let referenced = referenced_value_indices(dict_array);
123    for idx in 0..binary_values.len() {
124        if referenced.contains(&(idx as i32)) {
125            append_slot(&mut list_builder, binary_values.value(idx))?;
126        } else {
127            list_builder.append(false);
128        }
129    }
130    let list_array = list_builder.finish();
131    let taken = take(&list_array, dict_array.keys(), None)
132        .map_err(|e| DataFusionError::ArrowError(Box::new(e), None))?;
133    Ok(taken)
134}
135
136fn append_binary_slot(list_builder: &mut ListBuilder<BinaryBuilder>, items: Option<Vec<Vec<u8>>>) {
137    match items {
138        Some(values) => {
139            for v in values {
140                list_builder.values().append_value(v);
141            }
142            list_builder.append(true);
143        }
144        None => list_builder.append(false),
145    }
146}
147
148fn append_entries_to_struct_builder(
149    struct_builder: &mut StructBuilder,
150    entries: EntryList,
151) -> Result<()> {
152    for (key, value) in entries {
153        struct_builder
154            .field_builder::<StringBuilder>(0)
155            .ok_or_else(|| {
156                DataFusionError::Internal("failed to get jsonb_entries key builder".into())
157            })?
158            .append_value(&key);
159        struct_builder
160            .field_builder::<BinaryBuilder>(1)
161            .ok_or_else(|| {
162                DataFusionError::Internal("failed to get jsonb_entries value builder".into())
163            })?
164            .append_value(&value);
165        struct_builder.append(true);
166    }
167    Ok(())
168}
169
170fn append_entries_slot(
171    list_builder: &mut ListBuilder<StructBuilder>,
172    entries: Option<EntryList>,
173) -> Result<()> {
174    match entries {
175        Some(entries) => {
176            append_entries_to_struct_builder(list_builder.values(), entries)?;
177            list_builder.append(true);
178        }
179        None => list_builder.append(false),
180    }
181    Ok(())
182}
183
184/// Build `jsonb_entries`'/`jsonb_elements`'s list array: dictionary fast path when the input is
185/// `Dictionary<Int32, Binary>`, row-by-row via `create_binary_accessor` otherwise.
186fn build_binary_list<F>(array: &ArrayRef, func_name: &str, mut extract: F) -> Result<ArrayRef>
187where
188    F: FnMut(&[u8]) -> Result<Option<Vec<Vec<u8>>>>,
189{
190    if let Some(dict_array) = binary_dict(array) {
191        let list_builder = ListBuilder::new(BinaryBuilder::new()).with_field(binary_list_field());
192        return dict_fast_path(dict_array, list_builder, |builder, bytes| {
193            append_binary_slot(builder, extract(bytes)?);
194            Ok(())
195        });
196    }
197
198    let accessor = binary_accessor_or_err(array, func_name)?;
199    let mut list_builder = ListBuilder::new(BinaryBuilder::new()).with_field(binary_list_field());
200    for i in 0..accessor.len() {
201        if accessor.is_null(i) {
202            list_builder.append(false);
203        } else {
204            append_binary_slot(&mut list_builder, extract(accessor.value(i))?);
205        }
206    }
207    Ok(Arc::new(list_builder.finish()))
208}
209
210fn build_entries_list(array: &ArrayRef) -> Result<ArrayRef> {
211    if let Some(dict_array) = binary_dict(array) {
212        let capacity = dict_array.values().len();
213        let list_builder =
214            ListBuilder::new(StructBuilder::from_fields(entry_struct_fields(), capacity))
215                .with_field(entries_list_field());
216        return dict_fast_path(dict_array, list_builder, |builder, bytes| {
217            append_entries_slot(builder, object_or_array_entries(bytes)?)
218        });
219    }
220
221    let accessor = binary_accessor_or_err(array, "jsonb_entries")?;
222    let mut list_builder = ListBuilder::new(StructBuilder::from_fields(
223        entry_struct_fields(),
224        accessor.len(),
225    ))
226    .with_field(entries_list_field());
227    for i in 0..accessor.len() {
228        if accessor.is_null(i) {
229            list_builder.append(false);
230        } else {
231            append_entries_slot(
232                &mut list_builder,
233                object_or_array_entries(accessor.value(i))?,
234            )?;
235        }
236    }
237    Ok(Arc::new(list_builder.finish()))
238}
239
240// --- jsonb_entries ---
241
242/// A scalar UDF that expands a JSONB object or array into a list of `{key, value}` entries.
243///
244/// Accepts both `Binary` and `Dictionary<Int32, Binary>` inputs. For objects, `key` is the field
245/// name; for arrays, `key` is the element index as a string. Returns NULL for a JSON scalar or a
246/// NULL input (zero rows under `unnest`, same as an empty list); errors if the JSONB bytes fail
247/// to decode.
248#[derive(Debug, PartialEq, Eq, Hash)]
249pub struct JsonbEntries {
250    signature: Signature,
251}
252
253impl JsonbEntries {
254    pub fn new() -> Self {
255        Self {
256            signature: Signature::any(1, Volatility::Immutable),
257        }
258    }
259}
260
261impl Default for JsonbEntries {
262    fn default() -> Self {
263        Self::new()
264    }
265}
266
267impl ScalarUDFImpl for JsonbEntries {
268    fn name(&self) -> &str {
269        "jsonb_entries"
270    }
271
272    fn signature(&self) -> &Signature {
273        &self.signature
274    }
275
276    fn return_type(&self, _arg_types: &[DataType]) -> Result<DataType> {
277        Ok(entries_return_type())
278    }
279
280    fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result<ColumnarValue> {
281        let args = ColumnarValue::values_to_arrays(&args.args)?;
282        if args.len() != 1 {
283            return exec_err!("wrong number of arguments to jsonb_entries()");
284        }
285        Ok(ColumnarValue::Array(build_entries_list(&args[0])?))
286    }
287}
288
289/// Creates a user-defined function that expands a JSONB object or array into `{key, value}` entries.
290pub fn make_jsonb_entries_udf() -> ScalarUDF {
291    ScalarUDF::new_from_impl(JsonbEntries::new())
292}
293
294// --- jsonb_elements ---
295
296/// A scalar UDF that expands a JSONB array into a list of its elements.
297///
298/// Accepts both `Binary` and `Dictionary<Int32, Binary>` inputs. Returns NULL for a JSON object
299/// or scalar, or for a NULL input (zero rows under `unnest`, same as an empty array); errors if
300/// the JSONB bytes fail to decode.
301#[derive(Debug, PartialEq, Eq, Hash)]
302pub struct JsonbElements {
303    signature: Signature,
304}
305
306impl JsonbElements {
307    pub fn new() -> Self {
308        Self {
309            signature: Signature::any(1, Volatility::Immutable),
310        }
311    }
312}
313
314impl Default for JsonbElements {
315    fn default() -> Self {
316        Self::new()
317    }
318}
319
320impl ScalarUDFImpl for JsonbElements {
321    fn name(&self) -> &str {
322        "jsonb_elements"
323    }
324
325    fn signature(&self) -> &Signature {
326        &self.signature
327    }
328
329    fn return_type(&self, _arg_types: &[DataType]) -> Result<DataType> {
330        Ok(binary_list_return_type())
331    }
332
333    fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result<ColumnarValue> {
334        let args = ColumnarValue::values_to_arrays(&args.args)?;
335        if args.len() != 1 {
336            return exec_err!("wrong number of arguments to jsonb_elements()");
337        }
338        Ok(ColumnarValue::Array(build_binary_list(
339            &args[0],
340            "jsonb_elements",
341            array_elements,
342        )?))
343    }
344}
345
346/// Creates a user-defined function that expands a JSONB array into its elements.
347pub fn make_jsonb_elements_udf() -> ScalarUDF {
348    ScalarUDF::new_from_impl(JsonbElements::new())
349}
350
351// --- jsonb_path_elements ---
352
353/// Determine whether every row's `path` value is non-null and equal to the same string — the
354/// only case where a per-unique-blob dictionary shortcut can apply without per-row path nullity
355/// to reconcile against a per-unique-blob result. Returns `None` (disqualifying the fast path) as
356/// soon as a NULL or a differing value is seen, including when the array is empty.
357fn constant_non_null_path(paths: &StringArray) -> Option<&str> {
358    let mut constant: Option<&str> = None;
359    for i in 0..paths.len() {
360        if paths.is_null(i) {
361            return None;
362        }
363        let v = paths.value(i);
364        match constant {
365            None => constant = Some(v),
366            Some(c) if c == v => {}
367            Some(_) => return None,
368        }
369    }
370    constant
371}
372
373/// Dictionary fast path for a `path` that is constant and non-null across the whole batch: parse
374/// the path lazily, only once the first referenced slot needs it (so an all-null-key dictionary
375/// never parses `path` at all, matching `eval_jsonb_path_query`'s behavior when no row has both a
376/// non-null JSONB and a non-null path), then reuse the parsed `JsonPath` for every subsequent
377/// unique blob.
378fn dict_fast_path_path_elements<'p>(
379    dict_array: &DictionaryArray<Int32Type>,
380    path_str: &'p str,
381) -> Result<ArrayRef> {
382    let list_builder = ListBuilder::new(BinaryBuilder::new()).with_field(binary_list_field());
383    let mut parsed_path: Option<JsonPath<'p>> = None;
384    dict_fast_path(dict_array, list_builder, |builder, bytes| {
385        if parsed_path.is_none() {
386            let parsed = parse_json_path(path_str.as_bytes()).map_err(|e| {
387                DataFusionError::Execution(format!(
388                    "jsonb_path_elements: invalid JSONPath '{path_str}': {e}"
389                ))
390            })?;
391            parsed_path = Some(parsed);
392        }
393        let json_path = parsed_path.as_ref().expect("just set");
394        let items = path_select_all(bytes, json_path)?;
395        for item in items {
396            builder.values().append_value(item);
397        }
398        builder.append(true);
399        Ok(())
400    })
401}
402
403/// A scalar UDF that expands all matches of a JSONPath expression on a JSONB value into a list.
404///
405/// Accepts both `Binary` and `Dictionary<Int32, Binary>` for the JSONB argument; `path` is
406/// `Utf8`. A NULL `path` produces a NULL list for that row (matching `jsonb_path_query`). A path
407/// with zero matches produces an empty list, not NULL.
408#[derive(Debug, PartialEq, Eq, Hash)]
409pub struct JsonbPathElements {
410    signature: Signature,
411}
412
413impl JsonbPathElements {
414    pub fn new() -> Self {
415        Self {
416            signature: Signature::any(2, Volatility::Immutable),
417        }
418    }
419}
420
421impl Default for JsonbPathElements {
422    fn default() -> Self {
423        Self::new()
424    }
425}
426
427impl ScalarUDFImpl for JsonbPathElements {
428    fn name(&self) -> &str {
429        "jsonb_path_elements"
430    }
431
432    fn signature(&self) -> &Signature {
433        &self.signature
434    }
435
436    fn return_type(&self, _arg_types: &[DataType]) -> Result<DataType> {
437        Ok(binary_list_return_type())
438    }
439
440    fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result<ColumnarValue> {
441        let args = ColumnarValue::values_to_arrays(&args.args)?;
442        if args.len() != 2 {
443            return exec_err!("wrong number of arguments to jsonb_path_elements()");
444        }
445
446        let paths = args[1]
447            .as_any()
448            .downcast_ref::<StringArray>()
449            .ok_or_else(|| {
450                DataFusionError::Execution(
451                    "second argument to jsonb_path_elements must be a string".into(),
452                )
453            })?;
454
455        if let Some(dict_array) = binary_dict(&args[0])
456            && let Some(constant_path) = constant_non_null_path(paths)
457        {
458            return Ok(ColumnarValue::Array(dict_fast_path_path_elements(
459                dict_array,
460                constant_path,
461            )?));
462        }
463
464        let accessor = binary_accessor_or_err(&args[0], "jsonb_path_elements")?;
465        let mut list_builder =
466            ListBuilder::new(BinaryBuilder::new()).with_field(binary_list_field());
467        let mut path_cache: HashMap<&str, JsonPath> = HashMap::new();
468        for i in 0..accessor.len() {
469            if accessor.is_null(i) || paths.is_null(i) {
470                list_builder.append(false);
471                continue;
472            }
473            let path_str = paths.value(i);
474            if !path_cache.contains_key(path_str) {
475                let parsed = parse_json_path(path_str.as_bytes()).map_err(|e| {
476                    DataFusionError::Execution(format!(
477                        "jsonb_path_elements: invalid JSONPath '{path_str}': {e}"
478                    ))
479                })?;
480                path_cache.insert(path_str, parsed);
481            }
482            let json_path = path_cache.get(path_str).expect("just inserted");
483            let items = path_select_all(accessor.value(i), json_path)?;
484            for item in items {
485                list_builder.values().append_value(item);
486            }
487            list_builder.append(true);
488        }
489        Ok(ColumnarValue::Array(Arc::new(list_builder.finish())))
490    }
491}
492
493/// Creates a user-defined function that expands all JSONPath matches from a JSONB value into a list.
494pub fn make_jsonb_path_elements_udf() -> ScalarUDF {
495    ScalarUDF::new_from_impl(JsonbPathElements::new())
496}