diff --git a/Cargo.toml b/Cargo.toml index a6dd6a1..1e0b5b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,14 +41,14 @@ serde_json = { version = "1.0", default-features = false, features = ["std"] } zmij = "1.0" [dev-dependencies] +criterion = "0.5.1" goldenfile = "1.8" -serde_json = "1.0" json-deserializer = "0.4.4" -simd-json = "0.15.0" +json-five = "0.3.0" mockalloc = "0.1.2" -criterion = "0.5.1" proptest = "1.7" -json-five = "0.3.0" +serde_json = "1.0" +simd-json = "0.15.0" [features] default = ["databend", "preserve_order", "arbitrary_precision"] diff --git a/benches/strip_nulls.rs b/benches/strip_nulls.rs index f4e71ff..f22b87d 100644 --- a/benches/strip_nulls.rs +++ b/benches/strip_nulls.rs @@ -40,7 +40,7 @@ fn strip_value_nulls(val: &mut Value<'_>) { } } Value::Object(ref mut obj) => { - for (_, v) in obj.iter_mut() { + for v in obj.values_mut() { strip_value_nulls(v); } obj.retain(|_, v| !matches!(v, Value::Null)); diff --git a/src/core/databend/ser.rs b/src/core/databend/ser.rs index 9fb31a4..47c091d 100644 --- a/src/core/databend/ser.rs +++ b/src/core/databend/ser.rs @@ -1031,7 +1031,7 @@ impl<'a> Encoder<'a> { let (mut object_len, mut jentry_index) = self.base_encoder.encode_object_header(obj.len()); // encode all keys first - for (key, _) in obj.iter() { + for key in obj.keys() { let len = key.len(); object_len += len; self.base_encoder.buf.extend_from_slice(key.as_bytes()); @@ -1040,7 +1040,7 @@ impl<'a> Encoder<'a> { } // encode all values - for (_, value) in obj.iter() { + for value in obj.values() { let jentry = self.encode_value(value); object_len += jentry.length as usize; self.base_encoder.replace_jentry(jentry, &mut jentry_index); diff --git a/src/functions/path.rs b/src/functions/path.rs index 8e79f64..bc62954 100644 --- a/src/functions/path.rs +++ b/src/functions/path.rs @@ -448,8 +448,8 @@ impl RawJsonb<'_> { /// # Examples /// /// ```rust - /// use jsonb::jsonpath::parse_json_path; /// use jsonb::OwnedJsonb; + /// use jsonb::jsonpath::parse_json_path; /// /// let jsonb_value = r#"{"a": {"b": [1, 2, 3]}, "c": 4}"#.parse::().unwrap(); /// let raw_jsonb = jsonb_value.as_raw(); @@ -706,8 +706,8 @@ impl RawJsonb<'_> { /// ```rust /// use std::borrow::Cow; /// - /// use jsonb::keypath::KeyPath; /// use jsonb::OwnedJsonb; + /// use jsonb::keypath::KeyPath; /// /// // Deleting from an array /// let arr_jsonb = r#"[1, 2, 3]"#.parse::().unwrap(); @@ -1046,130 +1046,201 @@ impl RawJsonb<'_> { Ok(false) } - /// Extracts all scalar values from a JSONB value along with their key paths. + /// Visits every scalar leaf key path in a JSONB value. /// - /// This function recursively traverses the JSONB structure (both objects and arrays) - /// and collects all leaf node scalar values (null, boolean, number, string, etc.) - /// along with their corresponding key paths. The key path represents the navigation - /// path from the root to reach each scalar value. + /// This function recursively traverses objects and, unless `ignore_array` is true, + /// arrays. It invokes `visitor` once for each leaf instead of allocating and returning + /// a collection. The path slice is borrowed from traversal state and is only valid for + /// the duration of the callback. /// /// # Arguments /// - /// * `ignore_array` - When true, arrays are treated as leaf values and returned as - /// `Value::Array` without descending into their elements. + /// * `ignore_array` - When true, arrays are treated as leaves and their paths are + /// visited without descending into their elements. + /// * `visitor` - Called with the path from the root to each leaf. Returning an error + /// stops traversal and propagates that error to the caller. /// - /// # Returns - /// - /// * `Result, Value<'_>)>>` - A vector of tuples, each containing: - /// - `KeyPaths`: The path to reach the scalar value - /// - `Value`: The scalar value itself - /// - /// Empty objects or arrays are treated as leaf values and returned as `Value::Object` or - /// `Value::Array`. + /// Empty objects and arrays are treated as leaves. A scalar at the root has an empty + /// key path and is skipped. /// /// # Examples /// /// ```rust /// use jsonb::OwnedJsonb; + /// use jsonb::keypath::KeyPaths; + /// + /// let jsonb = r#"{"user":{"name":"Alice","scores":[85,92]}}"#.parse::().unwrap(); + /// let mut paths = Vec::new(); /// - /// let json = r#"{"user": {"name": "Alice", "scores": [85, 92, 78]}}"#; - /// let jsonb = json.parse::().unwrap(); /// let raw_jsonb = jsonb.as_raw(); - /// let result = raw_jsonb.extract_scalar_key_values(false); - /// assert!(result.is_ok()); - /// let result = result.unwrap(); - /// assert_eq!(result.len(), 4); - /// // Result contains: - /// // - path: "user", "name" -> value: "Alice" - /// // - path: "user", "scores", 0 -> value: 85 - /// // - path: "user", "scores", 1 -> value: 92 - /// // - path: "user", "scores", 2 -> value: 78 + /// raw_jsonb.visit_scalar_key_paths(false, |path| { + /// paths.push(KeyPaths { + /// paths: path.to_vec(), + /// }); + /// Ok(()) + /// }) + /// .unwrap(); + /// assert_eq!(paths.len(), 3); /// ``` + pub fn visit_scalar_key_paths<'a, F>(&'a self, ignore_array: bool, mut visitor: F) -> Result<()> + where + F: FnMut(&[KeyPath<'a>]) -> Result<()>, + { + let item = JsonbItem::from_raw_jsonb(*self)?; + let mut current_paths = Vec::with_capacity(3); + Self::visit_scalar_key_paths_recursive(item, ignore_array, &mut current_paths, &mut visitor) + } + + fn visit_scalar_key_paths_recursive<'a, F>( + current_item: JsonbItem<'a>, + ignore_array: bool, + current_paths: &mut Vec>, + visitor: &mut F, + ) -> Result<()> + where + F: FnMut(&[KeyPath<'a>]) -> Result<()>, + { + match current_item { + JsonbItem::Raw(raw) => { + if let Some(mut object_iter) = ObjectIterator::new(raw)? { + if object_iter.len() > 0 { + for object_result in &mut object_iter { + let (key, value) = object_result?; + current_paths.push(KeyPath::Name(Cow::Borrowed(key))); + Self::visit_scalar_key_paths_recursive( + value, + ignore_array, + current_paths, + visitor, + )?; + current_paths.pop(); + } + return Ok(()); + } + } else if !ignore_array { + if let Some(array_iter) = ArrayIterator::new(raw)? { + if array_iter.len() > 0 { + for (index, array_result) in array_iter.enumerate() { + let value = array_result?; + current_paths.push(KeyPath::Index(index as i32)); + Self::visit_scalar_key_paths_recursive( + value, + ignore_array, + current_paths, + visitor, + )?; + current_paths.pop(); + } + return Ok(()); + } + } + } + } + JsonbItem::Owned(_) => unreachable!(), + _ => {} + } + + if !current_paths.is_empty() { + visitor(current_paths)?; + } + Ok(()) + } + + /// Visits every scalar leaf and its key path in a JSONB value. + /// + /// This is the callback-based counterpart of [`RawJsonb::extract_scalar_key_values`]. + /// It recursively traverses objects and, unless `ignore_array` is true, arrays, but + /// avoids allocating a result vector. Both the path slice and value may borrow from the + /// input JSONB and are only valid according to their callback lifetimes. + /// + /// # Arguments + /// + /// * `ignore_array` - When true, arrays are treated as leaf `Value::Array` values + /// without descending into their elements. + /// * `visitor` - Called with the path and value of each leaf. Returning an error stops + /// traversal and propagates that error to the caller. + /// + /// Empty objects and arrays are treated as leaf values. JSON null is visited as + /// `Value::Null`; it is not treated as a missing path. A scalar at the root has an empty + /// key path and is skipped. + /// + /// # Examples /// /// ```rust - /// use jsonb::{OwnedJsonb, Value}; + /// use jsonb::OwnedJsonb; + /// use jsonb::Value; /// - /// let json = r#"{"user": {"name": "Alice", "scores": [85, 92, 78]}}"#; - /// let jsonb = json.parse::().unwrap(); - /// let raw_jsonb = jsonb.as_raw(); - /// let result = raw_jsonb.extract_scalar_key_values(true).unwrap(); + /// let jsonb = r#"{"user":{"name":"Alice","scores":[85,92]}}"#.parse::().unwrap(); + /// let mut values = Vec::new(); /// - /// assert_eq!(result.len(), 2); - /// assert!(result.iter().any(|(_, value)| matches!(value, Value::Array(_)))); - /// // Result contains: - /// // - path: "user", "name" -> value: "Alice" - /// // - path: "user", "scores" -> value: [85, 92, 78] + /// let raw_jsonb = jsonb.as_raw(); + /// raw_jsonb.visit_scalar_key_values(true, |paths, value| { + /// values.push((paths.len(), value)); + /// Ok(()) + /// }) + /// .unwrap(); + /// assert_eq!(values.len(), 2); + /// assert!( + /// values + /// .iter() + /// .any(|(_, value)| matches!(value, Value::Array(_))) + /// ); /// ``` - pub fn extract_scalar_key_values( - &self, + pub fn visit_scalar_key_values<'a, F>( + &'a self, ignore_array: bool, - ) -> Result, Value<'_>)>> { + mut visitor: F, + ) -> Result<()> + where + F: FnMut(&[KeyPath<'a>], Value<'a>) -> Result<()>, + { let item = JsonbItem::from_raw_jsonb(*self)?; - let mut result = Vec::with_capacity(16); let mut current_paths = Vec::with_capacity(3); - Self::extract_scalar_key_values_recursive( + Self::visit_scalar_key_values_recursive( item, ignore_array, &mut current_paths, - &mut result, - )?; - Ok(result) + &mut visitor, + ) } - /// Helper function for `extract_scalar_key_values` that recursively traverses the JSONB structure. - /// - /// This function implements a depth-first traversal of the JSONB document, building up the - /// key path as it goes and collecting scalar values when it reaches leaf nodes. - /// Empty objects or arrays are treated as leaf values and returned as `Value::Object` or - /// `Value::Array` instead of being skipped. - /// - /// # Arguments - /// - /// * `current_item` - The current JSONB item being processed - /// * `current_paths` - The current path from the root to this item (modified during traversal) - /// * `result` - The collection where extracted key-value pairs are stored - /// - /// # Returns - /// - /// * `Result<()>` - Success or error during traversal - fn extract_scalar_key_values_recursive<'a>( + fn visit_scalar_key_values_recursive<'a, F>( current_item: JsonbItem<'a>, ignore_array: bool, current_paths: &mut Vec>, - result: &mut Vec<(KeyPaths<'a>, Value<'a>)>, - ) -> Result<()> { + visitor: &mut F, + ) -> Result<()> + where + F: FnMut(&[KeyPath<'a>], Value<'a>) -> Result<()>, + { match current_item { JsonbItem::Raw(raw) => { - let object_iter_opt = ObjectIterator::new(raw)?; - if let Some(mut object_iter) = object_iter_opt { + if let Some(mut object_iter) = ObjectIterator::new(raw)? { if object_iter.len() > 0 { for object_result in &mut object_iter { - let (key, val_item) = object_result?; + let (key, value) = object_result?; current_paths.push(KeyPath::Name(Cow::Borrowed(key))); - // Recursively handle object values - Self::extract_scalar_key_values_recursive( - val_item, + Self::visit_scalar_key_values_recursive( + value, ignore_array, current_paths, - result, + visitor, )?; current_paths.pop(); } return Ok(()); } } else if !ignore_array { - let array_iter_opt = ArrayIterator::new(raw)?; - if let Some(array_iter) = array_iter_opt { + if let Some(array_iter) = ArrayIterator::new(raw)? { if array_iter.len() > 0 { - for (index, array_result) in &mut array_iter.enumerate() { - let val_item = array_result?; + for (index, array_result) in array_iter.enumerate() { + let value = array_result?; current_paths.push(KeyPath::Index(index as i32)); - // Recursively handle array values - Self::extract_scalar_key_values_recursive( - val_item, + Self::visit_scalar_key_values_recursive( + value, ignore_array, current_paths, - result, + visitor, )?; current_paths.pop(); } @@ -1178,43 +1249,119 @@ impl RawJsonb<'_> { } } if !current_paths.is_empty() { - let key_paths = KeyPaths { - paths: current_paths.clone(), - }; - let value = raw.to_value()?; - result.push((key_paths, value)); + visitor(current_paths, raw.to_value()?)?; } } JsonbItem::Owned(_) => unreachable!(), - _ => { - // ignore scalar value - if current_paths.is_empty() { - return Ok(()); + JsonbItem::Null => { + if !current_paths.is_empty() { + visitor(current_paths, Value::Null)?; + } + } + JsonbItem::Boolean(value) => { + if !current_paths.is_empty() { + visitor(current_paths, Value::Bool(value))?; + } + } + JsonbItem::String(value) => { + if !current_paths.is_empty() { + visitor(current_paths, Value::String(value))?; + } + } + JsonbItem::Number(value) => { + if !current_paths.is_empty() { + visitor(current_paths, Value::Number(value.as_number()?))?; + } + } + JsonbItem::Extension(value) => { + if !current_paths.is_empty() { + let value = match value.as_extension_value()? { + ExtensionValue::Binary(value) => Value::Binary(value), + ExtensionValue::Date(value) => Value::Date(value), + ExtensionValue::Timestamp(value) => Value::Timestamp(value), + ExtensionValue::TimestampTz(value) => Value::TimestampTz(value), + ExtensionValue::Interval(value) => Value::Interval(value), + }; + visitor(current_paths, value)?; } - let key_paths = KeyPaths { - paths: current_paths.clone(), - }; - let value = match current_item { - JsonbItem::Null => Value::Null, - JsonbItem::Boolean(val) => Value::Bool(val), - JsonbItem::String(val) => Value::String(val), - JsonbItem::Number(num) => Value::Number(num.as_number()?), - JsonbItem::Extension(ext) => { - let ext_val = ext.as_extension_value()?; - match ext_val { - ExtensionValue::Binary(val) => Value::Binary(val), - ExtensionValue::Date(val) => Value::Date(val), - ExtensionValue::Timestamp(val) => Value::Timestamp(val), - ExtensionValue::TimestampTz(val) => Value::TimestampTz(val), - ExtensionValue::Interval(val) => Value::Interval(val), - } - } - _ => unreachable!(), - }; - // Add the path and scalar value - result.push((key_paths, value)); } } Ok(()) } + + /// Extracts all scalar values from a JSONB value along with their key paths. + /// + /// This function recursively traverses the JSONB structure (both objects and arrays) + /// and collects all leaf node scalar values (null, boolean, number, string, etc.) + /// along with their corresponding key paths. The key path represents the navigation + /// path from the root to reach each scalar value. + /// + /// # Arguments + /// + /// * `ignore_array` - When true, arrays are treated as leaf values and returned as + /// `Value::Array` without descending into their elements. + /// + /// # Returns + /// + /// * `Result, Value<'_>)>>` - A vector of tuples, each containing: + /// - `KeyPaths`: The path to reach the scalar value + /// - `Value`: The scalar value itself + /// + /// Empty objects or arrays are treated as leaf values and returned as `Value::Object` or + /// `Value::Array`. + /// + /// # Examples + /// + /// ```rust + /// use jsonb::OwnedJsonb; + /// + /// let json = r#"{"user": {"name": "Alice", "scores": [85, 92, 78]}}"#; + /// let jsonb = json.parse::().unwrap(); + /// let raw_jsonb = jsonb.as_raw(); + /// let result = raw_jsonb.extract_scalar_key_values(false); + /// assert!(result.is_ok()); + /// let result = result.unwrap(); + /// assert_eq!(result.len(), 4); + /// // Result contains: + /// // - path: "user", "name" -> value: "Alice" + /// // - path: "user", "scores", 0 -> value: 85 + /// // - path: "user", "scores", 1 -> value: 92 + /// // - path: "user", "scores", 2 -> value: 78 + /// ``` + /// + /// ```rust + /// use jsonb::OwnedJsonb; + /// use jsonb::Value; + /// + /// let json = r#"{"user": {"name": "Alice", "scores": [85, 92, 78]}}"#; + /// let jsonb = json.parse::().unwrap(); + /// let raw_jsonb = jsonb.as_raw(); + /// let result = raw_jsonb.extract_scalar_key_values(true).unwrap(); + /// + /// assert_eq!(result.len(), 2); + /// assert!( + /// result + /// .iter() + /// .any(|(_, value)| matches!(value, Value::Array(_))) + /// ); + /// // Result contains: + /// // - path: "user", "name" -> value: "Alice" + /// // - path: "user", "scores" -> value: [85, 92, 78] + /// ``` + pub fn extract_scalar_key_values( + &self, + ignore_array: bool, + ) -> Result, Value<'_>)>> { + let mut result = Vec::with_capacity(16); + self.visit_scalar_key_values(ignore_array, |paths, value| { + result.push(( + KeyPaths { + paths: paths.to_vec(), + }, + value, + )); + Ok(()) + })?; + Ok(result) + } } diff --git a/src/jsonpath/selector.rs b/src/jsonpath/selector.rs index dd8fd37..6d87d41 100644 --- a/src/jsonpath/selector.rs +++ b/src/jsonpath/selector.rs @@ -1003,12 +1003,9 @@ impl<'a> Selector<'a> { for lval in lvals.iter() { for rval in rvals.iter() { - if let Some(res) = self.compare_value(op, lval.clone(), rval.clone()) { - if res { - return Some(true); - } - } else { - return None; + let res = self.compare_value(op, lval.clone(), rval.clone())?; + if res { + return Some(true); } } } diff --git a/src/keypath.rs b/src/keypath.rs index 42d51a9..fd6ff7f 100644 --- a/src/keypath.rs +++ b/src/keypath.rs @@ -15,6 +15,7 @@ use std::borrow::Cow; use std::fmt::Display; use std::fmt::Formatter; +use std::fmt::Write; use nom::branch::alt; use nom::character::complete::char; @@ -51,18 +52,21 @@ pub enum KeyPath<'a> { } /// Represents a set of owned key path chains. -#[derive(Debug, Clone, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)] +#[derive( + Debug, Clone, Eq, PartialEq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize, +)] pub struct OwnedKeyPaths { pub paths: Vec, } -/// Represents a valid owned key path. -#[derive(Debug, Clone, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)] +/// Represents a valid owned key path. Quoting is a serialization detail, so +/// quoted and unquoted object keys share the same `Name` representation. +#[derive( + Debug, Clone, Eq, PartialEq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize, +)] pub enum OwnedKeyPath { /// represents the index of an Array, allow negative indexing. Index(i32), - /// represents the quoted field name of an Object. - QuotedName(String), /// represents the field name of an Object. Name(String), } @@ -118,9 +122,6 @@ impl Display for OwnedKeyPath { OwnedKeyPath::Index(idx) => { write!(f, "{idx}")?; } - OwnedKeyPath::QuotedName(name) => { - write!(f, "\"{name}\"")?; - } OwnedKeyPath::Name(name) => { write!(f, "{name}")?; } @@ -138,19 +139,64 @@ impl<'a> KeyPaths<'a> { } impl OwnedKeyPaths { + pub fn from_key_path_slice(key_paths: &[KeyPath<'_>]) -> Self { + Self { + paths: key_paths.iter().map(KeyPath::to_owned).collect(), + } + } + pub fn as_key_paths(&self) -> KeyPaths<'_> { KeyPaths { paths: self.paths.iter().map(OwnedKeyPath::as_key_path).collect(), } } + + /// Encode this path into the compact canonical form used by virtual-column + /// metadata. Identifier-like keys use dot notation, array indexes use + /// brackets, and only keys containing special characters are quoted. + /// + /// Examples: `user.name`, `users[0].id`, `user.'profile.name'`. + pub fn to_canonical_path(&self) -> String { + let mut encoded = String::new(); + for path in &self.paths { + match path { + OwnedKeyPath::Index(index) => { + write!(encoded, "[{index}]").unwrap(); + } + OwnedKeyPath::Name(name) => { + if !encoded.is_empty() { + encoded.push('.'); + } + if is_ident(name) { + encoded.push_str(name); + } else { + encoded.push('\''); + for ch in name.chars() { + if ch == '\\' || ch == '\'' { + encoded.push('\\'); + } + encoded.push(ch); + } + encoded.push('\''); + } + } + } + } + encoded + } + + /// Decode the compact canonical representation produced by + /// [`Self::to_canonical_path`]. + pub fn from_canonical_path(path: &str) -> Result { + decode_canonical_path(path).ok_or(Error::InvalidKeyPath) + } } impl<'a> KeyPath<'a> { pub fn to_owned(&self) -> OwnedKeyPath { match self { KeyPath::Index(idx) => OwnedKeyPath::Index(*idx), - KeyPath::QuotedName(name) => OwnedKeyPath::QuotedName(name.to_string()), - KeyPath::Name(name) => OwnedKeyPath::Name(name.to_string()), + KeyPath::QuotedName(name) | KeyPath::Name(name) => OwnedKeyPath::Name(name.to_string()), } } } @@ -159,12 +205,107 @@ impl OwnedKeyPath { pub fn as_key_path(&self) -> KeyPath<'_> { match self { OwnedKeyPath::Index(idx) => KeyPath::Index(*idx), - OwnedKeyPath::QuotedName(name) => KeyPath::QuotedName(Cow::Borrowed(name.as_str())), OwnedKeyPath::Name(name) => KeyPath::Name(Cow::Borrowed(name.as_str())), } } } +fn decode_canonical_path(path: &str) -> Option { + let bytes = path.as_bytes(); + let mut index = 0; + let mut paths = Vec::new(); + while index < bytes.len() { + if bytes[index] == b'[' { + index += 1; + let start = index; + if index < bytes.len() && bytes[index] == b'-' { + index += 1; + } + while index < bytes.len() && bytes[index].is_ascii_digit() { + index += 1; + } + if start == index || (bytes.get(start) == Some(&b'-') && start + 1 == index) { + return None; + } + if index >= bytes.len() || bytes[index] != b']' { + return None; + } + let value = std::str::from_utf8(&bytes[start..index]) + .ok()? + .parse::() + .ok()?; + paths.push(OwnedKeyPath::Index(value)); + index += 1; + continue; + } + + if !paths.is_empty() { + if bytes[index] != b'.' { + return None; + } + index += 1; + if index >= bytes.len() { + return None; + } + } + + if bytes[index] == b'\'' { + index += 1; + let mut name = String::new(); + loop { + let rest = path.get(index..)?; + if rest.starts_with('\\') { + let escaped = rest.chars().nth(1)?; + if escaped != '\\' && escaped != '\'' { + return None; + } + name.push(escaped); + index += 1 + escaped.len_utf8(); + } else if rest.starts_with('\'') { + index += 1; + break; + } else { + let ch = rest.chars().next()?; + name.push(ch); + index += ch.len_utf8(); + } + } + paths.push(OwnedKeyPath::Name(name)); + continue; + } + + let rest = path.get(index..)?; + let mut chars = rest.chars(); + let first = chars.next()?; + if !is_ident_start(first) { + return None; + } + let mut end = index + first.len_utf8(); + for ch in chars { + if !is_ident_continue(ch) { + break; + } + end += ch.len_utf8(); + } + paths.push(OwnedKeyPath::Name(path[index..end].to_string())); + index = end; + } + (!paths.is_empty()).then_some(OwnedKeyPaths { paths }) +} + +fn is_ident(name: &str) -> bool { + let mut chars = name.chars(); + chars.next().is_some_and(is_ident_start) && chars.all(is_ident_continue) +} + +fn is_ident_start(ch: char) -> bool { + ch == '_' || ch.is_alphabetic() +} + +fn is_ident_continue(ch: char) -> bool { + ch == '_' || ch.is_alphanumeric() +} + /// Parsing the input string to key paths. pub fn parse_key_paths(input: &[u8]) -> Result, Error> { match key_paths(input) { @@ -207,3 +348,31 @@ fn key_paths(input: &[u8]) -> IResult<&[u8], Vec>> { )) .parse(input) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_owned_key_paths_canonical_roundtrip() { + let paths = OwnedKeyPaths { + paths: vec![ + OwnedKeyPath::Name("user".to_string()), + OwnedKeyPath::Name("profile.name".to_string()), + OwnedKeyPath::Index(0), + OwnedKeyPath::Name("it's".to_string()), + ], + }; + let encoded = paths.to_canonical_path(); + assert_eq!(encoded, "user.'profile.name'[0].'it\\'s'"); + assert_eq!(OwnedKeyPaths::from_canonical_path(&encoded).unwrap(), paths); + } + + #[test] + fn test_quoted_and_unquoted_paths_share_owned_identity() { + let quoted = KeyPath::QuotedName(Cow::Borrowed("name")).to_owned(); + let unquoted = KeyPath::Name(Cow::Borrowed("name")).to_owned(); + assert_eq!(quoted, unquoted); + assert_eq!(quoted, OwnedKeyPath::Name("name".to_string())); + } +} diff --git a/src/parser.rs b/src/parser.rs index eb9afc7..ebc1e49 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -55,17 +55,17 @@ use crate::constants::UINT64_MAX; use crate::constants::UINT64_MIN; // JSON literal constants -const NULL_LOWERCASE: [u8; 4] = [b'n', b'u', b'l', b'l']; -const NULL_UPPERCASE: [u8; 4] = [b'N', b'U', b'L', b'L']; -const TRUE_LOWERCASE: [u8; 4] = [b't', b'r', b'u', b'e']; -const TRUE_UPPERCASE: [u8; 4] = [b'T', b'R', b'U', b'E']; -const FALSE_LOWERCASE: [u8; 5] = [b'f', b'a', b'l', b's', b'e']; -const FALSE_UPPERCASE: [u8; 5] = [b'F', b'A', b'L', b'S', b'E']; - -const NAN_LOWERCASE: [u8; 3] = [b'n', b'a', b'n']; -const NAN_UPPERCASE: [u8; 3] = [b'N', b'A', b'N']; -const INFINITY_LOWERCASE: [u8; 8] = [b'i', b'n', b'f', b'i', b'n', b'i', b't', b'y']; -const INFINITY_UPPERCASE: [u8; 8] = [b'I', b'N', b'F', b'I', b'N', b'I', b'T', b'Y']; +const NULL_LOWERCASE: [u8; 4] = *b"null"; +const NULL_UPPERCASE: [u8; 4] = *b"NULL"; +const TRUE_LOWERCASE: [u8; 4] = *b"true"; +const TRUE_UPPERCASE: [u8; 4] = *b"TRUE"; +const FALSE_LOWERCASE: [u8; 5] = *b"false"; +const FALSE_UPPERCASE: [u8; 5] = *b"FALSE"; + +const NAN_LOWERCASE: [u8; 3] = *b"nan"; +const NAN_UPPERCASE: [u8; 3] = *b"NAN"; +const INFINITY_LOWERCASE: [u8; 8] = *b"infinity"; +const INFINITY_UPPERCASE: [u8; 8] = *b"INFINITY"; #[cfg(feature = "arbitrary_precision")] static POWER_TABLE: std::sync::LazyLock<[i256; 39]> = std::sync::LazyLock::new(|| { diff --git a/tests/it/functions.rs b/tests/it/functions.rs index a2bdc32..c367279 100644 --- a/tests/it/functions.rs +++ b/tests/it/functions.rs @@ -2043,6 +2043,155 @@ fn test_to_serde_json() { } } +#[test] +fn test_visit_scalar_key_paths() { + let json = r#"{"user":{"name":"Alice","scores":[85,92]},"empty":{}}"#; + let jsonb = json.parse::().unwrap(); + let raw_jsonb = jsonb.as_raw(); + + let mut paths = Vec::new(); + raw_jsonb + .visit_scalar_key_paths(false, |path| { + paths.push(KeyPaths { + paths: path.to_vec(), + }); + Ok(()) + }) + .unwrap(); + assert_eq!( + paths, + vec![ + KeyPaths { + paths: vec![KeyPath::Name(Cow::Borrowed("empty"))], + }, + KeyPaths { + paths: vec![ + KeyPath::Name(Cow::Borrowed("user")), + KeyPath::Name(Cow::Borrowed("name")), + ], + }, + KeyPaths { + paths: vec![ + KeyPath::Name(Cow::Borrowed("user")), + KeyPath::Name(Cow::Borrowed("scores")), + KeyPath::Index(0), + ], + }, + KeyPaths { + paths: vec![ + KeyPath::Name(Cow::Borrowed("user")), + KeyPath::Name(Cow::Borrowed("scores")), + KeyPath::Index(1), + ], + }, + ] + ); + + paths.clear(); + raw_jsonb + .visit_scalar_key_paths(true, |path| { + paths.push(KeyPaths { + paths: path.to_vec(), + }); + Ok(()) + }) + .unwrap(); + assert_eq!( + paths, + vec![ + KeyPaths { + paths: vec![KeyPath::Name(Cow::Borrowed("empty"))], + }, + KeyPaths { + paths: vec![ + KeyPath::Name(Cow::Borrowed("user")), + KeyPath::Name(Cow::Borrowed("name")), + ], + }, + KeyPaths { + paths: vec![ + KeyPath::Name(Cow::Borrowed("user")), + KeyPath::Name(Cow::Borrowed("scores")), + ], + }, + ] + ); +} + +#[test] +fn test_visit_scalar_key_values() { + let json = r#"{"empty":{},"null":null,"user":{"name":"Alice","scores":[85,92]}}"#; + let jsonb = json.parse::().unwrap(); + let raw_jsonb = jsonb.as_raw(); + + let mut values = Vec::new(); + raw_jsonb + .visit_scalar_key_values(false, |paths, value| { + values.push(( + KeyPaths { + paths: paths.to_vec(), + }, + value, + )); + Ok(()) + }) + .unwrap(); + assert_eq!(values, raw_jsonb.extract_scalar_key_values(false).unwrap()); + assert_eq!(values.len(), 5); + assert!(values.iter().any(|(paths, value)| { + paths.paths == vec![KeyPath::Name(Cow::Borrowed("null"))] && *value == Value::Null + })); + assert!(values.iter().any(|(paths, value)| { + paths.paths + == vec![ + KeyPath::Name(Cow::Borrowed("user")), + KeyPath::Name(Cow::Borrowed("scores")), + KeyPath::Index(1), + ] + && *value == Value::Number(Number::UInt64(92)) + })); + + values.clear(); + raw_jsonb + .visit_scalar_key_values(true, |paths, value| { + values.push(( + KeyPaths { + paths: paths.to_vec(), + }, + value, + )); + Ok(()) + }) + .unwrap(); + assert_eq!(values, raw_jsonb.extract_scalar_key_values(true).unwrap()); + assert_eq!(values.len(), 4); + assert!(values.iter().any(|(paths, value)| { + paths.paths + == vec![ + KeyPath::Name(Cow::Borrowed("user")), + KeyPath::Name(Cow::Borrowed("scores")), + ] + && *value + == Value::Array(vec![ + Value::Number(Number::UInt64(85)), + Value::Number(Number::UInt64(92)), + ]) + })); + + let root = "42".parse::().unwrap(); + let mut visited = false; + root.as_raw() + .visit_scalar_key_values(false, |_, _| { + visited = true; + Ok(()) + }) + .unwrap(); + assert!(!visited); + + let error = raw_jsonb.visit_scalar_key_values(false, |_, _| Err(Error::InvalidJson)); + assert_eq!(error, Err(Error::InvalidJson)); +} + #[test] fn test_extract_scalar_key_values() { // Test case 1: Simple object with scalar values