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
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
implement json_pretty
pedrocarlo committed Feb 2, 2025
commit 2e115d948d41424ee45ac646f4390d29485b039a
5 changes: 3 additions & 2 deletions COMPAT.md
Original file line number Diff line number Diff line change
@@ -23,6 +23,7 @@ This document describes the compatibility of Limbo with SQLite.
- [Extensions](#extensions)
- [UUID](#uuid)
- [regexp](#regexp)
- [Vector](#vector)

## Features

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

| Function | Status | Comment |
|------------------------------------|---------|----------------------------------------------------------------------------------------------------------------------------------------------|
| ---------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| json(json) | Partial | |
| jsonb(json) | | |
| json_array(value1,value2,...) | Yes | |
@@ -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,...) | | |
4 changes: 4 additions & 0 deletions core/function.rs
Original file line number Diff line number Diff line change
@@ -82,6 +82,7 @@ pub enum JsonFunc {
JsonValid,
JsonPatch,
JsonRemove,
JsonPretty,
}

#[cfg(feature = "json")]
@@ -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(),
}
)
}
@@ -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)),
37 changes: 22 additions & 15 deletions core/json/mod.rs
Original file line number Diff line number Diff line change
@@ -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)]
@@ -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
@@ -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))))
}
@@ -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))))
}
@@ -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);
@@ -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);
@@ -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);
@@ -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);
@@ -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);
@@ -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")),
@@ -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);
@@ -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")),
@@ -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);
@@ -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")),
@@ -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 {
@@ -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 {
39 changes: 39 additions & 0 deletions core/json/ser.rs
Original file line number Diff line number Diff line change
@@ -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,
@@ -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,
@@ -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 `"`
12 changes: 12 additions & 0 deletions core/translate/expr.rs
Original file line number Diff line number Diff line change
@@ -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 {
36 changes: 35 additions & 1 deletion core/vdbe/mod.rs
Original file line number Diff line number Diff line change
@@ -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),
@@ -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 => {
Loading