mirror of
https://github.com/aljazceru/turso.git
synced 2026-02-07 01:04:26 +01:00
Merge 'Various JSON and JSONB function improvements' from Ihor Andrianov
Added jsonb_remove, jsonb_replace, json_replace. Updated json_remove to use jsonb under the hood. Fixed json function big numbers serialization. Add tests for new functions. Closes #1140
This commit is contained in:
12
COMPAT.md
12
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 | |
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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<OwnedValue> {
|
||||
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<Vec<_>, _> = 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<OwnedValue> {
|
||||
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<OwnedValue> {
|
||||
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<OwnedValue> {
|
||||
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)]
|
||||
|
||||
@@ -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<u8> 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<usize> {
|
||||
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<usize> {
|
||||
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<Self> {
|
||||
@@ -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<TraverseResult> = 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<TraverseResult> = 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<TraverseResult>, 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<usize> {
|
||||
) -> Result<TraverseResult> {
|
||||
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}"#);
|
||||
}
|
||||
}
|
||||
|
||||
247
core/json/mod.rs
247
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<OwnedValue> {
|
||||
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<OwnedValue> {
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_dbtype_to_jsonb(val: &OwnedValue) -> crate::Result<Jsonb> {
|
||||
fn convert_dbtype_to_jsonb(val: &OwnedValue, strict: bool) -> crate::Result<Jsonb> {
|
||||
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<OwnedValue> {
|
||||
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<O
|
||||
if paths.is_empty() {
|
||||
return Ok(OwnedValue::Null);
|
||||
}
|
||||
|
||||
let (json, element_type) = jsonb_extract_internal(value, paths)?;
|
||||
let result = json_string_to_db_type(json, element_type, false)?;
|
||||
|
||||
let result = json_string_to_db_type(json, element_type, false, true)?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@@ -296,7 +318,7 @@ pub fn jsonb_extract(value: &OwnedValue, paths: &[OwnedValue]) -> 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<OwnedValue> {
|
||||
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<F, R>(json: &mut Val, path: JsonPath, closure: F) -> Option<R>
|
||||
where
|
||||
F: FnMut(Target) -> R,
|
||||
{
|
||||
find_target(json, &path).map(closure)
|
||||
}
|
||||
|
||||
fn find_target<'a>(json: &'a mut Val, path: &JsonPath) -> Option<Target<'a>> {
|
||||
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<F, R>(json: &mut Val, path: JsonPath, closure: F) -> Option<R>
|
||||
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("$"));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user