From 353422a25c4af4cdf167a5043a068155a9368ffd Mon Sep 17 00:00:00 2001 From: Ihor Andrianov Date: Sat, 15 Mar 2025 22:00:12 +0200 Subject: [PATCH 1/9] Add follow json path in jsonb --- core/json/jsonb.rs | 251 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 246 insertions(+), 5 deletions(-) diff --git a/core/json/jsonb.rs b/core/json/jsonb.rs index e40cda8ff..c928e6d19 100644 --- a/core/json/jsonb.rs +++ b/core/json/jsonb.rs @@ -1,6 +1,8 @@ use crate::{bail_parse_error, LimboError, Result}; use std::{fmt::Write, str::from_utf8}; +use super::json_path::{JsonPath, PathElement}; + const SIZE_MARKER_8BIT: u8 = 12; const SIZE_MARKER_16BIT: u8 = 13; const SIZE_MARKER_32BIT: u8 = 14; @@ -218,7 +220,7 @@ impl TryFrom for ElementType { type PayloadSize = usize; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub struct JsonbHeader(ElementType, PayloadSize); enum HeaderFormat { @@ -402,7 +404,7 @@ impl Jsonb { | JsonbHeader(ElementType::TEXTRAW, len) | JsonbHeader(ElementType::TEXTJ, len) | JsonbHeader(ElementType::TEXT5, len) => { - self.serialize_string(string, cursor, len, &header.0)? + self.serialize_string(string, cursor, len, &header.0, true)? } JsonbHeader(ElementType::INT, len) | JsonbHeader(ElementType::INT5, len) @@ -436,7 +438,7 @@ impl Jsonb { | ElementType::TEXTJ | ElementType::TEXT5 => { current_cursor = - self.serialize_string(string, current_cursor, len, &element_type)?; + self.serialize_string(string, current_cursor, len, &element_type, true)?; } _ => bail_parse_error!("malformed JSON"), } @@ -474,9 +476,13 @@ impl Jsonb { cursor: usize, len: usize, kind: &ElementType, + quote: bool, ) -> Result { let word_slice = &self.data[cursor..cursor + len]; - string.push('"'); + if quote { + string.push('"'); + } + match kind { // Can be serialized as is. Do not need escaping ElementType::TEXT => { @@ -630,7 +636,10 @@ impl Jsonb { unreachable!() } } - string.push('"'); + if quote { + string.push('"'); + } + Ok(cursor + len) } @@ -1367,9 +1376,122 @@ impl Jsonb { Ok(result) } + pub fn from_raw_data(data: &[u8]) -> Self { + Self::new(data.len(), Some(data)) + } + pub fn data(self) -> Vec { self.data } + + pub fn get_by_path(&self, path: &JsonPath) -> Result<(Jsonb, ElementType)> { + let mut pos = 0; + let mut string_buffer = String::with_capacity(1024); + for segment in path.elements.iter() { + pos = self.navigate_to_segment(segment, pos, &mut string_buffer)?; + } + 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)) + } + + fn navigate_to_segment( + &self, + segment: &PathElement, + pos: usize, + string_buffer: &mut String, + ) -> 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::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_header, skip_header) = self.read_header(current_pos)?; + let (key_type, key_size) = (key_header.0, key_header.1); + current_pos += skip_header; + + if matches!( + key_header.0, + ElementType::TEXT + | ElementType::TEXT5 + | ElementType::TEXTJ + | ElementType::TEXTRAW + ) { + string_buffer.clear(); + current_pos = self.serialize_string( + string_buffer, + current_pos, + key_size, + &key_type, + false, + )?; + + if compare((&string_buffer, key_type), (path_key, *is_raw)) { + return Ok(current_pos); + } else { + current_pos = self.skip_element(current_pos)?; + } + } else { + bail_parse_error!("Key is not text!") + }; + } + } + PathElement::ArrayLocator(idx) => { + if parent_type != ElementType::ARRAY { + bail_parse_error!("parent is not array"); + }; + if let Some(id) = idx { + let id = id.to_owned(); + if id > 0 { + for _ in 0..id as usize { + if current_pos < pos + parent_size { + current_pos = self.skip_element(current_pos)?; + } else { + bail_parse_error!("Index is bigger then array size"); + } + } + return Ok(current_pos); + // fix this after we remove serialized json + } else { + let mut temp_pos = current_pos; + let mut count_elements = 0; + while temp_pos < pos + parent_size { + temp_pos = self.skip_element(temp_pos)?; + count_elements += 1; + } + let corrected_idx = count_elements + id; + if corrected_idx < 0 { + bail_parse_error!("Index is bigger then array size") + } + for _ in 0..corrected_idx as usize { + if current_pos < pos + parent_size { + current_pos = self.skip_element(current_pos)?; + } else { + bail_parse_error!("Index is bigger then array size"); + } + } + return Ok(current_pos); + } + } else { + return Ok(pos); + } + } + } + + bail_parse_error!("Did not find anything") + } + + fn skip_element(&self, mut pos: usize) -> Result { + let (header, skip_header) = self.read_header(pos)?; + pos += skip_header + header.1; + Ok(pos) + } } impl std::str::FromStr for Jsonb { @@ -1380,6 +1502,125 @@ impl std::str::FromStr for Jsonb { } } +#[inline] +fn compare(key: (&str, ElementType), path_key: (&str, bool)) -> bool { + let (key, element_type) = key; + let (path_key, is_raw) = path_key; + if !is_raw && element_type == ElementType::TEXT { + if key.len() == path_key.len() { + return key == path_key; + } else { + return false; + } + } + if !is_raw { + return unescape_string(key) == path_key; + } + match element_type { + ElementType::TEXTJ | ElementType::TEXT5 | ElementType::TEXTRAW | ElementType::TEXT => { + return unescape_string(key) == unescape_string(path_key); + } + _ => {} + }; + + return false; +} + +#[inline] +pub fn unescape_string(input: &str) -> String { + let mut result = String::with_capacity(input.len()); + let mut chars = input.chars().peekable(); + + while let Some(c) = chars.next() { + if c == '\\' { + match chars.next() { + Some('n') => result.push('\n'), + Some('r') => result.push('\r'), + Some('t') => result.push('\t'), + Some('\\') => result.push('\\'), + Some('/') => result.push('/'), + Some('"') => result.push('"'), + Some('b') => result.push('\u{0008}'), + Some('f') => result.push('\u{000C}'), + + // Handle \uXXXX format (JSON style) + Some('u') => { + let mut code_point = String::new(); + for _ in 0..4 { + if let Some(hex_char) = chars.next() { + code_point.push(hex_char); + } else { + break; + } + } + + if let Ok(code) = u16::from_str_radix(&code_point, 16) { + // Check if this is a high surrogate + if (0xD800..=0xDBFF).contains(&code) { + if chars.next() == Some('\\') && chars.next() == Some('u') { + let mut low_surrogate = String::new(); + for _ in 0..4 { + if let Some(hex_char) = chars.next() { + low_surrogate.push(hex_char); + } else { + break; + } + } + + if let Ok(low_code) = u16::from_str_radix(&low_surrogate, 16) { + if (0xDC00..=0xDFFF).contains(&low_code) { + let high_ten_bits = (code - 0xD800) as u32; + let low_ten_bits = (low_code - 0xDC00) as u32; + let code_point = (high_ten_bits << 10) | low_ten_bits; + let unicode_value = code_point + 0x10000; + + if let Some(unicode_char) = char::from_u32(unicode_value) { + result.push(unicode_char); + } + } else { + // If low surrogate is invalid, just push both as separate chars + if let Some(c1) = char::from_u32(code as u32) { + result.push(c1); + } + if let Some(c2) = char::from_u32(low_code as u32) { + result.push(c2); + } + } + } + } else { + // No low surrogate, just push the high surrogate as is + if let Some(unicode_char) = char::from_u32(code as u32) { + result.push(unicode_char); + } + } + } else { + // Not a surrogate pair, just a regular Unicode character + if let Some(unicode_char) = char::from_u32(code as u32) { + result.push(unicode_char); + } + } + } + } + + Some(c) => { + // For any other escape sequence we don't recognize, + // just output the backslash and the character + result.push('\\'); + result.push(c); + } + None => { + // Handle trailing backslash + result.push('\\'); + } + } + } else { + result.push(c); + } + } + + result +} + #[inline] pub fn skip_whitespace(input: &[u8], mut pos: usize) -> usize { let len = input.len(); From e327707ac6abb8f1809527532bba1cd0563dc6da Mon Sep 17 00:00:00 2001 From: Ihor Andrianov Date: Sat, 15 Mar 2025 22:01:48 +0200 Subject: [PATCH 2/9] Fix json_path broken condition --- core/json/json_path.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/json/json_path.rs b/core/json/json_path.rs index 33631e469..1412ada9e 100644 --- a/core/json/json_path.rs +++ b/core/json/json_path.rs @@ -328,7 +328,9 @@ fn finalize_path<'a>( PPState::InKey => { if key_start < path.len() { let key = &path[key_start..]; - if key.starts_with('"') & !key.ends_with('"') { + if key.starts_with('"') && !key.ends_with('"') && key.len() > 1 + || (key.starts_with('"') && key.ends_with('"') && key.len() == 1) + { bail_parse_error!("Bad json path: {}", path) } path_components.push(PathElement::Key(Cow::Borrowed(key), false)); From dc6342c0de437703dd4927f971e605b43772da1a Mon Sep 17 00:00:00 2001 From: Ihor Andrianov Date: Sun, 16 Mar 2025 02:08:49 +0200 Subject: [PATCH 3/9] Add jsonb impl to existing json functions where possible --- core/json/json_operations.rs | 2 +- core/json/jsonb.rs | 109 +++++++++++-- core/json/mod.rs | 287 ++++++++++++++++------------------- core/types.rs | 6 +- 4 files changed, 234 insertions(+), 170 deletions(-) diff --git a/core/json/json_operations.rs b/core/json/json_operations.rs index e0e2ff09a..0be177dae 100644 --- a/core/json/json_operations.rs +++ b/core/json/json_operations.rs @@ -197,7 +197,7 @@ mod tests { } fn create_json(s: &str) -> OwnedValue { - OwnedValue::Text(Text::json(s)) + OwnedValue::Text(Text::json(s.to_string())) } #[test] diff --git a/core/json/jsonb.rs b/core/json/jsonb.rs index c928e6d19..97acd6aee 100644 --- a/core/json/jsonb.rs +++ b/core/json/jsonb.rs @@ -192,6 +192,25 @@ pub enum ElementType { RESERVED3 = 15, } +impl Into for ElementType { + fn into(self) -> String { + let result = match self { + ElementType::ARRAY => "array", + ElementType::OBJECT => "object", + ElementType::NULL => "null", + ElementType::TRUE => "true", + ElementType::FALSE => "false", + ElementType::FLOAT | ElementType::FLOAT5 => "real", + ElementType::INT | ElementType::INT5 => "integer", + ElementType::TEXT | ElementType::TEXT5 | ElementType::TEXTJ | ElementType::TEXTRAW => { + "text" + } + _ => unreachable!(), + }; + result.into() + } +} + impl TryFrom for ElementType { type Error = LimboError; @@ -223,7 +242,7 @@ type PayloadSize = usize; #[derive(Debug, Clone, Copy)] pub struct JsonbHeader(ElementType, PayloadSize); -enum HeaderFormat { +pub(crate) enum HeaderFormat { Inline([u8; 1]), // Small payloads embedded directly in the header OneByte([u8; 2]), // Medium payloads with 1-byte size field TwoBytes([u8; 3]), // Large payloads with 2-byte size field @@ -231,7 +250,7 @@ enum HeaderFormat { } impl HeaderFormat { - fn as_bytes(&self) -> &[u8] { + pub fn as_bytes(&self) -> &[u8] { match self { Self::Inline(bytes) => bytes, Self::OneByte(bytes) => bytes, @@ -246,6 +265,10 @@ impl JsonbHeader { Self(element_type, payload_size) } + pub fn make_null() -> Self { + Self(ElementType::NULL, 0) + } + fn from_slice(cursor: usize, slice: &[u8]) -> Result<(Self, usize)> { match slice.get(cursor) { Some(header_byte) => { @@ -296,7 +319,7 @@ impl JsonbHeader { } } - fn into_bytes(self) -> HeaderFormat { + pub fn into_bytes(self) -> HeaderFormat { let (element_type, payload_size) = (self.0, self.1); match payload_size { @@ -361,17 +384,36 @@ impl Jsonb { self.data.len() } + pub fn make_empty_array(size: usize) -> Self { + let mut jsonb = Self { + data: Vec::with_capacity(size), + }; + jsonb + .write_element_header(0, ElementType::ARRAY, 0) + .unwrap(); + jsonb + } + + pub fn append_to_array_unsafe(&mut self, data: &[u8]) { + self.data.extend_from_slice(data); + } + + pub fn finalize_array_unsafe(&mut self) -> Result<()> { + self.write_element_header(0, ElementType::ARRAY, self.len() - 1)?; + Ok(()) + } + fn read_header(&self, cursor: usize) -> Result<(JsonbHeader, usize)> { let (header, offset) = JsonbHeader::from_slice(cursor, &self.data)?; Ok((header, offset)) } - pub fn is_valid(&self) -> Result<()> { + pub fn is_valid(&self) -> Result { match self.read_header(0) { Ok((header, offset)) => { if let Some(_) = self.data.get(offset..offset + header.1) { - Ok(()) + Ok(header.0) } else { bail_parse_error!("malformed JSON") } @@ -1395,6 +1437,33 @@ impl Jsonb { Ok((Jsonb::from_raw_data(&self.data[pos..end]), header.0)) } + pub fn get_by_path_raw(&self, path: &JsonPath) -> Result<&[u8]> { + let mut pos = 0; + let mut string_buffer = String::with_capacity(1024); + for segment in path.elements.iter() { + pos = self.navigate_to_segment(segment, pos, &mut string_buffer)?; + } + let (header, skip_header) = self.read_header(pos)?; + let end = pos + skip_header + header.1; + Ok(&self.data[pos..end]) + } + + pub fn array_len(&self) -> Result { + let (header, header_skip) = self.read_header(0)?; + if header.0 != ElementType::ARRAY { + return Ok(0); + } + + let mut count = 0; + let mut pos = header_skip; + while pos < header_skip + header.1 { + pos = self.skip_element(pos)?; + count += 1; + } + + Ok(count) + } + fn navigate_to_segment( &self, segment: &PathElement, @@ -1448,7 +1517,8 @@ impl Jsonb { }; if let Some(id) = idx { let id = id.to_owned(); - if id > 0 { + + if id >= 0 { for _ in 0..id as usize { if current_pos < pos + parent_size { current_pos = self.skip_element(current_pos)?; @@ -1530,6 +1600,7 @@ fn compare(key: (&str, ElementType), path_key: (&str, bool)) -> bool { pub fn unescape_string(input: &str) -> String { let mut result = String::with_capacity(input.len()); let mut chars = input.chars().peekable(); + let mut code_point = String::with_capacity(5); while let Some(c) = chars.next() { if c == '\\' { @@ -1542,10 +1613,24 @@ pub fn unescape_string(input: &str) -> String { Some('"') => result.push('"'), Some('b') => result.push('\u{0008}'), Some('f') => result.push('\u{000C}'), - + Some('x') => { + code_point.clear(); + for _ in 0..2 { + if let Some(hex_char) = chars.next() { + code_point.push(hex_char); + } else { + break; + } + } + if let Ok(code) = u16::from_str_radix(&code_point, 16) { + if let Some(ch) = char::from_u32(code as u32) { + result.push(ch) + } + } + } // Handle \uXXXX format (JSON style) Some('u') => { - let mut code_point = String::new(); + code_point.clear(); for _ in 0..4 { if let Some(hex_char) = chars.next() { code_point.push(hex_char); @@ -1556,18 +1641,18 @@ pub fn unescape_string(input: &str) -> String { if let Ok(code) = u16::from_str_radix(&code_point, 16) { // Check if this is a high surrogate - if (0xD800..=0xDBFF).contains(&code) { + if matches!(code, 0xD800..=0xDBFF) { if chars.next() == Some('\\') && chars.next() == Some('u') { - let mut low_surrogate = String::new(); + code_point.clear(); for _ in 0..4 { if let Some(hex_char) = chars.next() { - low_surrogate.push(hex_char); + code_point.push(hex_char); } else { break; } } - if let Ok(low_code) = u16::from_str_radix(&low_surrogate, 16) { + if let Ok(low_code) = u16::from_str_radix(&code_point, 16) { if (0xDC00..=0xDFFF).contains(&low_code) { let high_ten_bits = (code - 0xD800) as u32; let low_ten_bits = (low_code - 0xDC00) as u32; diff --git a/core/json/mod.rs b/core/json/mod.rs index 5f63ae2e7..a3b6c5d71 100644 --- a/core/json/mod.rs +++ b/core/json/mod.rs @@ -5,6 +5,7 @@ mod json_path; mod jsonb; 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}; @@ -13,7 +14,7 @@ pub use crate::json::ser::to_string; use crate::types::{OwnedValue, Text, TextSubtype}; use crate::{bail_parse_error, json::de::ordered_object}; use indexmap::IndexMap; -use jsonb::Jsonb; +use jsonb::{ElementType, Jsonb, JsonbHeader}; use ser::to_string_pretty; use serde::{Deserialize, Serialize}; use std::borrow::Cow; @@ -49,7 +50,7 @@ pub fn get_json(json_value: &OwnedValue, indent: Option<&str>) -> crate::Result< None => to_string(&json_val)?, }; - Ok(OwnedValue::Text(Text::json(&json))) + Ok(OwnedValue::Text(Text::json(json))) } OwnedValue::Blob(b) => { let jsonbin = Jsonb::new(b.len(), Some(b)); @@ -67,25 +68,13 @@ pub fn get_json(json_value: &OwnedValue, indent: Option<&str>) -> crate::Result< None => to_string(&json_val)?, }; - Ok(OwnedValue::Text(Text::json(&json))) + Ok(OwnedValue::Text(Text::json(json))) } } } pub fn jsonb(json_value: &OwnedValue) -> crate::Result { - let jsonbin = match json_value { - OwnedValue::Null | OwnedValue::Integer(_) | OwnedValue::Float(_) | OwnedValue::Text(_) => { - Jsonb::from_str(&json_value.to_text().unwrap()) - } - OwnedValue::Blob(blob) => { - let blob = Jsonb::new(blob.len(), Some(&blob)); - blob.is_valid()?; - Ok(blob) - } - _ => { - unimplemented!() - } - }; + let jsonbin = convert_dbtype_to_jsonb(json_value); match jsonbin { Ok(jsonbin) => Ok(OwnedValue::Blob(Rc::new(jsonbin.data()))), Err(_) => { @@ -94,6 +83,26 @@ pub fn jsonb(json_value: &OwnedValue) -> crate::Result { } } +fn convert_dbtype_to_jsonb(val: &OwnedValue) -> crate::Result { + match val { + OwnedValue::Text(text) => Jsonb::from_str(text.as_str()), + 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"); + } + OwnedValue::Null => Jsonb::from_str("null"), + OwnedValue::Float(float) => { + let mut buff = ryu::Buffer::new(); + Jsonb::from_str(buff.format(*float)) + } + OwnedValue::Integer(int) => Jsonb::from_str(&int.to_string()), + } +} + fn get_json_value(json_value: &OwnedValue) -> crate::Result { match json_value { OwnedValue::Text(ref t) => match from_str::(t.as_str()) { @@ -149,29 +158,31 @@ pub fn json_array(values: &[OwnedValue]) -> crate::Result { } s.push(']'); - Ok(OwnedValue::Text(Text::json(&s))) + Ok(OwnedValue::Text(Text::json(s))) } pub fn json_array_length( json_value: &OwnedValue, json_path: Option<&OwnedValue>, ) -> crate::Result { - let json = get_json_value(json_value)?; + let json = convert_dbtype_to_jsonb(json_value)?; - let arr_val = if let Some(path) = json_path { - match json_extract_single(&json, path, true)? { - Some(val) => val, - None => return Ok(OwnedValue::Null), - } - } else { - &json - }; - - match arr_val { - Val::Array(val) => Ok(OwnedValue::Integer(val.len() as i64)), - Val::Null => Ok(OwnedValue::Null), - _ => Ok(OwnedValue::Integer(0)), + if json_path.is_none() { + let result = json.array_len()?; + return Ok(OwnedValue::Integer(result as i64)); } + + let path = json_path_from_owned_value(json_path.expect("We already checked none"), true)?; + + if let Some(path) = path { + if let Ok(len) = json + .get_by_path(&path) + .and_then(|(json, _)| json.array_len()) + { + return Ok(OwnedValue::Integer(len as i64)); + } + } + Ok(OwnedValue::Null) } pub fn json_set(json: &OwnedValue, values: &[OwnedValue]) -> crate::Result { @@ -222,13 +233,14 @@ pub fn json_arrow_extract(value: &OwnedValue, path: &OwnedValue) -> crate::Resul return Ok(OwnedValue::Null); } - let json = get_json_value(value)?; - let extracted = json_extract_single(&json, path, false)?; - - if let Some(val) = extracted { - let json = to_string(val)?; - - Ok(OwnedValue::Text(Text::json(&json))) + if let Some(path) = json_path_from_owned_value(path, false)? { + let json = convert_dbtype_to_jsonb(value)?; + let extracted = json.get_by_path(&path); + if let Ok((json, _)) = extracted { + Ok(OwnedValue::Text(Text::json(json.to_string()?))) + } else { + Ok(OwnedValue::Null) + } } else { Ok(OwnedValue::Null) } @@ -243,11 +255,17 @@ pub fn json_arrow_shift_extract( if let OwnedValue::Null = value { return Ok(OwnedValue::Null); } - - let json = get_json_value(value)?; - let extracted = json_extract_single(&json, path, false)?.unwrap_or(&Val::Null); - - convert_json_to_db_type(extracted, true) + if let Some(path) = json_path_from_owned_value(path, false)? { + let json = convert_dbtype_to_jsonb(value)?; + let extracted = json.get_by_path(&path); + if let Ok((json, element_type)) = extracted { + Ok(json_string_to_db_type(json.to_string()?, element_type)) + } else { + Ok(OwnedValue::Null) + } + } else { + Ok(OwnedValue::Null) + } } /// Extracts a JSON value from a JSON object or array. @@ -261,37 +279,66 @@ pub fn json_extract(value: &OwnedValue, paths: &[OwnedValue]) -> crate::Result { - return Ok(OwnedValue::Null); - } - _ => { - let extracted = json_extract_single(&json, path, true)?.unwrap_or(&Val::Null); - - if paths.len() == 1 && extracted == &Val::Null { - return Ok(OwnedValue::Null); - } - - result.push_str(&to_string(&extracted)?); - result.push(','); - } + return Ok(json_string_to_db_type( + expected_value.to_string()?, + value_type, + )); + } else { + return Ok(OwnedValue::Null); } } - result.pop(); // remove the final comma - result.push(']'); + let json = convert_dbtype_to_jsonb(value)?; + let mut result = Jsonb::make_empty_array(json.len()); - Ok(OwnedValue::Text(Text::json(&result))) + let paths = paths + .into_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); + if let Ok(data) = fragment { + result.append_to_array_unsafe(data); + } else { + result.append_to_array_unsafe(JsonbHeader::make_null().into_bytes().as_bytes()); + } + } else { + return Ok(OwnedValue::Null); + } + } + result.finalize_array_unsafe()?; + Ok(json_string_to_db_type( + result.to_string()?, + ElementType::ARRAY, + )) +} + +fn json_string_to_db_type(mut json: String, element_type: ElementType) -> OwnedValue { + match element_type { + ElementType::ARRAY | ElementType::OBJECT => OwnedValue::Text(Text::json(json)), + ElementType::TEXT | ElementType::TEXT5 | ElementType::TEXTJ | ElementType::TEXTRAW => { + json.remove(json.len() - 1); + json.remove(0); + OwnedValue::Text(Text { + value: Rc::new(json.into_bytes()), + subtype: TextSubtype::Text, + }) + } + ElementType::FLOAT5 | ElementType::FLOAT => { + OwnedValue::Float(json.parse().expect("Should be valid f64")) + } + ElementType::INT | ElementType::INT5 => { + OwnedValue::Integer(json.parse().expect("Should be valid i64")) + } + ElementType::TRUE => OwnedValue::Integer(1), + ElementType::FALSE => OwnedValue::Integer(0), + ElementType::NULL => OwnedValue::Null, + _ => unreachable!(), + } } /// Returns a value with type defined by SQLite documentation: @@ -324,7 +371,7 @@ fn convert_json_to_db_type(extracted: &Val, all_as_db: bool) -> crate::Result) -> crate::Result if let OwnedValue::Null = value { return Ok(OwnedValue::Null); } + if path.is_none() { + let json = convert_dbtype_to_jsonb(value)?; + let element_type = json.is_valid()?; - let json = get_json_value(value)?; + 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 = if let Some(path) = path { - match json_extract_single(&json, path, true)? { - Some(val) => val, - None => return Ok(OwnedValue::Null), + if let Ok((_, element_type)) = json.get_by_path(&path) { + return Ok(OwnedValue::Text(Text::json(element_type.into()))); + } else { + return Ok(OwnedValue::Null); } } else { - &json - }; - - let val = match json { - Val::Null => "null", - Val::Bool(v) => { - if *v { - "true" - } else { - "false" - } - } - Val::Integer(_) => "integer", - Val::Float(_) => "real", - Val::String(_) => "text", - Val::Array(_) => "array", - Val::Object(_) => "object", - Val::Removed => unreachable!(), - }; - - Ok(OwnedValue::Text(Text::json(val))) -} - -/// Returns the value at the given JSON path. If the path does not exist, it returns None. -/// If the path is an invalid path, returns an error. -/// -/// *strict* - if false, we will try to resolve the path even if it does not start with "$" -/// in a way that's compatible with the `->` and `->>` operators. See examples in the docs: -/// https://sqlite.org/json1.html#the_and_operators -fn json_extract_single<'a>( - json: &'a Val, - path: &OwnedValue, - strict: bool, -) -> crate::Result> { - let json_path = match json_path_from_owned_value(path, strict)? { - Some(path) => path, - None => return Ok(None), - }; - - let mut current_element = &Val::Null; - - for element in json_path.elements.iter() { - match element { - PathElement::Root() => { - current_element = json; - } - PathElement::Key(key, _) => match current_element { - Val::Object(map) => { - if let Some((_, value)) = map.iter().find(|(k, _)| k == key) { - current_element = value; - } else { - return Ok(None); - } - } - _ => return Ok(None), - }, - PathElement::ArrayLocator(idx) => match current_element { - Val::Array(array) => { - if let Some(mut idx) = *idx { - if idx < 0 { - idx += array.len() as i32; - } - - if idx < array.len() as i32 { - current_element = &array[idx as usize]; - } else { - return Ok(None); - } - } - } - _ => return Ok(None), - }, - } + return Ok(OwnedValue::Null); } - - Ok(Some(current_element)) } fn json_path_from_owned_value(path: &OwnedValue, strict: bool) -> crate::Result> { @@ -674,7 +653,7 @@ pub fn json_object(values: &[OwnedValue]) -> crate::Result { .collect::, _>>()?; let result = crate::json::to_string(&value_map)?; - Ok(OwnedValue::Text(Text::json(&result))) + Ok(OwnedValue::Text(Text::json(result))) } pub fn is_json_valid(json_value: &OwnedValue) -> crate::Result { @@ -866,7 +845,7 @@ mod tests { #[test] fn test_json_array_simple() { let text = OwnedValue::build_text("value1"); - let json = OwnedValue::Text(Text::json("\"value2\"")); + let json = OwnedValue::Text(Text::json("\"value2\"".to_string())); let input = vec![text, json, OwnedValue::Integer(1), OwnedValue::Float(1.1)]; let result = json_array(&input).unwrap(); @@ -1104,7 +1083,7 @@ mod tests { let text_key = OwnedValue::build_text("text_key"); let text_value = OwnedValue::build_text("text_value"); let json_key = OwnedValue::build_text("json_key"); - let json_value = OwnedValue::Text(Text::json(r#"{"json":"value","number":1}"#)); + let json_value = OwnedValue::Text(Text::json(r#"{"json":"value","number":1}"#.to_string())); let integer_key = OwnedValue::build_text("integer_key"); let integer_value = OwnedValue::Integer(1); let float_key = OwnedValue::build_text("float_key"); @@ -1138,7 +1117,7 @@ mod tests { #[test] fn test_json_object_json_value_is_rendered_as_json() { let key = OwnedValue::build_text("key"); - let value = OwnedValue::Text(Text::json(r#"{"json":"value"}"#)); + let value = OwnedValue::Text(Text::json(r#"{"json":"value"}"#.to_string())); let input = vec![key, value]; let result = json_object(&input).unwrap(); diff --git a/core/types.rs b/core/types.rs index 2b3bd2e38..593c99efd 100644 --- a/core/types.rs +++ b/core/types.rs @@ -45,9 +45,9 @@ impl Text { } } - pub fn json(value: &str) -> Self { + pub fn json(value: String) -> Self { Self { - value: Rc::new(value.as_bytes().to_vec()), + value: Rc::new(value.into_bytes()), subtype: TextSubtype::Json, } } @@ -186,7 +186,7 @@ impl OwnedValue { return Ok(OwnedValue::Null); }; if v.is_json() { - Ok(OwnedValue::Text(Text::json(text))) + Ok(OwnedValue::Text(Text::json(text.to_string()))) } else { Ok(OwnedValue::build_text(text)) } From 0b22fbd5669fe68bce64c7c29c2bd0afa53b0190 Mon Sep 17 00:00:00 2001 From: Ihor Andrianov Date: Sun, 16 Mar 2025 03:26:08 +0200 Subject: [PATCH 4/9] Add jsonb to json_valid --- core/json/mod.rs | 20 +++++++++----------- core/vdbe/mod.rs | 2 +- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/core/json/mod.rs b/core/json/mod.rs index a3b6c5d71..3690b2a5e 100644 --- a/core/json/mod.rs +++ b/core/json/mod.rs @@ -656,18 +656,16 @@ pub fn json_object(values: &[OwnedValue]) -> crate::Result { Ok(OwnedValue::Text(Text::json(result))) } -pub fn is_json_valid(json_value: &OwnedValue) -> crate::Result { - match json_value { - OwnedValue::Text(ref t) => match from_str::(t.as_str()) { - Ok(_) => Ok(OwnedValue::Integer(1)), - Err(_) => Ok(OwnedValue::Integer(0)), - }, - OwnedValue::Blob(_) => { - bail_parse_error!("Unsuported!") - } - OwnedValue::Null => Ok(OwnedValue::Null), - _ => Ok(OwnedValue::Integer(1)), +pub fn is_json_valid(json_value: &OwnedValue) -> OwnedValue { + if matches!(json_value, OwnedValue::Null) { + return OwnedValue::Null; } + let json_is_ok = convert_dbtype_to_jsonb(json_value) + .and_then(|_| { + return Ok(OwnedValue::Integer(1)); + }) + .unwrap_or(OwnedValue::Integer(0)); + json_is_ok } pub fn json_quote(value: &OwnedValue) -> crate::Result { diff --git a/core/vdbe/mod.rs b/core/vdbe/mod.rs index 7621146d6..72eb20645 100644 --- a/core/vdbe/mod.rs +++ b/core/vdbe/mod.rs @@ -2216,7 +2216,7 @@ impl Program { } JsonFunc::JsonValid => { let json_value = &state.registers[*start_reg]; - state.registers[*dest] = is_json_valid(json_value)?; + state.registers[*dest] = is_json_valid(json_value); } JsonFunc::JsonPatch => { assert_eq!(arg_count, 2); From a878738a1ee2d60d99ce71ad8d3e4956b7e6ff1e Mon Sep 17 00:00:00 2001 From: Ihor Andrianov Date: Sun, 16 Mar 2025 03:27:40 +0200 Subject: [PATCH 5/9] Fix jsonb bug with unclosed string and two commas in a row --- core/json/jsonb.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/core/json/jsonb.rs b/core/json/jsonb.rs index 97acd6aee..a497d691e 100644 --- a/core/json/jsonb.rs +++ b/core/json/jsonb.rs @@ -884,6 +884,9 @@ impl Jsonb { b',' if !first => { pos += 1; // consume ',' pos = skip_whitespace(input, pos); + if input[pos] == b',' { + bail_parse_error!("2 commas in a row are not allowed") + } } _ => { // Parse key (must be string) @@ -898,8 +901,11 @@ impl Jsonb { pos = skip_whitespace(input, pos); // Parse value - can be any JSON value including another object - pos = self.deserialize_value(input, pos, depth)?; - + pos = self.deserialize_value(input, pos, depth + 1)?; + pos = skip_whitespace(input, pos); + if pos < input.len() && !matches!(input[pos], b',' | b'}') { + bail_parse_error!("Should be , or }}") + } first = false; } } @@ -989,6 +995,8 @@ impl Jsonb { if quoted && c == quote { break; // End of string + } else if !quoted && (c == b'"' || c == b'\'') { + bail_parse_error!("Something gone wrong") } else if c == b'\\' { // Handle escape sequences if pos >= input.len() { @@ -1095,6 +1103,7 @@ impl Jsonb { pos += 2; element_type = ElementType::TEXT5; } + _ => { bail_parse_error!("Invalid escape sequence: \\{}", esc as char); } @@ -2068,6 +2077,10 @@ world""#, assert!(Jsonb::from_str("}").is_err()); assert!(Jsonb::from_str("]").is_err()); + assert!(Jsonb::from_str(r#"{"a":"55,"b":72}"#).is_err()); + + assert!(Jsonb::from_str(r#"{"a":"55",,"b":72}"#).is_err()); + // Unclosed string assert!(Jsonb::from_str(r#"{"key":"value"#).is_err()); From a3a93763472fbef0a81dcef5d08f20eaa693a3da Mon Sep 17 00:00:00 2001 From: Ihor Andrianov Date: Sun, 16 Mar 2025 03:36:02 +0200 Subject: [PATCH 6/9] unblock some tests --- testing/json.test | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/testing/json.test b/testing/json.test index c6dc99553..45a4ecf8e 100755 --- a/testing/json.test +++ b/testing/json.test @@ -291,16 +291,15 @@ do_execsql_test json_extract_object_3 { SELECT json_extract('{"a": [1,2,3]}', '$.a', '$.a[0]', '$.a[1]', null, '$.a[3]') } {{}} -# TODO: fix me - this passes on SQLite and needs to be fixed in Limbo. -# \x61 is the ASCII code for 'a' -# do_execsql_test json_extract_with_escaping { -# SELECT json_extract('{"\x61": 1}', '$.a') -# } {{1}} -# TODO: fix me - this passes on SQLite and needs to be fixed in Limbo. -#do_execsql_test json_extract_with_escaping_2 { -# SELECT json_extract('{"\x61": 1}', '$.\x61') -#} {{1}} +# \x61 is the ASCII code for 'a' +do_execsql_test json_extract_with_escaping { + SELECT json_extract('{"\x61": 1}', '$.a') +} {{1}} + +do_execsql_test json_extract_with_escaping_2 { + SELECT json_extract('{"\x61": 1}', '$."\x61"') +} {{1}} do_execsql_test json_extract_null_path { SELECT json_extract(1, null) @@ -406,10 +405,10 @@ do_execsql_test json_arrow_shift_implicit_root_path { SELECT '{"a":1}' ->> 'a'; } {{1}} -# TODO: fix me after rebasing on top of https://github.com/tursodatabase/limbo/pull/631 - use the Option value in json_extract_single -#do_execsql_test json_arrow_implicit_root_path_undefined_key { -# SELECT '{"a":1}' -> 'x'; -#} {{}} + +do_execsql_test json_arrow_implicit_root_path_undefined_key { + SELECT '{"a":1}' -> 'x'; +} {{}} do_execsql_test json_arrow_shift_implicit_root_path_undefined_key { SELECT '{"a":1}' ->> 'x'; @@ -476,10 +475,9 @@ do_execsql_test json_arrow_shift_array { SELECT '[1,2,3]' ->> '$' } {{[1,2,3]}} -# TODO: fix me - this passes on SQLite and needs to be fixed in Limbo. -#do_execsql_test json_extract_quote { -# SELECT json_extract('{"\"":1 }', '$.\"') -#} {{1}} +do_execsql_test json_extract_quote { + SELECT json_extract('{"\"":1 }', '$."\""') +} {{1}} # Overflows 2**32 is equivalent to 0 do_execsql_test json_extract_overflow_int32_1 { From 23d7d82b6cb3ce0aab266db0c43957a90fcff15b Mon Sep 17 00:00:00 2001 From: Ihor Andrianov Date: Sun, 16 Mar 2025 15:14:29 +0200 Subject: [PATCH 7/9] add jsonb_extract function --- core/function.rs | 4 ++ core/json/jsonb.rs | 2 +- core/json/mod.rs | 92 ++++++++++++++++++++++++++++-------------- core/translate/expr.rs | 21 +++++----- core/vdbe/mod.rs | 18 +++++++++ 5 files changed, 96 insertions(+), 41 deletions(-) diff --git a/core/function.rs b/core/function.rs index 333266eea..6a309ffc5 100644 --- a/core/function.rs +++ b/core/function.rs @@ -77,6 +77,7 @@ pub enum JsonFunc { JsonArrowExtract, JsonArrowShiftExtract, JsonExtract, + JsonbExtract, JsonObject, JsonType, JsonErrorPosition, @@ -99,6 +100,7 @@ impl Display for JsonFunc { Self::Jsonb => "jsonb".to_string(), Self::JsonArray => "json_array".to_string(), Self::JsonExtract => "json_extract".to_string(), + Self::JsonbExtract => "jsonb_extract".to_string(), Self::JsonArrayLength => "json_array_length".to_string(), Self::JsonArrowExtract => "->".to_string(), Self::JsonArrowShiftExtract => "->>".to_string(), @@ -559,6 +561,8 @@ impl Func { #[cfg(feature = "json")] "json_extract" => Ok(Func::Json(JsonFunc::JsonExtract)), #[cfg(feature = "json")] + "jsonb_extract" => Ok(Func::Json(JsonFunc::JsonbExtract)), + #[cfg(feature = "json")] "json_object" => Ok(Func::Json(JsonFunc::JsonObject)), #[cfg(feature = "json")] "json_type" => Ok(Func::Json(JsonFunc::JsonType)), diff --git a/core/json/jsonb.rs b/core/json/jsonb.rs index a497d691e..fa8218c4b 100644 --- a/core/json/jsonb.rs +++ b/core/json/jsonb.rs @@ -1563,7 +1563,7 @@ impl Jsonb { } } - bail_parse_error!("Did not find anything") + bail_parse_error!("Not found") } fn skip_element(&self, mut pos: usize) -> Result { diff --git a/core/json/mod.rs b/core/json/mod.rs index 3690b2a5e..692dddb7a 100644 --- a/core/json/mod.rs +++ b/core/json/mod.rs @@ -259,7 +259,7 @@ pub fn json_arrow_shift_extract( let json = convert_dbtype_to_jsonb(value)?; let extracted = json.get_by_path(&path); if let Ok((json, element_type)) = extracted { - Ok(json_string_to_db_type(json.to_string()?, element_type)) + Ok(json_string_to_db_type(json, element_type, false)?) } else { Ok(OwnedValue::Null) } @@ -278,17 +278,44 @@ pub fn json_extract(value: &OwnedValue, paths: &[OwnedValue]) -> crate::Result crate::Result { + if let OwnedValue::Null = value { + return Ok(OwnedValue::Null); + } + + 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, true)?; + + Ok(result) +} + +fn jsonb_extract_internal( + value: &OwnedValue, + paths: &[OwnedValue], +) -> crate::Result<(Jsonb, ElementType)> { + 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 (expected_value, value_type) = json.get_by_path(&path)?; - - return Ok(json_string_to_db_type( - expected_value.to_string()?, - value_type, - )); + if let Ok((json, value_type)) = json.get_by_path(&path) { + return Ok((json, value_type)); + } else { + return Ok((null, ElementType::NULL)); + } } else { - return Ok(OwnedValue::Null); + return Ok((null, ElementType::NULL)); } } @@ -307,36 +334,41 @@ pub fn json_extract(value: &OwnedValue, paths: &[OwnedValue]) -> crate::Result OwnedValue { +fn json_string_to_db_type( + json: Jsonb, + element_type: ElementType, + raw_flag: bool, +) -> crate::Result { + let mut json_string = json.to_string()?; + if raw_flag && matches!(element_type, ElementType::ARRAY | ElementType::OBJECT) { + return Ok(OwnedValue::Blob(Rc::new(json.data()))); + } match element_type { - ElementType::ARRAY | ElementType::OBJECT => OwnedValue::Text(Text::json(json)), + ElementType::ARRAY | ElementType::OBJECT => Ok(OwnedValue::Text(Text::json(json_string))), ElementType::TEXT | ElementType::TEXT5 | ElementType::TEXTJ | ElementType::TEXTRAW => { - json.remove(json.len() - 1); - json.remove(0); - OwnedValue::Text(Text { - value: Rc::new(json.into_bytes()), + 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, - }) + })) } - ElementType::FLOAT5 | ElementType::FLOAT => { - OwnedValue::Float(json.parse().expect("Should be valid f64")) - } - ElementType::INT | ElementType::INT5 => { - OwnedValue::Integer(json.parse().expect("Should be valid i64")) - } - ElementType::TRUE => OwnedValue::Integer(1), - ElementType::FALSE => OwnedValue::Integer(0), - ElementType::NULL => OwnedValue::Null, + 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::TRUE => Ok(OwnedValue::Integer(1)), + ElementType::FALSE => Ok(OwnedValue::Integer(0)), + ElementType::NULL => Ok(OwnedValue::Null), _ => unreachable!(), } } diff --git a/core/translate/expr.rs b/core/translate/expr.rs index 24e7418b3..ab9496c0e 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -894,16 +894,17 @@ pub fn translate_expr( func_ctx, ) } - JsonFunc::JsonArray | JsonFunc::JsonExtract | JsonFunc::JsonSet => { - translate_function( - program, - args.as_deref().unwrap_or_default(), - referenced_tables, - resolver, - target_register, - func_ctx, - ) - } + JsonFunc::JsonArray + | JsonFunc::JsonExtract + | JsonFunc::JsonSet + | JsonFunc::JsonbExtract => translate_function( + program, + args.as_deref().unwrap_or_default(), + referenced_tables, + resolver, + target_register, + func_ctx, + ), JsonFunc::JsonArrowExtract | JsonFunc::JsonArrowShiftExtract => { unreachable!( "These two functions are only reachable via the -> and ->> operators" diff --git a/core/vdbe/mod.rs b/core/vdbe/mod.rs index 72eb20645..3f5729d0e 100644 --- a/core/vdbe/mod.rs +++ b/core/vdbe/mod.rs @@ -53,6 +53,7 @@ use crate::{ 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, }; use crate::{info, CheckpointStatus}; use crate::{ @@ -2172,6 +2173,23 @@ impl Program { Err(e) => return Err(e), } } + JsonFunc::JsonbExtract => { + let result = match arg_count { + 0 => jsonb_extract(&OwnedValue::Null, &[]), + _ => { + let val = &state.registers[*start_reg]; + let reg_values = &state.registers + [*start_reg + 1..*start_reg + arg_count]; + + jsonb_extract(val, reg_values) + } + }; + + match result { + Ok(json) => state.registers[*dest] = json, + Err(e) => return Err(e), + } + } JsonFunc::JsonArrowExtract | JsonFunc::JsonArrowShiftExtract => { assert_eq!(arg_count, 2); let json = &state.registers[*start_reg]; From 328ebfcd9ef63a4a92e5f103f8235ee50862bb9c Mon Sep 17 00:00:00 2001 From: Ihor Andrianov Date: Sun, 16 Mar 2025 15:31:06 +0200 Subject: [PATCH 8/9] clippy --- core/json/jsonb.rs | 29 ++++++++++++++--------------- core/json/mod.rs | 17 +++++++---------- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/core/json/jsonb.rs b/core/json/jsonb.rs index fa8218c4b..14f047035 100644 --- a/core/json/jsonb.rs +++ b/core/json/jsonb.rs @@ -192,22 +192,21 @@ pub enum ElementType { RESERVED3 = 15, } -impl Into for ElementType { - fn into(self) -> String { - let result = match self { - ElementType::ARRAY => "array", - ElementType::OBJECT => "object", - ElementType::NULL => "null", - ElementType::TRUE => "true", - ElementType::FALSE => "false", - ElementType::FLOAT | ElementType::FLOAT5 => "real", - ElementType::INT | ElementType::INT5 => "integer", +impl From for String { + fn from(element_type: ElementType) -> String { + match element_type { + ElementType::ARRAY => "array".to_string(), + ElementType::OBJECT => "object".to_string(), + ElementType::NULL => "null".to_string(), + ElementType::TRUE => "true".to_string(), + ElementType::FALSE => "false".to_string(), + ElementType::FLOAT | ElementType::FLOAT5 => "real".to_string(), + ElementType::INT | ElementType::INT5 => "integer".to_string(), ElementType::TEXT | ElementType::TEXT5 | ElementType::TEXTJ | ElementType::TEXTRAW => { - "text" + "text".to_string() } _ => unreachable!(), - }; - result.into() + } } } @@ -412,7 +411,7 @@ impl Jsonb { pub fn is_valid(&self) -> Result { match self.read_header(0) { Ok((header, offset)) => { - if let Some(_) = self.data.get(offset..offset + header.1) { + if self.data.get(offset..offset + header.1).is_some() { Ok(header.0) } else { bail_parse_error!("malformed JSON") @@ -1602,7 +1601,7 @@ fn compare(key: (&str, ElementType), path_key: (&str, bool)) -> bool { _ => {} }; - return false; + false } #[inline] diff --git a/core/json/mod.rs b/core/json/mod.rs index 692dddb7a..71c444e81 100644 --- a/core/json/mod.rs +++ b/core/json/mod.rs @@ -87,7 +87,7 @@ fn convert_dbtype_to_jsonb(val: &OwnedValue) -> crate::Result { match val { OwnedValue::Text(text) => Jsonb::from_str(text.as_str()), OwnedValue::Blob(blob) => { - let json = Jsonb::from_raw_data(&blob); + let json = Jsonb::from_raw_data(blob); json.is_valid()?; Ok(json) } @@ -446,12 +446,12 @@ pub fn json_type(value: &OwnedValue, path: Option<&OwnedValue>) -> crate::Result let json = convert_dbtype_to_jsonb(value)?; if let Ok((_, element_type)) = json.get_by_path(&path) { - return Ok(OwnedValue::Text(Text::json(element_type.into()))); + Ok(OwnedValue::Text(Text::json(element_type.into()))) } else { - return Ok(OwnedValue::Null); + Ok(OwnedValue::Null) } } else { - return Ok(OwnedValue::Null); + Ok(OwnedValue::Null) } } @@ -692,12 +692,9 @@ pub fn is_json_valid(json_value: &OwnedValue) -> OwnedValue { if matches!(json_value, OwnedValue::Null) { return OwnedValue::Null; } - let json_is_ok = convert_dbtype_to_jsonb(json_value) - .and_then(|_| { - return Ok(OwnedValue::Integer(1)); - }) - .unwrap_or(OwnedValue::Integer(0)); - json_is_ok + convert_dbtype_to_jsonb(json_value) + .map(|_| OwnedValue::Integer(1)) + .unwrap_or(OwnedValue::Integer(0)) } pub fn json_quote(value: &OwnedValue) -> crate::Result { From 2fb18b41774549a98bb13aeda628f12d21f399f8 Mon Sep 17 00:00:00 2001 From: Ihor Andrianov Date: Mon, 17 Mar 2025 16:20:43 +0200 Subject: [PATCH 9/9] update compat --- COMPAT.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/COMPAT.md b/COMPAT.md index bc20ce0ed..2939ddd51 100644 --- a/COMPAT.md +++ b/COMPAT.md @@ -361,14 +361,14 @@ Modifiers: | Function | Status | Comment | | ---------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | json(json) | Partial | | -| jsonb(json) | | | +| 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 | -| jsonb_extract(json,path,...) | | | +| jsonb_extract(json,path,...) | Yes | | | json -> path | Yes | | | json ->> path | Yes | | | json_insert(json,path,value,...) | | |