risingwave_expr_impl/scalar/
jsonb_object.rs

1// Copyright 2025 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use risingwave_common::types::ListRef;
16use risingwave_common::util::iter_util::ZipEqFast;
17use risingwave_expr::{ExprError, Result, function};
18
19/// Builds a JSON object out of a text array.
20///
21/// The array must have either exactly one dimension with an even number of members,
22/// in which case they are taken as alternating key/value pairs, or two dimensions
23/// such that each inner array has exactly two elements, which are taken as a key/value pair.
24/// All values are converted to JSON strings.
25///
26/// # Examples
27///
28/// ```slt
29/// query T
30/// select jsonb_object('{a, 1, b, def, c, 3.5}' :: text[]);
31/// ----
32/// {"a": "1", "b": "def", "c": "3.5"}
33///
34/// query error array must have even number of elements
35/// select jsonb_object('{a, 1, b, "def", c}' :: text[]);
36///
37/// query error null value not allowed for object key
38/// select jsonb_object(array[null, 'b']);
39///
40/// query T
41/// select jsonb_object(array['a', null]);
42/// ----
43/// {"a": null}
44/// ```
45#[function("jsonb_object(varchar[]) -> jsonb")]
46fn jsonb_object_1d(array: ListRef<'_>, writer: &mut jsonbb::Builder) -> Result<()> {
47    if array.len() % 2 == 1 {
48        return Err(ExprError::InvalidParam {
49            name: "array",
50            reason: "array must have even number of elements".into(),
51        });
52    }
53    writer.begin_object();
54    for [key, value] in array.iter().array_chunks() {
55        match key {
56            Some(s) => writer.add_string(s.into_utf8()),
57            None => {
58                return Err(ExprError::InvalidParam {
59                    name: "array",
60                    reason: "null value not allowed for object key".into(),
61                });
62            }
63        }
64        match value {
65            Some(s) => writer.add_string(s.into_utf8()),
66            None => writer.add_null(),
67        }
68    }
69    writer.end_object();
70    Ok(())
71}
72
73/// Builds a JSON object out of a text array.
74///
75/// The array must have either exactly one dimension with an even number of members,
76/// in which case they are taken as alternating key/value pairs, or two dimensions
77/// such that each inner array has exactly two elements, which are taken as a key/value pair.
78/// All values are converted to JSON strings.
79///
80/// # Examples
81///
82/// ```slt
83/// query T
84/// select jsonb_object('{{a, 1}, {b, def}, {c, 3.5}}' :: text[][]);
85/// ----
86/// {"a": "1", "b": "def", "c": "3.5"}
87///
88/// # FIXME: `null` should be parsed as a null value instead of a "null" string.
89/// # query error null value not allowed for object key
90/// # select jsonb_object('{{a, 1}, {null, "def"}, {c, 3.5}}' :: text[][]);
91///
92/// query error array must have two columns
93/// select jsonb_object('{{a, 1, 2}, {b, "def"}, {c, 3.5}}' :: text[][]);
94/// ```
95#[function("jsonb_object(varchar[][]) -> jsonb")]
96fn jsonb_object_2d(array: ListRef<'_>, writer: &mut jsonbb::Builder) -> Result<()> {
97    writer.begin_object();
98    for kv in array.iter() {
99        let Some(kv) = kv else {
100            return Err(ExprError::InvalidParam {
101                name: "array",
102                reason: "Unexpected array element.".into(),
103            });
104        };
105        let kv = kv.into_list();
106        if kv.len() != 2 {
107            return Err(ExprError::InvalidParam {
108                name: "array",
109                reason: "array must have two columns".into(),
110            });
111        }
112        match kv.get(0).unwrap() {
113            Some(s) => writer.add_string(s.into_utf8()),
114            None => {
115                return Err(ExprError::InvalidParam {
116                    name: "array",
117                    reason: "null value not allowed for object key".into(),
118                });
119            }
120        }
121        match kv.get(1).unwrap() {
122            Some(s) => writer.add_string(s.into_utf8()),
123            None => writer.add_null(),
124        }
125    }
126    writer.end_object();
127    Ok(())
128}
129
130/// This form of `jsonb_object` takes keys and values pairwise from separate text arrays.
131/// Otherwise it is identical to the one-argument form.
132///
133/// # Examples
134///
135/// ```slt
136/// query T
137/// select jsonb_object('{a,b}', '{1,2}');
138/// ----
139/// {"a": "1", "b": "2"}
140///
141/// query error mismatched array dimensions
142/// select jsonb_object('{a,b}', '{1,2,3}');
143///
144/// # FIXME: `null` should be parsed as a null value instead of a "null" string.
145/// # query error null value not allowed for object key
146/// # select jsonb_object('{a,null}', '{1,2}');
147/// ```
148#[function("jsonb_object(varchar[], varchar[]) -> jsonb")]
149fn jsonb_object_kv(
150    keys: ListRef<'_>,
151    values: ListRef<'_>,
152    writer: &mut jsonbb::Builder,
153) -> Result<()> {
154    if keys.len() != values.len() {
155        return Err(ExprError::InvalidParam {
156            name: "values",
157            reason: "mismatched array dimensions".into(),
158        });
159    }
160    writer.begin_object();
161    for (key, value) in keys.iter().zip_eq_fast(values.iter()) {
162        match key {
163            Some(s) => writer.add_string(s.into_utf8()),
164            None => {
165                return Err(ExprError::InvalidParam {
166                    name: "keys",
167                    reason: "null value not allowed for object key".into(),
168                });
169            }
170        }
171        match value {
172            Some(s) => writer.add_string(s.into_utf8()),
173            None => writer.add_null(),
174        }
175    }
176    writer.end_object();
177    Ok(())
178}