risingwave_expr_impl/scalar/
jsonb_delete.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 std::collections::HashSet;
16
17use jsonbb::ValueRef;
18use risingwave_common::types::{JsonbRef, ListRef};
19use risingwave_expr::{ExprError, Result, function};
20
21/// Removes a key (and its value) from a JSON object, or matching string value(s) from a JSON array.
22///
23/// Examples:
24///
25/// ```slt
26/// # remove key from object
27/// query T
28/// SELECT '{"a": "b", "c": "d"}'::jsonb - 'a';
29/// ----
30/// {"c": "d"}
31///
32/// # remove matching value from array
33/// query T
34/// SELECT '["a", "b", "c", "b"]'::jsonb - 'b';
35/// ----
36/// ["a", "c"]
37///
38/// query error cannot delete from scalar
39/// SELECT '1'::jsonb - 'b';
40/// ```
41#[function("subtract(jsonb, varchar) -> jsonb")]
42fn jsonb_remove(v: JsonbRef<'_>, key: &str, writer: &mut jsonbb::Builder) -> Result<()> {
43    match v.into() {
44        ValueRef::Object(obj) => {
45            writer.begin_object();
46            for (k, v) in obj.iter() {
47                if k != key {
48                    writer.add_string(k);
49                    writer.add_value(v);
50                }
51            }
52            writer.end_object();
53            Ok(())
54        }
55        ValueRef::Array(arr) => {
56            writer.begin_array();
57            for value in arr.iter() {
58                if value.as_str() != Some(key) {
59                    writer.add_value(value);
60                }
61            }
62            writer.end_array();
63            Ok(())
64        }
65        _ => Err(ExprError::InvalidParam {
66            name: "jsonb",
67            reason: "cannot delete from scalar".into(),
68        }),
69    }
70}
71
72/// Deletes all matching keys or array elements from the left operand.
73///
74/// Examples:
75///
76/// ```slt
77/// query T
78/// SELECT '{"a": "b", "c": "d"}'::jsonb - '{a,c}'::text[];
79/// ----
80/// {}
81///
82/// query error cannot delete from scalar
83/// SELECT '1'::jsonb - '{a,c}'::text[];
84/// ```
85#[function("subtract(jsonb, varchar[]) -> jsonb")]
86fn jsonb_remove_keys(
87    v: JsonbRef<'_>,
88    keys: ListRef<'_>,
89    writer: &mut jsonbb::Builder,
90) -> Result<()> {
91    let keys_set: HashSet<&str> = keys.iter().flatten().map(|s| s.into_utf8()).collect();
92
93    match v.into() {
94        ValueRef::Object(obj) => {
95            writer.begin_object();
96            for (k, v) in obj.iter() {
97                if !keys_set.contains(k) {
98                    writer.add_string(k);
99                    writer.add_value(v);
100                }
101            }
102            writer.end_object();
103            Ok(())
104        }
105        ValueRef::Array(arr) => {
106            writer.begin_array();
107            for value in arr.iter() {
108                if let Some(s) = value.as_str()
109                    && keys_set.contains(s)
110                {
111                    continue;
112                }
113                writer.add_value(value);
114            }
115            writer.end_array();
116            Ok(())
117        }
118        _ => Err(ExprError::InvalidParam {
119            name: "jsonb",
120            reason: "cannot delete from scalar".into(),
121        }),
122    }
123}
124
125/// Deletes the array element with the specified index (negative integers count from the end).
126/// Throws an error if JSON value is not an array.
127///
128/// Examples:
129///
130/// ```slt
131/// query T
132/// SELECT '["a", "b"]'::jsonb - 1;
133/// ----
134/// ["a"]
135///
136/// query T
137/// SELECT '["a", "b"]'::jsonb - -1;
138/// ----
139/// ["a"]
140///
141/// query T
142/// SELECT '["a", "b"]'::jsonb - 2;
143/// ----
144/// ["a", "b"]
145///
146/// query T
147/// SELECT '["a", "b"]'::jsonb - -3;
148/// ----
149/// ["a", "b"]
150///
151/// query error cannot delete from scalar
152/// SELECT '1'::jsonb - 1;
153///
154/// query error cannot delete from object using integer index
155/// SELECT '{"a": 1}'::jsonb - 1;
156/// ```
157#[function("subtract(jsonb, int4) -> jsonb")]
158fn jsonb_remove_index(v: JsonbRef<'_>, index: i32, writer: &mut jsonbb::Builder) -> Result<()> {
159    let array = match v.into() {
160        ValueRef::Array(array) => array,
161        ValueRef::Object(_) => {
162            return Err(ExprError::InvalidParam {
163                name: "jsonb",
164                reason: "cannot delete from object using integer index".into(),
165            });
166        }
167        _ => {
168            return Err(ExprError::InvalidParam {
169                name: "jsonb",
170                reason: "cannot delete from scalar".into(),
171            });
172        }
173    };
174    let Some(idx) = normalize_array_index(array.len(), index) else {
175        // out of bounds index returns original value
176        writer.add_value(v.into());
177        return Ok(());
178    };
179
180    writer.begin_array();
181    for (i, value) in array.iter().enumerate() {
182        if i != idx {
183            writer.add_value(value);
184        }
185    }
186    writer.end_array();
187    Ok(())
188}
189
190/// Deletes the field or array element at the specified path, where path elements can be
191/// either field keys or array indexes.
192///
193/// Examples:
194///
195/// ```slt
196/// # Basic test case
197/// query T
198/// SELECT '["a", {"b":1}]'::jsonb #- '{1,b}';
199/// ----
200/// ["a", {}]
201///
202/// # Invalid path
203/// query error path element at position 1 is null
204/// SELECT '["a", {"b":1}]'::jsonb #- array[null];
205///
206/// # Removing non-existent key from an object
207/// query T
208/// SELECT '{"a": 1, "b": 2}'::jsonb #- '{c}';
209/// ----
210/// {"a": 1, "b": 2}
211///
212/// # Removing an existing key from an object
213/// query T
214/// SELECT '{"a": 1, "b": 2}'::jsonb #- '{a}';
215/// ----
216/// {"b": 2}
217///
218/// # Removing an item from an array by positive index
219/// query T
220/// SELECT '["a", "b", "c"]'::jsonb #- '{1}';
221/// ----
222/// ["a", "c"]
223///
224/// # Removing an item from an array by negative index
225/// query T
226/// SELECT '["a", "b", "c"]'::jsonb #- '{-1}';
227/// ----
228/// ["a", "b"]
229///
230/// # Removing a non-existent index from an array
231/// query T
232/// SELECT '["a", "b", "c"]'::jsonb #- '{3}';
233/// ----
234/// ["a", "b", "c"]
235///
236/// # Path element is not an integer for array
237/// query error path element at position 1 is not an integer: "a"
238/// SELECT '["a", "b", "c"]'::jsonb #- '{a}';
239///
240/// # Path to deeply nested value
241/// query T
242/// SELECT '{"a": {"b": {"c": [1, 2, 3]}}}'::jsonb #- '{a,b,c,1}';
243/// ----
244/// {"a": {"b": {"c": [1, 3]}}}
245///
246/// # Path terminates early (before reaching the final depth of the JSON)
247/// query T
248/// SELECT '{"a": {"b": {"c": [1, 2, 3]}}}'::jsonb #- '{a}';
249/// ----
250/// {}
251///
252/// # Removing non-existent path in nested structure
253/// query T
254/// SELECT '{"a": {"b": {"c": [1, 2, 3]}}}'::jsonb #- '{a,x}';
255/// ----
256/// {"a": {"b": {"c": [1, 2, 3]}}}
257///
258/// # Path is longer than the depth of the JSON structure
259/// query T
260/// SELECT '{"a": 1}'::jsonb #- '{a,b}';
261/// ----
262/// {"a": 1}
263///
264/// # Edge case: Removing root
265/// query T
266/// SELECT '{"a": 1}'::jsonb #- '{}';
267/// ----
268/// {"a": 1}
269///
270/// # Edge case: Empty array
271/// query T
272/// SELECT '[]'::jsonb #- '{a}';
273/// ----
274/// []
275///
276/// # Edge case: Empty object
277/// query T
278/// SELECT '{}'::jsonb #- '{null}';
279/// ----
280/// {}
281///
282/// query error cannot delete path in scalar
283/// SELECT '1'::jsonb #- '{}';
284/// ```
285#[function("jsonb_delete_path(jsonb, varchar[]) -> jsonb")]
286fn jsonb_delete_path(
287    v: JsonbRef<'_>,
288    path: ListRef<'_>,
289    writer: &mut jsonbb::Builder,
290) -> Result<()> {
291    if v.is_scalar() {
292        return Err(ExprError::InvalidParam {
293            name: "jsonb",
294            reason: "cannot delete path in scalar".into(),
295        });
296    }
297    if path.is_empty() {
298        writer.add_value(v.into());
299        return Ok(());
300    }
301    jsonbb_remove_path(v.into(), path, 0, writer)?;
302    Ok(())
303}
304
305// Recursively remove `path[i..]` from `jsonb` and write the result to `builder`.
306// Panics if `i` is out of bounds.
307fn jsonbb_remove_path(
308    jsonb: ValueRef<'_>,
309    path: ListRef<'_>,
310    i: usize,
311    builder: &mut jsonbb::Builder,
312) -> Result<()> {
313    match jsonb {
314        ValueRef::Object(obj) => {
315            if obj.is_empty() {
316                builder.add_value(jsonb);
317                return Ok(());
318            }
319            let key = path
320                .get(i)
321                .unwrap()
322                .ok_or_else(|| ExprError::InvalidParam {
323                    name: "path",
324                    reason: format!("path element at position {} is null", i + 1).into(),
325                })?
326                .into_utf8();
327            if !obj.contains_key(key) {
328                builder.add_value(jsonb);
329                return Ok(());
330            }
331            builder.begin_object();
332            for (k, v) in obj.iter() {
333                if k != key {
334                    builder.add_string(k);
335                    builder.add_value(v);
336                    continue;
337                }
338                if i != path.len() - 1 {
339                    builder.add_string(k);
340                    // recursively remove path[i+1..] from v
341                    jsonbb_remove_path(v, path, i + 1, builder)?;
342                }
343            }
344            builder.end_object();
345            Ok(())
346        }
347        ValueRef::Array(array) => {
348            if array.is_empty() {
349                builder.add_value(jsonb);
350                return Ok(());
351            }
352            let key = path
353                .get(i)
354                .unwrap()
355                .ok_or_else(|| ExprError::InvalidParam {
356                    name: "path",
357                    reason: format!("path element at position {} is null", i + 1).into(),
358                })?
359                .into_utf8();
360            let idx = key.parse::<i32>().map_err(|_| ExprError::InvalidParam {
361                name: "path",
362                reason: format!(
363                    "path element at position {} is not an integer: \"{}\"",
364                    i + 1,
365                    key
366                )
367                .into(),
368            })?;
369            let Some(idx) = normalize_array_index(array.len(), idx) else {
370                // out of bounds index returns original value
371                builder.add_value(jsonb);
372                return Ok(());
373            };
374            builder.begin_array();
375            for (j, v) in array.iter().enumerate() {
376                if j != idx {
377                    builder.add_value(v);
378                    continue;
379                }
380                if i != path.len() - 1 {
381                    // recursively remove path[i+1..] from v
382                    jsonbb_remove_path(v, path, i + 1, builder)?;
383                }
384            }
385            builder.end_array();
386            Ok(())
387        }
388        _ => {
389            builder.add_value(jsonb);
390            Ok(())
391        }
392    }
393}
394
395/// Normalizes an array index to `0..len`.
396/// Negative indices count from the end. i.e. `-len..0 => 0..len`.
397/// Returns `None` if index is out of bounds.
398fn normalize_array_index(len: usize, index: i32) -> Option<usize> {
399    if index < -(len as i32) || index >= (len as i32) {
400        return None;
401    }
402    if index >= 0 {
403        Some(index as usize)
404    } else {
405        Some((len as i32 + index) as usize)
406    }
407}
408
409/// Recursively removes all object fields that have null values from the given JSON value.
410/// Null values that are not object fields are untouched.
411///
412/// Examples:
413///
414/// ```slt
415/// query T
416/// SELECT jsonb_strip_nulls('[{"f1":1, "f2":null}, 2, null, 3]');
417/// ----
418/// [{"f1": 1}, 2, null, 3]
419/// ```
420#[function("jsonb_strip_nulls(jsonb) -> jsonb")]
421fn jsonb_strip_nulls(v: JsonbRef<'_>, writer: &mut jsonbb::Builder) {
422    jsonbb_strip_nulls(v.into(), writer);
423}
424
425/// Recursively removes all object fields that have null values from the given JSON value.
426fn jsonbb_strip_nulls(jsonb: ValueRef<'_>, builder: &mut jsonbb::Builder) {
427    match jsonb {
428        ValueRef::Object(obj) => {
429            builder.begin_object();
430            for (k, v) in obj.iter() {
431                if let ValueRef::Null = v {
432                    continue;
433                }
434                builder.add_string(k);
435                jsonbb_strip_nulls(v, builder);
436            }
437            builder.end_object();
438        }
439        ValueRef::Array(array) => {
440            builder.begin_array();
441            for v in array.iter() {
442                jsonbb_strip_nulls(v, builder);
443            }
444            builder.end_array();
445        }
446        _ => builder.add_value(jsonb),
447    }
448}