diff --git a/COMPAT.md b/COMPAT.md index 2939ddd51..ed7b5e59f 100644 --- a/COMPAT.md +++ b/COMPAT.md @@ -360,14 +360,14 @@ Modifiers: | Function | Status | Comment | | ---------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------- | -| json(json) | Partial | | +| json(json) | Yes | | | jsonb(json) | Yes | | | json_array(value1,value2,...) | Yes | | | jsonb_array(value1,value2,...) | | | | json_array_length(json) | Yes | | | json_array_length(json,path) | Yes | | | json_error_position(json) | Yes | | -| json_extract(json,path,...) | Partial | Does not fully support unicode literal syntax and does not allow numbers > 2^127 - 1 (which SQLite truncates to i32), does not support BLOBs | +| json_extract(json,path,...) | Yes | | | jsonb_extract(json,path,...) | Yes | | | json -> path | Yes | | | json ->> path | Yes | | @@ -378,10 +378,10 @@ Modifiers: | json_patch(json1,json2) | Yes | | | jsonb_patch(json1,json2) | | | | json_pretty(json) | Partial | Shares same json(val) limitations. Also, when passing blobs for indentation, conversion is not exactly the same as in SQLite | -| json_remove(json,path,...) | Partial | Uses same json path parser as json_extract so shares same limitations. | -| jsonb_remove(json,path,...) | | | -| json_replace(json,path,value,...) | | | -| jsonb_replace(json,path,value,...) | | | +| json_remove(json,path,...) | Yes | | +| jsonb_remove(json,path,...) | Yes | | +| json_replace(json,path,value,...) | Yes | | +| jsonb_replace(json,path,value,...) | Yes | | | json_set(json,path,value,...) | Yes | | | jsonb_set(json,path,value,...) | | | | json_type(json) | Yes | | diff --git a/core/function.rs b/core/function.rs index 6a309ffc5..ffcc0087b 100644 --- a/core/function.rs +++ b/core/function.rs @@ -84,6 +84,9 @@ pub enum JsonFunc { JsonValid, JsonPatch, JsonRemove, + JsonbRemove, + JsonReplace, + JsonbReplace, JsonPretty, JsonSet, JsonQuote, @@ -110,6 +113,9 @@ impl Display for JsonFunc { Self::JsonValid => "json_valid".to_string(), Self::JsonPatch => "json_patch".to_string(), Self::JsonRemove => "json_remove".to_string(), + Self::JsonbRemove => "jsonb_remove".to_string(), + Self::JsonReplace => "json_replace".to_string(), + Self::JsonbReplace => "jsonb_replace".to_string(), Self::JsonPretty => "json_pretty".to_string(), Self::JsonSet => "json_set".to_string(), Self::JsonQuote => "json_quote".to_string(), @@ -575,6 +581,12 @@ impl Func { #[cfg(feature = "json")] "json_remove" => Ok(Self::Json(JsonFunc::JsonRemove)), #[cfg(feature = "json")] + "jsonb_remove" => Ok(Self::Json(JsonFunc::JsonbRemove)), + #[cfg(feature = "json")] + "json_replace" => Ok(Self::Json(JsonFunc::JsonReplace)), + #[cfg(feature = "json")] + "jsonb_replace" => Ok(Self::Json(JsonFunc::JsonReplace)), + #[cfg(feature = "json")] "json_pretty" => Ok(Self::Json(JsonFunc::JsonPretty)), #[cfg(feature = "json")] "json_set" => Ok(Self::Json(JsonFunc::JsonSet)), diff --git a/core/json/json_operations.rs b/core/json/json_operations.rs index 0be177dae..c54dc797c 100644 --- a/core/json/json_operations.rs +++ b/core/json/json_operations.rs @@ -1,12 +1,12 @@ -use std::collections::VecDeque; +use std::{collections::VecDeque, rc::Rc}; -use crate::{ - json::{mutate_json_by_path, Target}, - types::OwnedValue, +use crate::types::OwnedValue; + +use super::{ + convert_dbtype_to_jsonb, convert_json_to_db_type, get_json_value, json_path_from_owned_value, + json_string_to_db_type, Val, }; -use super::{convert_json_to_db_type, get_json_value, json_path::json_path, Val}; - /// Represents a single patch operation in the merge queue. /// /// Used internally by the `merge_patch` function to track the path and value @@ -155,33 +155,72 @@ pub fn json_remove(args: &[OwnedValue]) -> crate::Result { return Ok(OwnedValue::Null); } - let mut parsed_target = get_json_value(&args[0])?; - if args.len() == 1 { - return Ok(args[0].clone()); + let mut json = convert_dbtype_to_jsonb(&args[0], true)?; + for arg in &args[1..] { + if let Some(path) = json_path_from_owned_value(arg, true)? { + let _ = json.remove_by_path(&path); + } } - let paths: Result, _> = args[1..] - .iter() - .map(|path| { - if let OwnedValue::Text(path) = path { - json_path(path.as_str()) - } else { - crate::bail_constraint_error!("bad JSON path: {:?}", path.to_string()) - } - }) - .collect(); - let paths = paths?; + let el_type = json.is_valid()?; - for path in paths { - mutate_json_by_path(&mut parsed_target, path, |val| match val { - Target::Array(arr, index) => { - arr.remove(index); - } - Target::Value(val) => *val = Val::Removed, - }); + json_string_to_db_type(json, el_type, false, true) +} + +pub fn jsonb_remove(args: &[OwnedValue]) -> crate::Result { + if args.is_empty() { + return Ok(OwnedValue::Null); } - convert_json_to_db_type(&parsed_target, false) + let mut json = convert_dbtype_to_jsonb(&args[0], true)?; + for arg in &args[1..] { + if let Some(path) = json_path_from_owned_value(arg, true)? { + json.remove_by_path(&path)?; + } + } + + Ok(OwnedValue::Blob(Rc::new(json.data()))) +} + +pub fn json_replace(args: &[OwnedValue]) -> crate::Result { + if args.is_empty() { + return Ok(OwnedValue::Null); + } + + let mut json = convert_dbtype_to_jsonb(&args[0], true)?; + let other = args[1..].chunks_exact(2); + for chunk in other { + let path = json_path_from_owned_value(&chunk[0], true)?; + + let value = convert_dbtype_to_jsonb(&chunk[1], false)?; + if let Some(path) = path { + let _ = json.replace_by_path(&path, value); + } + } + + let el_type = json.is_valid()?; + + json_string_to_db_type(json, el_type, false, false) +} + +pub fn jsonb_replace(args: &[OwnedValue]) -> crate::Result { + if args.is_empty() { + return Ok(OwnedValue::Null); + } + + let mut json = convert_dbtype_to_jsonb(&args[0], true)?; + let other = args[1..].chunks_exact(2); + for chunk in other { + let path = json_path_from_owned_value(&chunk[0], true)?; + let value = convert_dbtype_to_jsonb(&chunk[1], false)?; + if let Some(path) = path { + let _ = json.replace_by_path(&path, value); + } + } + + let el_type = json.is_valid()?; + + json_string_to_db_type(json, el_type, false, true) } #[cfg(test)] diff --git a/core/json/jsonb.rs b/core/json/jsonb.rs index 14f047035..3e63a228e 100644 --- a/core/json/jsonb.rs +++ b/core/json/jsonb.rs @@ -1,5 +1,5 @@ use crate::{bail_parse_error, LimboError, Result}; -use std::{fmt::Write, str::from_utf8}; +use std::{cmp::Ordering, fmt::Write, str::from_utf8}; use super::json_path::{JsonPath, PathElement}; @@ -238,6 +238,14 @@ impl TryFrom for ElementType { type PayloadSize = usize; +type TargetPos = usize; +type KeyPos = usize; + +pub enum TraverseResult { + Value(TargetPos), + ObjectValue(TargetPos, KeyPos), +} + #[derive(Debug, Clone, Copy)] pub struct JsonbHeader(ElementType, PayloadSize); @@ -388,7 +396,7 @@ impl Jsonb { data: Vec::with_capacity(size), }; jsonb - .write_element_header(0, ElementType::ARRAY, 0) + .write_element_header(0, ElementType::ARRAY, 0, false) .unwrap(); jsonb } @@ -398,7 +406,7 @@ impl Jsonb { } pub fn finalize_array_unsafe(&mut self) -> Result<()> { - self.write_element_header(0, ElementType::ARRAY, self.len() - 1)?; + self.write_element_header(0, ElementType::ARRAY, self.len() - 1, false)?; Ok(()) } @@ -437,7 +445,6 @@ impl Jsonb { fn serialize_value(&self, string: &mut String, cursor: usize) -> Result { let (header, skip_header) = self.read_header(cursor)?; let cursor = cursor + skip_header; - let current_cursor = match header { JsonbHeader(ElementType::OBJECT, len) => self.serialize_object(string, cursor, len)?, JsonbHeader(ElementType::ARRAY, len) => self.serialize_array(string, cursor, len)?, @@ -831,7 +838,7 @@ impl Jsonb { b'"' | b'\'' => { pos = self.deserialize_string(input, pos)?; } - c if (c >= b'0' && c <= b'9') + c if (b'0'..=b'9').contains(&c) || c == b'-' || c == b'+' || c == b'.' @@ -859,7 +866,7 @@ impl Jsonb { } let header_pos = self.len(); - self.write_element_header(header_pos, ElementType::OBJECT, 0)?; + self.write_element_header(header_pos, ElementType::OBJECT, 0, false)?; let obj_start = self.len(); let mut first = true; @@ -876,7 +883,12 @@ impl Jsonb { return Ok(pos); } else { let obj_size = self.len() - obj_start; - self.write_element_header(header_pos, ElementType::OBJECT, obj_size)?; + self.write_element_header( + header_pos, + ElementType::OBJECT, + obj_size, + false, + )?; return Ok(pos); } } @@ -917,7 +929,7 @@ impl Jsonb { } let header_pos = self.len(); - self.write_element_header(header_pos, ElementType::ARRAY, 0)?; + self.write_element_header(header_pos, ElementType::ARRAY, 0, false)?; let arr_start = self.len(); let mut first = true; @@ -934,7 +946,7 @@ impl Jsonb { return Ok(pos); } else { let arr_len = self.len() - arr_start; - self.write_element_header(header_pos, ElementType::ARRAY, arr_len)?; + self.write_element_header(header_pos, ElementType::ARRAY, arr_len, false)?; return Ok(pos); } } @@ -966,8 +978,41 @@ impl Jsonb { let quoted = quote == b'"' || quote == b'\''; let mut len = 0; + if quoted { + // Try to find the closing quote and check for simple string + let mut end_pos = pos; + let is_simple = true; + + while end_pos < input.len() { + let c = input[end_pos]; + if c == quote { + // Found end of string - check if it's simple + if is_simple { + let len = end_pos - pos; + let header_pos = self.data.len(); + + // Write header and content + if len <= 11 { + self.data + .push((ElementType::TEXT as u8) | ((len as u8) << 4)); + } else { + self.write_element_header(header_pos, ElementType::TEXT, len, false)?; + } + + self.data.extend_from_slice(&input[pos..end_pos]); + return Ok(end_pos + 1); // Skip past closing quote + } + break; + } else if c == b'\\' || c < 32 { + // Not a simple string + break; + } + end_pos += 1; + } + } + // Write placeholder header to be updated later - self.write_element_header(string_start, ElementType::TEXT, 0)?; + self.write_element_header(string_start, ElementType::TEXT, 0, false)?; if pos >= input.len() { bail_parse_error!("Unexpected end of input in string"); @@ -981,7 +1026,7 @@ impl Jsonb { len += 1; if pos < input.len() && input[pos] == b':' { - self.write_element_header(string_start, element_type, len)?; + self.write_element_header(string_start, element_type, len, false)?; return Ok(pos); } } @@ -1124,7 +1169,7 @@ impl Jsonb { } // Write final header with correct type and size - self.write_element_header(string_start, element_type, len)?; + self.write_element_header(string_start, element_type, len, false)?; Ok(pos) } @@ -1137,7 +1182,7 @@ impl Jsonb { let mut is_json5 = false; // Write placeholder header - self.write_element_header(num_start, ElementType::INT, 0)?; + self.write_element_header(num_start, ElementType::INT, 0, false)?; // Handle sign if pos < input.len() && (input[pos] == b'-' || input[pos] == b'+') { @@ -1181,7 +1226,7 @@ impl Jsonb { bail_parse_error!("Invalid hex number: no digits after 0x"); } - self.write_element_header(num_start, ElementType::INT5, len)?; + self.write_element_header(num_start, ElementType::INT5, len, false)?; return Ok(pos); } else if pos < input.len() && input[pos].is_ascii_digit() { // Leading zero followed by digit is not allowed in standard JSON @@ -1214,6 +1259,7 @@ impl Jsonb { num_start, ElementType::FLOAT5, len + INFINITY_CHAR_COUNT as usize, + false, )?; return Ok(pos); @@ -1267,15 +1313,13 @@ impl Jsonb { } else { ElementType::FLOAT } + } else if is_json5 { + ElementType::INT5 } else { - if is_json5 { - ElementType::INT5 - } else { - ElementType::INT - } + ElementType::INT }; - self.write_element_header(num_start, element_type, len)?; + self.write_element_header(num_start, element_type, len, false)?; Ok(pos) } @@ -1346,8 +1390,9 @@ impl Jsonb { cursor: usize, element_type: ElementType, payload_size: usize, + size_might_change: bool, ) -> Result { - if payload_size <= 11 { + if payload_size <= 11 && !size_might_change { let header_byte = (element_type as u8) | ((payload_size as u8) << 4); if cursor == self.len() { self.data.push(header_byte); @@ -1363,44 +1408,55 @@ impl Jsonb { let header_len = header_bytes.len(); if cursor == self.len() { self.data.extend_from_slice(header_bytes); + Ok(header_len) } else { // Calculate difference in length - let old_len = 1; // We're replacing 1 byte + let old_len = if size_might_change { + let (_, offset) = self.read_header(cursor)?; + offset + } else { + 1 + }; // We're replacing 1 byte let new_len = header_bytes.len(); let diff = new_len as isize - old_len as isize; // Resize the Vec if needed - if diff > 0 { - // Need to make room - self.data.resize(self.data.len() + diff as usize, 0); + match diff.cmp(&0isize) { + Ordering::Greater => { + // Need to make room + self.data.resize(self.data.len() + diff as usize, 0); - // Shift data after cursor to the right - unsafe { - let ptr = self.data.as_mut_ptr(); - std::ptr::copy( - ptr.add(cursor + old_len), - ptr.add(cursor + new_len), - self.data.len() - cursor - new_len, - ); + // Shift data after cursor to the right + unsafe { + let ptr = self.data.as_mut_ptr(); + std::ptr::copy( + ptr.add(cursor + old_len), + ptr.add(cursor + new_len), + self.data.len() - cursor - new_len, + ); + } } - } else if diff < 0 { - // Need to shrink - unsafe { - let ptr = self.data.as_mut_ptr(); - std::ptr::copy( - ptr.add(cursor + old_len), - ptr.add(cursor + new_len), - self.data.len() - cursor - old_len, - ); + Ordering::Less => { + // Need to shrink + unsafe { + let ptr = self.data.as_mut_ptr(); + std::ptr::copy( + ptr.add(cursor + old_len), + ptr.add(cursor + new_len), + self.data.len() - cursor - old_len, + ); + } } - } + Ordering::Equal => (), + }; // Copy the header bytes for (i, &byte) in header_bytes.iter().enumerate() { self.data[cursor + i] = byte; } + + Ok(new_len) } - Ok(header_len) } fn from_str(input: &str) -> Result { @@ -1437,22 +1493,34 @@ impl Jsonb { pub fn get_by_path(&self, path: &JsonPath) -> Result<(Jsonb, ElementType)> { let mut pos = 0; let mut string_buffer = String::with_capacity(1024); + let mut nav_result: TraverseResult; + for segment in path.elements.iter() { - pos = self.navigate_to_segment(segment, pos, &mut string_buffer)?; + nav_result = self.navigate_to_segment(segment, pos, &mut string_buffer)?; + pos = match nav_result { + TraverseResult::Value(v) => v, + TraverseResult::ObjectValue(v, _) => v, + } } - let (header, skip_header) = self.read_header(pos)?; - let end = pos + skip_header + header.1; - Ok((Jsonb::from_raw_data(&self.data[pos..end]), header.0)) + let (JsonbHeader(element_type, value_size), header_size) = self.read_header(pos)?; + let end = pos + header_size + value_size; + Ok((Jsonb::from_raw_data(&self.data[pos..end]), element_type)) } pub fn get_by_path_raw(&self, path: &JsonPath) -> Result<&[u8]> { let mut pos = 0; let mut string_buffer = String::with_capacity(1024); + let mut nav_result: TraverseResult; + for segment in path.elements.iter() { - pos = self.navigate_to_segment(segment, pos, &mut string_buffer)?; + nav_result = self.navigate_to_segment(segment, pos, &mut string_buffer)?; + pos = match nav_result { + TraverseResult::Value(v) => v, + TraverseResult::ObjectValue(v, _) => v, + } } - let (header, skip_header) = self.read_header(pos)?; - let end = pos + skip_header + header.1; + let (JsonbHeader(_, value_size), header_size) = self.read_header(pos)?; + let end = pos + header_size + value_size; Ok(&self.data[pos..end]) } @@ -1472,23 +1540,138 @@ impl Jsonb { Ok(count) } + pub fn remove_by_path(&mut self, path: &JsonPath) -> Result<()> { + let mut pos = 0; + let mut string_buffer = String::with_capacity(self.len() / 2); + let element_len = path.elements.len(); + + let mut nav_stack: Vec = Vec::with_capacity(element_len); + + for segment in path.elements.iter() { + let nav_result = self.navigate_to_segment(segment, pos, &mut string_buffer)?; + pos = match nav_result { + TraverseResult::Value(v) => v, + TraverseResult::ObjectValue(v, _) => v, + }; + nav_stack.push(nav_result) + } + let target = nav_stack.pop().unwrap(); + + match target { + TraverseResult::Value(target_pos) => { + if target_pos == 0 { + let null = JsonbHeader::make_null().into_bytes(); + self.data.clear(); + self.data.push(null.as_bytes()[0]); + return Ok(()); + }; + let (target_header, offset) = self.read_header(target_pos)?; + let delta = offset + target_header.1; + self.data.drain(target_pos..target_pos + delta); + + // delta is alway positive + self.recalculate_headers(nav_stack, delta as isize)?; + } + TraverseResult::ObjectValue(target_pos, key_pos) => { + let (JsonbHeader(_, target_size), target_header_size) = + self.read_header(target_pos)?; + let delta = (target_pos + target_header_size + target_size) - key_pos; + self.data.drain(key_pos..key_pos + delta); + + // delta is alway positive + self.recalculate_headers(nav_stack, delta as isize)?; + } + } + + Ok(()) + } + + pub fn replace_by_path(&mut self, path: &JsonPath, value: Jsonb) -> Result<()> { + let mut pos = 0; + let mut string_buffer = String::with_capacity(self.len() / 2); + let element_len = path.elements.len(); + + let mut nav_stack: Vec = Vec::with_capacity(element_len); + + for segment in path.elements.iter() { + let nav_result = self.navigate_to_segment(segment, pos, &mut string_buffer)?; + pos = match nav_result { + TraverseResult::Value(v) => v, + TraverseResult::ObjectValue(v, _) => v, + }; + nav_stack.push(nav_result) + } + + let target = nav_stack.pop().expect("Target should always be present"); + + match target { + TraverseResult::Value(target_pos) | TraverseResult::ObjectValue(target_pos, _) => { + let (JsonbHeader(_, target_size), target_header_size) = + self.read_header(target_pos)?; + let target_delta = target_header_size + target_size; + let value_delta = value.len(); + let delta: isize = target_delta as isize - value_delta as isize; + self.data.splice( + target_pos..target_pos + target_delta, + value.data().into_iter(), + ); + + self.recalculate_headers(nav_stack, delta)?; + } + } + + Ok(()) + } + + fn recalculate_headers(&mut self, stack: Vec, delta: isize) -> Result<()> { + let mut delta = delta; + let stack = stack.into_iter().rev(); + + // Going backwards parent by parent and recalculating headers + for parent in stack { + let pos = match parent { + TraverseResult::Value(v) => v, + TraverseResult::ObjectValue(v, _) => v, + }; + let (JsonbHeader(value_type, value_size), header_size) = self.read_header(pos)?; + + let new_size = if delta < 0 { + value_size.saturating_add(delta.unsigned_abs()) + } else { + value_size.saturating_sub(delta as usize) + }; + + let new_header_size = self.write_element_header(pos, value_type, new_size, true)?; + + let diff = new_header_size.abs_diff(header_size); + if new_header_size <= header_size { + delta += diff as isize; + } else if new_header_size > header_size { + delta -= diff as isize; + } + } + + Ok(()) + } + fn navigate_to_segment( &self, segment: &PathElement, pos: usize, string_buffer: &mut String, - ) -> Result { + ) -> Result { let (header, skip_header) = self.read_header(pos)?; let (parent_type, parent_size) = (header.0, header.1); let mut current_pos = pos + skip_header; match segment { - PathElement::Root() => return Ok(0), + PathElement::Root() => return Ok(TraverseResult::Value(0)), PathElement::Key(path_key, is_raw) => { if parent_type != ElementType::OBJECT { bail_parse_error!("parent is not object") }; while current_pos < pos + parent_size { + let key_pos = current_pos; let (key_header, skip_header) = self.read_header(current_pos)?; let (key_type, key_size) = (key_header.0, key_header.1); current_pos += skip_header; @@ -1510,7 +1693,7 @@ impl Jsonb { )?; if compare((&string_buffer, key_type), (path_key, *is_raw)) { - return Ok(current_pos); + return Ok(TraverseResult::ObjectValue(current_pos, key_pos)); } else { current_pos = self.skip_element(current_pos)?; } @@ -1534,7 +1717,7 @@ impl Jsonb { bail_parse_error!("Index is bigger then array size"); } } - return Ok(current_pos); + return Ok(TraverseResult::Value(current_pos)); // fix this after we remove serialized json } else { let mut temp_pos = current_pos; @@ -1554,10 +1737,10 @@ impl Jsonb { bail_parse_error!("Index is bigger then array size"); } } - return Ok(current_pos); + return Ok(TraverseResult::Value(current_pos)); } } else { - return Ok(pos); + return Ok(TraverseResult::Value(pos)); } } } @@ -2306,3 +2489,284 @@ world""#, assert!(result.contains(r#""unicode":"\u00A9 2023""#)); } } + +#[cfg(test)] +mod path_mutation_tests { + use super::*; + use crate::json::json_path; + + #[test] + fn test_remove_by_path_simple_object() { + // Test removing a simple key from an object + let mut jsonb = Jsonb::from_str(r#"{"a": 1, "b": 2, "c": 3}"#).unwrap(); + let path = json_path("$.b").unwrap(); + + jsonb.remove_by_path(&path).unwrap(); + assert_eq!(jsonb.to_string().unwrap(), r#"{"a":1,"c":3}"#); + } + + #[test] + fn test_remove_by_path_array_element() { + // Test removing an element from an array + let mut jsonb = Jsonb::from_str(r#"[10, 20, 30, 40]"#).unwrap(); + let path = json_path("$[1]").unwrap(); + + jsonb.remove_by_path(&path).unwrap(); + assert_eq!(jsonb.to_string().unwrap(), r#"[10,30,40]"#); + } + + #[test] + fn test_remove_by_path_nested_object() { + // Test removing a nested property + let mut jsonb = Jsonb::from_str( + r#"{"user": {"name": "Alice", "age": 30, "email": "alice@example.com"}}"#, + ) + .unwrap(); + let path = json_path("$.user.email").unwrap(); + + jsonb.remove_by_path(&path).unwrap(); + assert_eq!( + jsonb.to_string().unwrap(), + r#"{"user":{"name":"Alice","age":30}}"# + ); + } + + #[test] + fn test_remove_by_path_nested_array() { + // Test removing an element from a nested array + let mut jsonb = Jsonb::from_str(r#"{"data": {"values": [1, 2, 3, 4, 5]}}"#).unwrap(); + let path = json_path("$.data.values[2]").unwrap(); + + jsonb.remove_by_path(&path).unwrap(); + assert_eq!( + jsonb.to_string().unwrap(), + r#"{"data":{"values":[1,2,4,5]}}"# + ); + } + + #[test] + fn test_remove_by_path_quoted_key() { + // Test removing an element with a key that contains special characters + let mut jsonb = + Jsonb::from_str(r#"{"normal": 1, "key.with.dots": 2, "key[0]": 3}"#).unwrap(); + let path = json_path(r#"$."key.with.dots""#).unwrap(); + + jsonb.remove_by_path(&path).unwrap(); + assert_eq!(jsonb.to_string().unwrap(), r#"{"normal":1,"key[0]":3}"#); + } + + #[test] + fn test_remove_by_path_entire_object() { + // Test removing the entire object + let mut jsonb = Jsonb::from_str(r#"{"a": 1, "b": 2}"#).unwrap(); + let path = json_path("$").unwrap(); + + jsonb.remove_by_path(&path).unwrap(); + assert_eq!(jsonb.to_string().unwrap(), "null"); + } + + #[test] + fn test_remove_by_path_nonexistent() { + // Test behavior when the path doesn't exist + let mut jsonb = Jsonb::from_str(r#"{"a": 1, "b": 2}"#).unwrap(); + let path = json_path("$.c").unwrap(); + + // This should return an error + let result = jsonb.remove_by_path(&path); + assert!(result.is_err()); + + // Original JSON should remain unchanged + assert_eq!(jsonb.to_string().unwrap(), r#"{"a":1,"b":2}"#); + } + + #[test] + fn test_remove_by_path_complex_nested() { + // Test removing from a complex nested structure + let mut jsonb = Jsonb::from_str( + r#" + { + "store": { + "book": [ + { + "category": "fiction", + "author": "J.R.R. Tolkien", + "title": "The Lord of the Rings", + "price": 22.99 + }, + { + "category": "fiction", + "author": "George R.R. Martin", + "title": "A Song of Ice and Fire", + "price": 19.99 + } + ], + "bicycle": { + "color": "red", + "price": 399.99 + } + } + } + "#, + ) + .unwrap(); + + // Remove the first book's title + let path = json_path("$.store.book[0].title").unwrap(); + jsonb.remove_by_path(&path).unwrap(); + + // Verify the first book no longer has a title but everything else is intact + let result = jsonb.to_string().unwrap(); + assert!(result.contains(r#""author":"J.R.R. Tolkien"#)); + assert!(result.contains(r#""price":22.99"#)); + assert!(!result.contains(r#""title":"The Lord of the Rings"#)); + assert!(result.contains(r#""title":"A Song of Ice and Fire"#)); + } + + #[test] + fn test_replace_by_path_simple_value() { + // Test replacing a simple value + let mut jsonb = Jsonb::from_str(r#"{"a": 1, "b": 2, "c": 3}"#).unwrap(); + let path = json_path("$.b").unwrap(); + let new_value = Jsonb::from_str("42").unwrap(); + + jsonb.replace_by_path(&path, new_value).unwrap(); + assert_eq!(jsonb.to_string().unwrap(), r#"{"a":1,"b":42,"c":3}"#); + } + + #[test] + fn test_replace_by_path_complex_value() { + // Test replacing with a more complex value + let mut jsonb = Jsonb::from_str(r#"{"name": "Original", "value": 123}"#).unwrap(); + let path = json_path("$.value").unwrap(); + let new_value = Jsonb::from_str(r#"{"nested": true, "array": [1, 2, 3]}"#).unwrap(); + + jsonb.replace_by_path(&path, new_value).unwrap(); + assert_eq!( + jsonb.to_string().unwrap(), + r#"{"name":"Original","value":{"nested":true,"array":[1,2,3]}}"# + ); + } + + #[test] + fn test_replace_by_path_array_element() { + // Test replacing an array element + let mut jsonb = Jsonb::from_str(r#"[10, 20, 30, 40]"#).unwrap(); + let path = json_path("$[2]").unwrap(); + let new_value = Jsonb::from_str(r#""replaced""#).unwrap(); + + jsonb.replace_by_path(&path, new_value).unwrap(); + assert_eq!(jsonb.to_string().unwrap(), r#"[10,20,"replaced",40]"#); + } + + #[test] + fn test_replace_by_path_nested_object() { + // Test replacing a property in a nested object + let mut jsonb = Jsonb::from_str(r#"{"user": {"name": "Alice", "age": 30}}"#).unwrap(); + let path = json_path("$.user.age").unwrap(); + let new_value = Jsonb::from_str("31").unwrap(); + + jsonb.replace_by_path(&path, new_value).unwrap(); + assert_eq!( + jsonb.to_string().unwrap(), + r#"{"user":{"name":"Alice","age":31}}"# + ); + } + + #[test] + fn test_replace_by_path_entire_object() { + // Test replacing the entire object + let mut jsonb = Jsonb::from_str(r#"{"old": "data"}"#).unwrap(); + let path = json_path("$").unwrap(); + let new_value = Jsonb::from_str(r#"["completely", "new", "structure"]"#).unwrap(); + + jsonb.replace_by_path(&path, new_value).unwrap(); + assert_eq!( + jsonb.to_string().unwrap(), + r#"["completely","new","structure"]"# + ); + } + + #[test] + fn test_replace_by_path_with_longer_value() { + // Test replacing with a significantly longer value to trigger header recalculation + let mut jsonb = Jsonb::from_str(r#"{"key": "short"}"#).unwrap(); + let path = json_path("$.key").unwrap(); + let new_value = Jsonb::from_str(r#""this is a much longer string that will require more storage space and potentially change the header size""#).unwrap(); + + jsonb.replace_by_path(&path, new_value).unwrap(); + assert_eq!( + jsonb.to_string().unwrap(), + r#"{"key":"this is a much longer string that will require more storage space and potentially change the header size"}"# + ); + } + + #[test] + fn test_replace_by_path_with_shorter_value() { + // Test replacing with a significantly shorter value to trigger header recalculation + let mut jsonb = Jsonb::from_str(r#"{"key": "this is a long string that takes up considerable space in the binary format"}"#).unwrap(); + let path = json_path("$.key").unwrap(); + let new_value = Jsonb::from_str(r#""short""#).unwrap(); + + jsonb.replace_by_path(&path, new_value).unwrap(); + assert_eq!(jsonb.to_string().unwrap(), r#"{"key":"short"}"#); + } + + #[test] + fn test_replace_by_path_deeply_nested() { + // Test replacing a value in a deeply nested structure + let mut jsonb = Jsonb::from_str( + r#" + { + "level1": { + "level2": { + "level3": { + "level4": { + "target": "original value" + } + } + } + } + } + "#, + ) + .unwrap(); + + let path = json_path("$.level1.level2.level3.level4.target").unwrap(); + let new_value = Jsonb::from_str(r#""replaced value""#).unwrap(); + + jsonb.replace_by_path(&path, new_value).unwrap(); + assert!(jsonb + .to_string() + .unwrap() + .contains(r#""target":"replaced value""#)); + } + + #[test] + fn test_replace_by_path_null_with_complex() { + // Test replacing a null value with a complex structure + let mut jsonb = Jsonb::from_str(r#"{"data": null}"#).unwrap(); + let path = json_path("$.data").unwrap(); + let new_value = Jsonb::from_str(r#"{"complex": {"nested": [1, 2, 3]}}"#).unwrap(); + + jsonb.replace_by_path(&path, new_value).unwrap(); + assert_eq!( + jsonb.to_string().unwrap(), + r#"{"data":{"complex":{"nested":[1,2,3]}}}"# + ); + } + + #[test] + fn test_replace_by_path_nonexistent() { + // Test behavior when the path doesn't exist + let mut jsonb = Jsonb::from_str(r#"{"a": 1, "b": 2}"#).unwrap(); + let path = json_path("$.c").unwrap(); + let new_value = Jsonb::from_str("42").unwrap(); + + // This should return an error + let result = jsonb.replace_by_path(&path, new_value); + assert!(result.is_err()); + + // Original JSON should remain unchanged + assert_eq!(jsonb.to_string().unwrap(), r#"{"a":1,"b":2}"#); + } +} diff --git a/core/json/mod.rs b/core/json/mod.rs index 71c444e81..bc3e6504b 100644 --- a/core/json/mod.rs +++ b/core/json/mod.rs @@ -8,7 +8,9 @@ mod ser; use crate::bail_constraint_error; pub use crate::json::de::from_str; use crate::json::error::Error as JsonError; -pub use crate::json::json_operations::{json_patch, json_remove}; +pub use crate::json::json_operations::{ + json_patch, json_remove, json_replace, jsonb_remove, jsonb_replace, +}; use crate::json::json_path::{json_path, JsonPath, PathElement}; pub use crate::json::ser::to_string; use crate::types::{OwnedValue, Text, TextSubtype}; @@ -74,7 +76,7 @@ pub fn get_json(json_value: &OwnedValue, indent: Option<&str>) -> crate::Result< } pub fn jsonb(json_value: &OwnedValue) -> crate::Result { - let jsonbin = convert_dbtype_to_jsonb(json_value); + let jsonbin = convert_dbtype_to_jsonb(json_value, true); match jsonbin { Ok(jsonbin) => Ok(OwnedValue::Blob(Rc::new(jsonbin.data()))), Err(_) => { @@ -83,18 +85,38 @@ pub fn jsonb(json_value: &OwnedValue) -> crate::Result { } } -fn convert_dbtype_to_jsonb(val: &OwnedValue) -> crate::Result { +fn convert_dbtype_to_jsonb(val: &OwnedValue, strict: bool) -> crate::Result { match val { - OwnedValue::Text(text) => Jsonb::from_str(text.as_str()), + OwnedValue::Text(text) => { + let res = if text.subtype == TextSubtype::Json || strict { + // Parse directly as JSON if it's already JSON subtype or strict mode is on + Jsonb::from_str(text.as_str()) + } else { + // Handle as a string literal otherwise + let mut str = text.as_str().replace('"', "\\\""); + if &str == "null" { + // Special case for "null" + Jsonb::from_str(&str) + } else { + // Quote the string to make it a JSON string + str.insert(0, '"'); + str.push('"'); + Jsonb::from_str(&str) + } + }; + res + } OwnedValue::Blob(blob) => { let json = Jsonb::from_raw_data(blob); json.is_valid()?; Ok(json) } OwnedValue::Record(_) | OwnedValue::Agg(_) => { - bail_constraint_error!("Wront number of arguments"); + bail_constraint_error!("Wrong number of arguments"); } - OwnedValue::Null => Jsonb::from_str("null"), + OwnedValue::Null => Ok(Jsonb::from_raw_data( + JsonbHeader::make_null().into_bytes().as_bytes(), + )), OwnedValue::Float(float) => { let mut buff = ryu::Buffer::new(); Jsonb::from_str(buff.format(*float)) @@ -165,7 +187,7 @@ pub fn json_array_length( json_value: &OwnedValue, json_path: Option<&OwnedValue>, ) -> crate::Result { - let json = convert_dbtype_to_jsonb(json_value)?; + let json = convert_dbtype_to_jsonb(json_value, true)?; if json_path.is_none() { let result = json.array_len()?; @@ -234,7 +256,7 @@ pub fn json_arrow_extract(value: &OwnedValue, path: &OwnedValue) -> crate::Resul } if let Some(path) = json_path_from_owned_value(path, false)? { - let json = convert_dbtype_to_jsonb(value)?; + let json = convert_dbtype_to_jsonb(value, true)?; let extracted = json.get_by_path(&path); if let Ok((json, _)) = extracted { Ok(OwnedValue::Text(Text::json(json.to_string()?))) @@ -256,10 +278,10 @@ pub fn json_arrow_shift_extract( return Ok(OwnedValue::Null); } if let Some(path) = json_path_from_owned_value(path, false)? { - let json = convert_dbtype_to_jsonb(value)?; + let json = convert_dbtype_to_jsonb(value, true)?; let extracted = json.get_by_path(&path); if let Ok((json, element_type)) = extracted { - Ok(json_string_to_db_type(json, element_type, false)?) + Ok(json_string_to_db_type(json, element_type, false, true)?) } else { Ok(OwnedValue::Null) } @@ -279,9 +301,9 @@ pub fn json_extract(value: &OwnedValue, paths: &[OwnedValue]) -> crate::Result crate::Result< } let (json, element_type) = jsonb_extract_internal(value, paths)?; - let result = json_string_to_db_type(json, element_type, true)?; + let result = json_string_to_db_type(json, element_type, false, true)?; Ok(result) } @@ -308,7 +330,7 @@ fn jsonb_extract_internal( let null = Jsonb::from_raw_data(JsonbHeader::make_null().into_bytes().as_bytes()); if paths.len() == 1 { if let Some(path) = json_path_from_owned_value(&paths[0], true)? { - let json = convert_dbtype_to_jsonb(value)?; + let json = convert_dbtype_to_jsonb(value, true)?; if let Ok((json, value_type)) = json.get_by_path(&path) { return Ok((json, value_type)); } else { @@ -319,12 +341,10 @@ fn jsonb_extract_internal( } } - let json = convert_dbtype_to_jsonb(value)?; + let json = convert_dbtype_to_jsonb(value, true)?; let mut result = Jsonb::make_empty_array(json.len()); - let paths = paths - .into_iter() - .map(|p| json_path_from_owned_value(p, true)); + let paths = paths.iter().map(|p| json_path_from_owned_value(p, true)); for path in paths { if let Some(path) = path? { let fragment = json.get_by_path_raw(&path); @@ -345,27 +365,44 @@ fn json_string_to_db_type( json: Jsonb, element_type: ElementType, raw_flag: bool, + raw_text: bool, ) -> crate::Result { let mut json_string = json.to_string()?; - if raw_flag && matches!(element_type, ElementType::ARRAY | ElementType::OBJECT) { + if raw_flag { return Ok(OwnedValue::Blob(Rc::new(json.data()))); } match element_type { ElementType::ARRAY | ElementType::OBJECT => Ok(OwnedValue::Text(Text::json(json_string))), ElementType::TEXT | ElementType::TEXT5 | ElementType::TEXTJ | ElementType::TEXTRAW => { - json_string.remove(json_string.len() - 1); - json_string.remove(0); - Ok(OwnedValue::Text(Text { - value: Rc::new(json_string.into_bytes()), - subtype: TextSubtype::Text, - })) + if raw_text { + json_string.remove(json_string.len() - 1); + json_string.remove(0); + Ok(OwnedValue::Text(Text { + value: Rc::new(json_string.into_bytes()), + subtype: TextSubtype::Json, + })) + } else { + Ok(OwnedValue::Text(Text { + value: Rc::new(json_string.into_bytes()), + subtype: TextSubtype::Text, + })) + } } ElementType::FLOAT5 | ElementType::FLOAT => Ok(OwnedValue::Float( json_string.parse().expect("Should be valid f64"), )), - ElementType::INT | ElementType::INT5 => Ok(OwnedValue::Integer( - json_string.parse().expect("Should be valid i64"), - )), + ElementType::INT | ElementType::INT5 => { + let result = i64::from_str(&json_string); + if let Ok(int) = result { + Ok(OwnedValue::Integer(int)) + } else { + let res = f64::from_str(&json_string); + match res { + Ok(num) => Ok(OwnedValue::Float(num)), + Err(_) => Ok(OwnedValue::Null), + } + } + } ElementType::TRUE => Ok(OwnedValue::Integer(1)), ElementType::FALSE => Ok(OwnedValue::Integer(0)), ElementType::NULL => Ok(OwnedValue::Null), @@ -437,13 +474,13 @@ pub fn json_type(value: &OwnedValue, path: Option<&OwnedValue>) -> crate::Result return Ok(OwnedValue::Null); } if path.is_none() { - let json = convert_dbtype_to_jsonb(value)?; + let json = convert_dbtype_to_jsonb(value, true)?; let element_type = json.is_valid()?; return Ok(OwnedValue::Text(Text::json(element_type.into()))); } if let Some(path) = json_path_from_owned_value(path.unwrap(), true)? { - let json = convert_dbtype_to_jsonb(value)?; + let json = convert_dbtype_to_jsonb(value, true)?; if let Ok((_, element_type)) = json.get_by_path(&path) { Ok(OwnedValue::Text(Text::json(element_type.into()))) @@ -501,60 +538,6 @@ enum Target<'a> { Value(&'a mut Val), } -fn mutate_json_by_path(json: &mut Val, path: JsonPath, closure: F) -> Option -where - F: FnMut(Target) -> R, -{ - find_target(json, &path).map(closure) -} - -fn find_target<'a>(json: &'a mut Val, path: &JsonPath) -> Option> { - let mut current = json; - for (i, key) in path.elements.iter().enumerate() { - let is_last = i == path.elements.len() - 1; - match key { - PathElement::Root() => continue, - PathElement::ArrayLocator(index) => match current { - Val::Array(arr) => { - if let Some(index) = match index { - Some(i) if *i < 0 => arr.len().checked_sub(i.unsigned_abs() as usize), - Some(i) => ((*i as usize) < arr.len()).then_some(*i as usize), - None => Some(arr.len()), - } { - if is_last { - return Some(Target::Array(arr, index)); - } else { - current = &mut arr[index]; - } - } else { - return None; - } - } - _ => { - return None; - } - }, - PathElement::Key(key, _) => match current { - Val::Object(obj) => { - if let Some(pos) = &obj - .iter() - .position(|(k, v)| k == key && !matches!(v, Val::Removed)) - { - let val = &mut obj[*pos].1; - current = val; - } else { - return None; - } - } - _ => { - return None; - } - }, - } - } - Some(Target::Value(current)) -} - fn create_and_mutate_json_by_path(json: &mut Val, path: JsonPath, closure: F) -> Option where F: FnOnce(Target) -> R, @@ -692,7 +675,7 @@ pub fn is_json_valid(json_value: &OwnedValue) -> OwnedValue { if matches!(json_value, OwnedValue::Null) { return OwnedValue::Null; } - convert_dbtype_to_jsonb(json_value) + convert_dbtype_to_jsonb(json_value, true) .map(|_| OwnedValue::Integer(1)) .unwrap_or(OwnedValue::Integer(0)) } @@ -1235,100 +1218,6 @@ mod tests { } } - #[test] - fn test_find_target_array() { - let mut val = Val::Array(vec![ - Val::String("first".to_string()), - Val::String("second".to_string()), - ]); - let path = JsonPath { - elements: vec![PathElement::ArrayLocator(Some(0))], - }; - - match find_target(&mut val, &path) { - Some(Target::Array(_, idx)) => assert_eq!(idx, 0), - _ => panic!("Expected Array target"), - } - } - - #[test] - fn test_find_target_negative_index() { - let mut val = Val::Array(vec![ - Val::String("first".to_string()), - Val::String("second".to_string()), - ]); - let path = JsonPath { - elements: vec![PathElement::ArrayLocator(Some(-1))], - }; - - match find_target(&mut val, &path) { - Some(Target::Array(_, idx)) => assert_eq!(idx, 1), - _ => panic!("Expected Array target"), - } - } - - #[test] - fn test_find_target_object() { - let mut val = Val::Object(vec![("key".to_string(), Val::String("value".to_string()))]); - let path = JsonPath { - elements: vec![PathElement::Key(Cow::Borrowed("key"), false)], - }; - - match find_target(&mut val, &path) { - Some(Target::Value(_)) => {} - _ => panic!("Expected Value target"), - } - } - - #[test] - fn test_find_target_removed() { - let mut val = Val::Object(vec![ - ("key".to_string(), Val::Removed), - ("key".to_string(), Val::String("value".to_string())), - ]); - let path = JsonPath { - elements: vec![PathElement::Key(Cow::Borrowed("key"), false)], - }; - - match find_target(&mut val, &path) { - Some(Target::Value(val)) => assert!(matches!(val, Val::String(_))), - _ => panic!("Expected second value, not removed"), - } - } - - #[test] - fn test_mutate_json() { - let mut val = Val::Array(vec![Val::String("test".to_string())]); - let path = JsonPath { - elements: vec![PathElement::ArrayLocator(Some(0))], - }; - - let result = mutate_json_by_path(&mut val, path, |target| match target { - Target::Array(arr, idx) => { - arr.remove(idx); - "removed" - } - _ => panic!("Expected Array target"), - }); - - assert_eq!(result, Some("removed")); - assert!(matches!(val, Val::Array(arr) if arr.is_empty())); - } - - #[test] - fn test_mutate_json_none() { - let mut val = Val::Array(vec![]); - let path = JsonPath { - elements: vec![PathElement::ArrayLocator(Some(0))], - }; - - let result: Option<()> = mutate_json_by_path(&mut val, path, |_| { - panic!("Should not be called"); - }); - - assert_eq!(result, None); - } - #[test] fn test_json_path_from_owned_value_root_strict() { let path = OwnedValue::Text(Text::new("$")); diff --git a/core/translate/expr.rs b/core/translate/expr.rs index ab9496c0e..bb1343360 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -897,7 +897,10 @@ pub fn translate_expr( JsonFunc::JsonArray | JsonFunc::JsonExtract | JsonFunc::JsonSet - | JsonFunc::JsonbExtract => translate_function( + | JsonFunc::JsonbExtract + | JsonFunc::JsonReplace + | JsonFunc::JsonbReplace + | JsonFunc::JsonbRemove => translate_function( program, args.as_deref().unwrap_or_default(), referenced_tables, diff --git a/core/vdbe/mod.rs b/core/vdbe/mod.rs index 0aa015615..6c5b368f4 100644 --- a/core/vdbe/mod.rs +++ b/core/vdbe/mod.rs @@ -53,8 +53,8 @@ use crate::{ function::JsonFunc, json::get_json, json::is_json_valid, json::json_array, json::json_array_length, json::json_arrow_extract, json::json_arrow_shift_extract, json::json_error_position, json::json_extract, json::json_object, json::json_patch, - json::json_quote, json::json_remove, json::json_set, json::json_type, json::jsonb, - json::jsonb_extract, + json::json_quote, json::json_remove, json::json_replace, json::json_set, json::json_type, + json::jsonb, json::jsonb_extract, json::jsonb_remove, json::jsonb_replace, }; use crate::{info, CheckpointStatus}; use crate::{ @@ -2193,6 +2193,7 @@ impl Program { Err(e) => return Err(e), } } + JsonFunc::JsonArrowExtract | JsonFunc::JsonArrowShiftExtract => { assert_eq!(arg_count, 2); let json = &state.registers[*start_reg]; @@ -2247,9 +2248,40 @@ impl Program { state.registers[*dest] = json_patch(target, patch)?; } JsonFunc::JsonRemove => { - state.registers[*dest] = json_remove( + if let Ok(json) = json_remove( &state.registers[*start_reg..*start_reg + arg_count], - )?; + ) { + state.registers[*dest] = json; + } else { + state.registers[*dest] = OwnedValue::Null; + } + } + JsonFunc::JsonbRemove => { + if let Ok(json) = jsonb_remove( + &state.registers[*start_reg..*start_reg + arg_count], + ) { + state.registers[*dest] = json; + } else { + state.registers[*dest] = OwnedValue::Null; + } + } + JsonFunc::JsonReplace => { + if let Ok(json) = json_replace( + &state.registers[*start_reg..*start_reg + arg_count], + ) { + state.registers[*dest] = json; + } else { + state.registers[*dest] = OwnedValue::Null; + } + } + JsonFunc::JsonbReplace => { + if let Ok(json) = jsonb_replace( + &state.registers[*start_reg..*start_reg + arg_count], + ) { + state.registers[*dest] = json; + } else { + state.registers[*dest] = OwnedValue::Null; + } } JsonFunc::JsonPretty => { let json_value = &state.registers[*start_reg]; diff --git a/testing/json.test b/testing/json.test index 45a4ecf8e..61c5e5177 100755 --- a/testing/json.test +++ b/testing/json.test @@ -980,6 +980,240 @@ do_execsql_test json_control_chars { SELECT json(jsonb('{"control": "Bell: \u0007 Backspace: \u0008 Form feed: \u000C"}')); } {{{"control":"Bell: \u0007 Backspace: \u0008 Form feed: \u000C"}}} + +# Tests for json_replace() function + +# Basic replacement tests +do_execsql_test json_replace_basic_1 { + SELECT json_replace('{"a": 1, "b": 2}', '$.a', 42) +} {{{"a":42,"b":2}}} + +do_execsql_test json_replace_basic_2 { + SELECT json_replace('{"a": 1, "b": 2}', '$.c', 3) +} {{{"a":1,"b":2}}} + +do_execsql_test json_replace_multiple_paths { + SELECT json_replace('{"a": 1, "b": 2, "c": 3}', '$.a', 10, '$.c', 30) +} {{{"a":10,"b":2,"c":30}}} + +# Testing different JSON types +do_execsql_test json_replace_string { + SELECT json_replace('{"name": "Alice"}', '$.name', 'Bob') +} {{{"name":"Bob"}}} + +do_execsql_test json_replace_number_with_string { + SELECT json_replace('{"age": 25}', '$.age', 'unknown') +} {{{"age":"unknown"}}} + +do_execsql_test json_replace_with_null { + SELECT json_replace('{"a": 1, "b": 2}', '$.a', NULL) +} {{{"a":null,"b":2}}} + +do_execsql_test json_replace_with_json_object { + SELECT json_replace('{"user": {"name": "Alice"}}', '$.user', '{"name": "Bob", "age": 30}') +} {{{"user":"{\"name\": \"Bob\", \"age\": 30}"}}} + +# Array tests +do_execsql_test json_replace_array_element { + SELECT json_replace('[1, 2, 3, 4]', '$[1]', 99) +} {{[1,99,3,4]}} + +do_execsql_test json_replace_array_negative_index { + SELECT json_replace('[1, 2, 3, 4]', '$[#-1]', 99) +} {{[1,2,3,99]}} + +do_execsql_test json_replace_array_out_of_bounds { + SELECT json_replace('[1, 2, 3]', '$[5]', 99) +} {{[1,2,3]}} + +do_execsql_test json_replace_entire_array { + SELECT json_replace('[1, 2, 3]', '$', '{"replaced": true}') +} {{"{\"replaced\": true}"}} + +# Nested structures +do_execsql_test json_replace_nested_object { + SELECT json_replace('{"user": {"name": "Alice", "age": 30}}', '$.user.age', 31) +} {{{"user":{"name":"Alice","age":31}}}} + +do_execsql_test json_replace_nested_array { + SELECT json_replace('{"data": [10, 20, 30]}', '$.data[1]', 99) +} {{{"data":[10,99,30]}}} + +do_execsql_test json_replace_deep_nesting { + SELECT json_replace( + '{"level1": {"level2": {"level3": {"value": 0}}}}', + '$.level1.level2.level3.value', + 42 + ) +} {{{"level1":{"level2":{"level3":{"value":42}}}}}} + +# Edge cases +do_execsql_test json_replace_empty_object { + SELECT json_replace('{}', '$.anything', 42) +} {{{}}} + +do_execsql_test json_replace_empty_array { + SELECT json_replace('[]', '$[0]', 42) +} {{[]}} + +do_execsql_test json_replace_quoted_key { + SELECT json_replace('{"key.with.dots": 1}', '$."key.with.dots"', 42) +} {{{"key.with.dots":42}}} + +do_execsql_test json_replace_root { + SELECT json_replace('{"old": "value"}', '$', '{"new": "object"}') +} {{"{\"new\": \"object\"}"}} + +do_execsql_test json_replace_types_boolean { + SELECT typeof(json_extract(json_replace('{"flag": null}', '$.flag', 1=1), '$.flag')) +} {{integer}} + +do_execsql_test json_replace_types_integer { + SELECT typeof(json_extract(json_replace('{"num": "text"}', '$.num', 42), '$.num')) +} {{integer}} + +do_execsql_test json_replace_types_real { + SELECT typeof(json_extract(json_replace('{"num": 1}', '$.num', 3.14), '$.num')) +} {{real}} + +do_execsql_test json_replace_types_text { + SELECT typeof(json_extract(json_replace('{"val": 1}', '$.val', 'text'), '$.val')) +} {{text}} + +# Tests for json_remove() function + +# Basic removal tests +do_execsql_test json_remove_basic_1 { + SELECT json_remove('{"a": 1, "b": 2, "c": 3}', '$.b') +} {{{"a":1,"c":3}}} + +do_execsql_test json_remove_basic_2 { + SELECT json_remove('{"a": 1, "b": 2}', '$.c') +} {{{"a":1,"b":2}}} + +do_execsql_test json_remove_multiple_paths { + SELECT json_remove('{"a": 1, "b": 2, "c": 3, "d": 4}', '$.a', '$.c') +} {{{"b":2,"d":4}}} + +# Array tests +do_execsql_test json_remove_array_element { + SELECT json_remove('[1, 2, 3, 4]', '$[1]') +} {{[1,3,4]}} + +do_execsql_test json_remove_array_negative_index { + SELECT json_remove('[1, 2, 3, 4]', '$[#-1]') +} {{[1,2,3]}} + +do_execsql_test json_remove_array_multiple_elements { + SELECT json_remove('[0, 1, 2, 3, 4, 5]', '$[1]', '$[3]') +} {{[0,2,3,5]}} + +do_execsql_test json_remove_array_out_of_bounds { + SELECT json_remove('[1, 2, 3]', '$[5]') +} {{[1,2,3]}} + +# Nested structures +do_execsql_test json_remove_nested_object { + SELECT json_remove('{"user": {"name": "Alice", "age": 30, "email": "alice@example.com"}}', '$.user.email') +} {{{"user":{"name":"Alice","age":30}}}} + +do_execsql_test json_remove_nested_array { + SELECT json_remove('{"data": [10, 20, 30, 40]}', '$.data[2]') +} {{{"data":[10,20,40]}}} + +do_execsql_test json_remove_deep_nesting { + SELECT json_remove( + '{"level1": {"level2": {"level3": {"a": 1, "b": 2, "c": 3}}}}', + '$.level1.level2.level3.b' + ) +} {{{"level1":{"level2":{"level3":{"a":1,"c":3}}}}}} + +# Edge cases +do_execsql_test json_remove_empty_object { + SELECT json_remove('{}', '$.anything') +} {{{}}} + +do_execsql_test json_remove_empty_array { + SELECT json_remove('[]', '$[0]') +} {{[]}} + +do_execsql_test json_remove_quoted_key { + SELECT json_remove('{"key.with.dots": 1, "normal": 2}', '$."key.with.dots"') +} {{{"normal":2}}} + +do_execsql_test json_remove_all_properties { + SELECT json_remove('{"a": 1, "b": 2}', '$.a', '$.b') +} {{{}}} + +do_execsql_test json_remove_all_array_elements { + SELECT json_remove('[1, 2, 3]', '$[0]', '$[0]', '$[0]') +} {{[]}} + +do_execsql_test json_remove_root { + SELECT json_remove('{"a": 1}', '$') +} {} + +# Complex example tests + +do_execsql_test json_remove_complex_1 { + SELECT json_remove( + '{"store": {"book": [ + {"category": "fiction", "author": "Herman Melville", "title": "Moby Dick", "price": 8.99}, + {"category": "fiction", "author": "J. R. R. Tolkien", "title": "The Lord of the Rings", "price": 22.99} + ], "bicycle": {"color": "red", "price": 19.95}}}', + '$.store.book[0].price', + '$.store.bicycle' + ) +} {{{"store":{"book":[{"category":"fiction","author":"Herman Melville","title":"Moby Dick"},{"category":"fiction","author":"J. R. R. Tolkien","title":"The Lord of the Rings","price":22.99}]}}}} + +do_execsql_test json_replace_complex_1 { + SELECT json_replace( + '{"store": {"book": [ + {"category": "fiction", "author": "Herman Melville", "title": "Moby Dick", "price": 8.99}, + {"category": "fiction", "author": "J. R. R. Tolkien", "title": "The Lord of the Rings", "price": 22.99} + ], "bicycle": {"color": "red", "price": 19.95}}}', + '$.store.book[0].price', 10.99, + '$.store.bicycle.color', 'blue', + '$.store.book[1].title', 'The Hobbit' + ) +} {{{"store":{"book":[{"category":"fiction","author":"Herman Melville","title":"Moby Dick","price":10.99},{"category":"fiction","author":"J. R. R. Tolkien","title":"The Hobbit","price":22.99}],"bicycle":{"color":"blue","price":19.95}}}}} + +# Combination of replace and remove +do_execsql_test json_replace_after_remove { + SELECT json_replace(json_remove('{"a": 1, "b": 2, "c": 3}', '$.a'), '$.b', 42) +} {{{"b":42,"c":3}}} + +do_execsql_test json_remove_after_replace { + SELECT json_remove(json_replace('{"a": 1, "b": 2, "c": 3}', '$.b', 42), '$.c') +} {{{"a":1,"b":42}}} + +# Tests for idempotence +do_execsql_test json_replace_idempotence { + SELECT json_replace('{"a": 1}', '$.a', 1) +} {{{"a":1}}} + +do_execsql_test json_remove_idempotence { + SELECT json_remove(json_remove('{"a": 1, "b": 2}', '$.a'), '$.a') +} {{{"b":2}}} + +# Compare with extracted values +do_execsql_test json_remove_with_extract { + SELECT json_extract(json_remove('{"a": 1, "b": 2, "c": {"d": 3}}', '$.b'), '$.c.d') +} {{3}} + +do_execsql_test json_replace_with_extract { + SELECT json_extract(json_replace('{"a": 1, "b": 2}', '$.a', 42), '$.a') +} {{42}} + +# Check for consistency between -> operator and json_extract after mutations +do_execsql_test json_replace_with_arrow { + SELECT json_replace('{"a": 1, "b": 2}', '$.a', 42) -> '$.a' +} {{42}} + +do_execsql_test json_remove_with_arrow { + SELECT json_remove('{"a": 1, "b": {"c": 3}}', '$.a') -> '$.b.c' +} {{3}} + # Escape character tests in sqlite source depend on json_valid and in some syntax that is not implemented # yet in limbo. # See https://github.com/sqlite/sqlite/blob/255548562b125e6c148bb27d49aaa01b2fe61dba/test/json102.test#L690