Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement json_pretty #860

Merged
merged 1 commit into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions COMPAT.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ This document describes the compatibility of Limbo with SQLite.
- [Extensions](#extensions)
- [UUID](#uuid)
- [regexp](#regexp)
- [Vector](#vector)

## Features

Expand Down Expand Up @@ -349,7 +350,7 @@ Modifiers:
#### JSON functions

| Function | Status | Comment |
|------------------------------------|---------|----------------------------------------------------------------------------------------------------------------------------------------------|
| ---------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| json(json) | Partial | |
| jsonb(json) | | |
| json_array(value1,value2,...) | Yes | |
Expand All @@ -367,7 +368,7 @@ Modifiers:
| jsonb_object(label1,value1,...) | | |
| json_patch(json1,json2) | Yes | |
| jsonb_patch(json1,json2) | | |
| json_pretty(json) | | |
| 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,...) | | |
Expand Down
4 changes: 4 additions & 0 deletions core/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ pub enum JsonFunc {
JsonValid,
JsonPatch,
JsonRemove,
JsonPretty,
}

#[cfg(feature = "json")]
Expand All @@ -103,6 +104,7 @@ impl Display for JsonFunc {
Self::JsonValid => "json_valid".to_string(),
Self::JsonPatch => "json_patch".to_string(),
Self::JsonRemove => "json_remove".to_string(),
Self::JsonPretty => "json_pretty".to_string(),
}
)
}
Expand Down Expand Up @@ -534,6 +536,8 @@ impl Func {
"json_patch" => Ok(Self::Json(JsonFunc::JsonPatch)),
#[cfg(feature = "json")]
"json_remove" => Ok(Self::Json(JsonFunc::JsonRemove)),
#[cfg(feature = "json")]
"json_pretty" => Ok(Self::Json(JsonFunc::JsonPretty)),
"unixepoch" => Ok(Self::Scalar(ScalarFunc::UnixEpoch)),
"julianday" => Ok(Self::Scalar(ScalarFunc::JulianDay)),
"hex" => Ok(Self::Scalar(ScalarFunc::Hex)),
Expand Down
37 changes: 22 additions & 15 deletions core/json/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub use crate::json::ser::to_string;
use crate::types::{LimboText, OwnedValue, TextSubtype};
use indexmap::IndexMap;
use jsonb::Error as JsonbError;
use ser::to_string_pretty;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
Expand All @@ -31,7 +32,7 @@ pub enum Val {
Object(Vec<(String, Val)>),
}

pub fn get_json(json_value: &OwnedValue) -> crate::Result<OwnedValue> {
pub fn get_json(json_value: &OwnedValue, indent: Option<&str>) -> crate::Result<OwnedValue> {
match json_value {
OwnedValue::Text(ref t) => {
// optimization: once we know the subtype is a valid JSON, we do not have
Expand All @@ -41,7 +42,10 @@ pub fn get_json(json_value: &OwnedValue) -> crate::Result<OwnedValue> {
}

let json_val = get_json_value(json_value)?;
let json = to_string(&json_val).unwrap();
let json = match indent {
Some(indent) => to_string_pretty(&json_val, indent).unwrap(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

? instead of unwrap?

None => to_string(&json_val).unwrap(),
};

Ok(OwnedValue::Text(LimboText::json(Rc::new(json))))
}
Expand All @@ -57,7 +61,10 @@ pub fn get_json(json_value: &OwnedValue) -> crate::Result<OwnedValue> {
OwnedValue::Null => Ok(OwnedValue::Null),
_ => {
let json_val = get_json_value(json_value)?;
let json = to_string(&json_val).unwrap();
let json = match indent {
Some(indent) => to_string_pretty(&json_val, indent).unwrap(),
None => to_string(&json_val).unwrap(),
};

Ok(OwnedValue::Text(LimboText::json(Rc::new(json))))
}
Expand Down Expand Up @@ -536,7 +543,7 @@ mod tests {
#[test]
fn test_get_json_valid_json5() {
let input = OwnedValue::build_text(Rc::new("{ key: 'value' }".to_string()));
let result = get_json(&input).unwrap();
let result = get_json(&input, None).unwrap();
if let OwnedValue::Text(result_str) = result {
assert!(result_str.value.contains("\"key\":\"value\""));
assert_eq!(result_str.subtype, TextSubtype::Json);
Expand All @@ -548,7 +555,7 @@ mod tests {
#[test]
fn test_get_json_valid_json5_double_single_quotes() {
let input = OwnedValue::build_text(Rc::new("{ key: ''value'' }".to_string()));
let result = get_json(&input).unwrap();
let result = get_json(&input, None).unwrap();
if let OwnedValue::Text(result_str) = result {
assert!(result_str.value.contains("\"key\":\"value\""));
assert_eq!(result_str.subtype, TextSubtype::Json);
Expand All @@ -560,7 +567,7 @@ mod tests {
#[test]
fn test_get_json_valid_json5_infinity() {
let input = OwnedValue::build_text(Rc::new("{ \"key\": Infinity }".to_string()));
let result = get_json(&input).unwrap();
let result = get_json(&input, None).unwrap();
if let OwnedValue::Text(result_str) = result {
assert!(result_str.value.contains("{\"key\":9e999}"));
assert_eq!(result_str.subtype, TextSubtype::Json);
Expand All @@ -572,7 +579,7 @@ mod tests {
#[test]
fn test_get_json_valid_json5_negative_infinity() {
let input = OwnedValue::build_text(Rc::new("{ \"key\": -Infinity }".to_string()));
let result = get_json(&input).unwrap();
let result = get_json(&input, None).unwrap();
if let OwnedValue::Text(result_str) = result {
assert!(result_str.value.contains("{\"key\":-9e999}"));
assert_eq!(result_str.subtype, TextSubtype::Json);
Expand All @@ -584,7 +591,7 @@ mod tests {
#[test]
fn test_get_json_valid_json5_nan() {
let input = OwnedValue::build_text(Rc::new("{ \"key\": NaN }".to_string()));
let result = get_json(&input).unwrap();
let result = get_json(&input, None).unwrap();
if let OwnedValue::Text(result_str) = result {
assert!(result_str.value.contains("{\"key\":null}"));
assert_eq!(result_str.subtype, TextSubtype::Json);
Expand All @@ -596,7 +603,7 @@ mod tests {
#[test]
fn test_get_json_invalid_json5() {
let input = OwnedValue::build_text(Rc::new("{ key: value }".to_string()));
let result = get_json(&input);
let result = get_json(&input, None);
match result {
Ok(_) => panic!("Expected error for malformed JSON"),
Err(e) => assert!(e.to_string().contains("malformed JSON")),
Expand All @@ -606,7 +613,7 @@ mod tests {
#[test]
fn test_get_json_valid_jsonb() {
let input = OwnedValue::build_text(Rc::new("{\"key\":\"value\"}".to_string()));
let result = get_json(&input).unwrap();
let result = get_json(&input, None).unwrap();
if let OwnedValue::Text(result_str) = result {
assert!(result_str.value.contains("\"key\":\"value\""));
assert_eq!(result_str.subtype, TextSubtype::Json);
Expand All @@ -618,7 +625,7 @@ mod tests {
#[test]
fn test_get_json_invalid_jsonb() {
let input = OwnedValue::build_text(Rc::new("{key:\"value\"".to_string()));
let result = get_json(&input);
let result = get_json(&input, None);
match result {
Ok(_) => panic!("Expected error for malformed JSON"),
Err(e) => assert!(e.to_string().contains("malformed JSON")),
Expand All @@ -629,7 +636,7 @@ mod tests {
fn test_get_json_blob_valid_jsonb() {
let binary_json = b"\x40\0\0\x01\x10\0\0\x03\x10\0\0\x03\x61\x73\x64\x61\x64\x66".to_vec();
let input = OwnedValue::Blob(Rc::new(binary_json));
let result = get_json(&input).unwrap();
let result = get_json(&input, None).unwrap();
if let OwnedValue::Text(result_str) = result {
assert!(result_str.value.contains("\"asd\":\"adf\""));
assert_eq!(result_str.subtype, TextSubtype::Json);
Expand All @@ -642,7 +649,7 @@ mod tests {
fn test_get_json_blob_invalid_jsonb() {
let binary_json: Vec<u8> = vec![0xA2, 0x62, 0x6B, 0x31, 0x62, 0x76]; // Incomplete binary JSON
let input = OwnedValue::Blob(Rc::new(binary_json));
let result = get_json(&input);
let result = get_json(&input, None);
match result {
Ok(_) => panic!("Expected error for malformed JSON"),
Err(e) => assert!(e.to_string().contains("malformed JSON")),
Expand All @@ -652,7 +659,7 @@ mod tests {
#[test]
fn test_get_json_non_text() {
let input = OwnedValue::Null;
let result = get_json(&input).unwrap();
let result = get_json(&input, None).unwrap();
if let OwnedValue::Null = result {
// Test passed
} else {
Expand Down Expand Up @@ -809,7 +816,7 @@ mod tests {
#[test]
fn test_json_array_length_simple_json_subtype() {
let input = OwnedValue::build_text(Rc::new("[1,2,3]".to_string()));
let wrapped = get_json(&input).unwrap();
let wrapped = get_json(&input, None).unwrap();
let result = json_array_length(&wrapped, None).unwrap();

if let OwnedValue::Integer(res) = result {
Expand Down
39 changes: 39 additions & 0 deletions core/json/ser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ where
Ok(string)
}

/// Attempts to serialize the input as a JSON5 string (actually a JSON string).
pub fn to_string_pretty<T>(value: &T, indent: &str) -> Result<String>
where
T: Serialize,
{
let vec = to_vec_pretty(value, indent)?;
let string = String::from_utf8(vec).map_err(|err| Error::from(err.utf8_error()))?;
Ok(string)
}

struct Serializer<W, F = CompactFormatter> {
writer: W,
formatter: F,
Expand All @@ -39,6 +49,17 @@ where
}
}

impl<'a, W> Serializer<W, PrettyFormatter<'a>>
where
W: io::Write,
{
/// Creates a new JSON pretty print serializer.
#[inline]
pub fn pretty(writer: W, indent: &'a str) -> Self {
Serializer::with_formatter(writer, PrettyFormatter::with_indent(indent.as_bytes()))
}
}

impl<W, F> Serializer<W, F>
where
W: io::Write,
Expand Down Expand Up @@ -553,6 +574,24 @@ where
Ok(writer)
}

pub fn to_writer_pretty<W, T>(writer: W, value: &T, indent: &str) -> Result<()>
where
W: io::Write,
T: ?Sized + Serialize,
{
let mut ser = Serializer::pretty(writer, indent);
value.serialize(&mut ser)
}

pub fn to_vec_pretty<T>(value: &T, indent: &str) -> Result<Vec<u8>>
where
T: ?Sized + Serialize,
{
let mut writer = Vec::with_capacity(128);
to_writer_pretty(&mut writer, value, indent)?;
Ok(writer)
}

/// Represents a character escape code in a type-safe manner.
pub enum CharEscape {
/// An escaped quote `"`
Expand Down
12 changes: 12 additions & 0 deletions core/translate/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1108,6 +1108,18 @@ pub fn translate_expr(
});
Ok(target_register)
}
JsonFunc::JsonPretty => {
let args = expect_arguments_max!(args, 2, j);

translate_function(
program,
args,
referenced_tables,
resolver,
target_register,
func_ctx,
)
}
},
Func::Scalar(srf) => {
match srf {
Expand Down
36 changes: 35 additions & 1 deletion core/vdbe/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1690,7 +1690,7 @@ impl Program {
crate::function::Func::Json(json_func) => match json_func {
JsonFunc::Json => {
let json_value = &state.registers[*start_reg];
let json_str = get_json(json_value);
let json_str = get_json(json_value, None);
match json_str {
Ok(json) => state.registers[*dest] = json,
Err(e) => return Err(e),
Expand Down Expand Up @@ -1787,6 +1787,40 @@ impl Program {
&state.registers[*start_reg..*start_reg + arg_count],
)?;
}
JsonFunc::JsonPretty => {
let json_value = &state.registers[*start_reg];
let indent = if arg_count > 1 {
Some(&state.registers[*start_reg + 1])
} else {
None
};

// Blob should be converted to Ascii in a lossy way
// However, Rust strings uses utf-8
// so the behavior at the moment is slightly different
// To the way blobs are parsed here in SQLite.
let indent = match indent {
Some(value) => match value {
OwnedValue::Text(text) => text.value.as_str(),
OwnedValue::Integer(val) => &val.to_string(),
OwnedValue::Float(val) => &val.to_string(),
OwnedValue::Blob(val) => &String::from_utf8_lossy(val),
OwnedValue::Agg(ctx) => match ctx.final_value() {
OwnedValue::Text(text) => text.value.as_str(),
OwnedValue::Integer(val) => &val.to_string(),
OwnedValue::Float(val) => &val.to_string(),
OwnedValue::Blob(val) => &String::from_utf8_lossy(val),
_ => " ",
},
_ => " ",
},
// If the second argument is omitted or is NULL, then indentation is four spaces per level
None => " ",
};

let json_str = get_json(json_value, Some(indent))?;
state.registers[*dest] = json_str;
}
},
crate::function::Func::Scalar(scalar_func) => match scalar_func {
ScalarFunc::Cast => {
Expand Down
Loading
Loading