risingwave_expr_impl/scalar/jsonb_set.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 jsonbb::ValueRef;
16use risingwave_common::types::{JsonbRef, ListRef};
17use risingwave_expr::{ExprError, Result, function};
18
19/// Returns `target` with the item designated by `path` replaced by `new_value`, or with `new_value`
20/// added if `create_if_missing` is true (which is the default) and the item designated by path does
21/// not exist. All earlier steps in the path must exist, or the `target` is returned unchanged. As
22/// with the path oriented operators, negative integers that appear in the path count from the end
23/// of JSON arrays.
24///
25/// If the last path step is an array index that is out of range, and `create_if_missing` is true,
26/// the new value is added at the beginning of the array if the index is negative, or at the end of
27/// the array if it is positive.
28///
29/// # Examples
30///
31/// ```slt
32/// query T
33/// SELECT jsonb_set('[{"f1":1,"f2":null},2,null,3]', '{0,f1}', '[2,3,4]', false);
34/// ----
35/// [{"f1": [2, 3, 4], "f2": null}, 2, null, 3]
36///
37/// query T
38/// SELECT jsonb_set('[{"f1":1,"f2":null},2]', '{0,f3}', '[2,3,4]');
39/// ----
40/// [{"f1": 1, "f2": null, "f3": [2, 3, 4]}, 2]
41/// ```
42#[function("jsonb_set(jsonb, varchar[], jsonb, boolean) -> jsonb")]
43fn jsonb_set4(
44 target: JsonbRef<'_>,
45 path: ListRef<'_>,
46 new_value: JsonbRef<'_>,
47 create_if_missing: bool,
48 writer: &mut jsonbb::Builder,
49) -> Result<()> {
50 if target.is_scalar() {
51 return Err(ExprError::InvalidParam {
52 name: "jsonb",
53 reason: "cannot set path in scalar".into(),
54 });
55 }
56 let target: ValueRef<'_> = target.into();
57 let new_value: ValueRef<'_> = new_value.into();
58 jsonbb_set_path(target, path, 0, new_value, create_if_missing, writer)?;
59 Ok(())
60}
61
62#[function("jsonb_set(jsonb, varchar[], jsonb) -> jsonb")]
63fn jsonb_set3(
64 target: JsonbRef<'_>,
65 path: ListRef<'_>,
66 new_value: JsonbRef<'_>,
67 writer: &mut jsonbb::Builder,
68) -> Result<()> {
69 jsonb_set4(target, path, new_value, true, writer)
70}
71
72/// Recursively set `path[i..]` in `target` to `new_value` and write the result to `builder`.
73///
74/// Panics if `i` is out of bounds.
75fn jsonbb_set_path(
76 target: ValueRef<'_>,
77 path: ListRef<'_>,
78 i: usize,
79 new_value: ValueRef<'_>,
80 create_if_missing: bool,
81 builder: &mut jsonbb::Builder,
82) -> Result<()> {
83 let last_step = i == path.len() - 1;
84 match target {
85 ValueRef::Object(obj) => {
86 let key = path
87 .get(i)
88 .unwrap()
89 .ok_or_else(|| ExprError::InvalidParam {
90 name: "path",
91 reason: format!("path element at position {} is null", i + 1).into(),
92 })?
93 .into_utf8();
94 builder.begin_object();
95 for (k, v) in obj.iter() {
96 builder.add_string(k);
97 if k != key {
98 builder.add_value(v);
99 } else if last_step {
100 builder.add_value(new_value);
101 } else {
102 // recursively set path[i+1..] in v
103 jsonbb_set_path(v, path, i + 1, new_value, create_if_missing, builder)?;
104 }
105 }
106 if create_if_missing && last_step && !obj.contains_key(key) {
107 builder.add_string(key);
108 builder.add_value(new_value);
109 }
110 builder.end_object();
111 Ok(())
112 }
113 ValueRef::Array(array) => {
114 let key = path
115 .get(i)
116 .unwrap()
117 .ok_or_else(|| ExprError::InvalidParam {
118 name: "path",
119 reason: format!("path element at position {} is null", i + 1).into(),
120 })?
121 .into_utf8();
122 let idx = key.parse::<i32>().map_err(|_| ExprError::InvalidParam {
123 name: "path",
124 reason: format!(
125 "path element at position {} is not an integer: \"{}\"",
126 i + 1,
127 key
128 )
129 .into(),
130 })?;
131 let Some(idx) = normalize_array_index(array.len(), idx) else {
132 // out of bounds index
133 if create_if_missing {
134 builder.begin_array();
135 // the new value is added at the beginning of the array if the index is negative
136 if idx < 0 {
137 builder.add_value(new_value);
138 }
139 for v in array.iter() {
140 builder.add_value(v);
141 }
142 // or at the end of the array if it is positive.
143 if idx >= 0 {
144 builder.add_value(new_value);
145 }
146 builder.end_array();
147 } else {
148 builder.add_value(target);
149 }
150 return Ok(());
151 };
152 builder.begin_array();
153 for (j, v) in array.iter().enumerate() {
154 if j != idx {
155 builder.add_value(v);
156 continue;
157 }
158 if last_step {
159 builder.add_value(new_value);
160 } else {
161 // recursively set path[i+1..] in v
162 jsonbb_set_path(v, path, i + 1, new_value, create_if_missing, builder)?;
163 }
164 }
165 builder.end_array();
166 Ok(())
167 }
168 _ => {
169 builder.add_value(target);
170 Ok(())
171 }
172 }
173}
174
175/// Normalizes an array index to `0..len`.
176/// Negative indices count from the end. i.e. `-len..0 => 0..len`.
177/// Returns `None` if index is out of bounds.
178fn normalize_array_index(len: usize, index: i32) -> Option<usize> {
179 if index < -(len as i32) || index >= (len as i32) {
180 return None;
181 }
182 if index >= 0 {
183 Some(index as usize)
184 } else {
185 Some((len as i32 + index) as usize)
186 }
187}