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}