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}