From 040004d4ef4e8a63a388ee6e86deeb9541e67984 Mon Sep 17 00:00:00 2001 From: pedrocarlo Date: Tue, 21 Jan 2025 15:46:52 -0300 Subject: [PATCH] Implement json_quote --- core/function.rs | 4 ++++ core/json/mod.rs | 31 +++++++++++++++++++++++++++++++ core/json/ser.rs | 6 +++++- core/translate/expr.rs | 12 ++++++++++++ core/vdbe/mod.rs | 9 +++++++++ testing/json.test | 41 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 102 insertions(+), 1 deletion(-) diff --git a/core/function.rs b/core/function.rs index 5023c3c94..21c608551 100644 --- a/core/function.rs +++ b/core/function.rs @@ -79,6 +79,7 @@ pub enum JsonFunc { JsonObject, JsonType, JsonErrorPosition, + JsonQuote, } #[cfg(feature = "json")] @@ -97,6 +98,7 @@ impl Display for JsonFunc { Self::JsonObject => "json_object".to_string(), Self::JsonType => "json_type".to_string(), Self::JsonErrorPosition => "json_error_position".to_string(), + Self::JsonQuote => "json_quote".to_string(), } ) } @@ -517,6 +519,8 @@ impl Func { "json_type" => Ok(Func::Json(JsonFunc::JsonType)), #[cfg(feature = "json")] "json_error_position" => Ok(Self::Json(JsonFunc::JsonErrorPosition)), + #[cfg(feature = "json")] + "json_quote" => Ok(Self::Json(JsonFunc::JsonQuote)), "unixepoch" => Ok(Self::Scalar(ScalarFunc::UnixEpoch)), "julianday" => Ok(Self::Scalar(ScalarFunc::JulianDay)), "hex" => Ok(Self::Scalar(ScalarFunc::Hex)), diff --git a/core/json/mod.rs b/core/json/mod.rs index fee87e3df..2cac9485e 100644 --- a/core/json/mod.rs +++ b/core/json/mod.rs @@ -12,6 +12,7 @@ pub use crate::json::ser::to_string; use crate::types::{LimboText, OwnedValue, TextSubtype}; use indexmap::IndexMap; use jsonb::Error as JsonbError; +use ser::escape; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] @@ -448,6 +449,36 @@ pub fn json_object(values: &[OwnedValue]) -> crate::Result { Ok(OwnedValue::Text(LimboText::json(Rc::new(result)))) } +pub fn json_quote(value: &OwnedValue) -> crate::Result { + match value { + OwnedValue::Text(ref t) => { + // If X is a JSON value returned by another JSON function, + // then this function is a no-op + if t.subtype == TextSubtype::Json { + // Should just return the json value with no quotes + return Ok(value.to_owned()); + } + + let quoted_value = format!("\"{}\"", escape(&t.value)); + + Ok(OwnedValue::Text(LimboText::new(Rc::new(quoted_value)))) + } + // Numbers are unquoted in json + OwnedValue::Integer(ref int) => Ok(OwnedValue::Integer(int.to_owned())), + OwnedValue::Float(ref float) => Ok(OwnedValue::Float(float.to_owned())), + OwnedValue::Blob(_) => crate::bail_constraint_error!("JSON cannot hold BLOB values"), + OwnedValue::Null => { + let null_value = "null".to_string(); + + Ok(OwnedValue::Text(LimboText::new(Rc::new(null_value)))) + } + _ => { + // TODO not too sure what message should be here + crate::bail_parse_error!("Syntax error"); + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/core/json/ser.rs b/core/json/ser.rs index 3d5646584..2c34c0e64 100644 --- a/core/json/ser.rs +++ b/core/json/ser.rs @@ -368,7 +368,11 @@ impl ser::SerializeStructVariant for &mut Serializer { } } -fn escape(v: &str) -> String { +// TODO not sure if this function can be public +// But wanted to avoid having the same copy of the function in two different places +// Maybe this function could be placed in mod file instead +// As it is used by the json quote funcion +pub fn escape(v: &str) -> String { v.chars() .flat_map(|c| match c { '"' => vec!['\\', c], diff --git a/core/translate/expr.rs b/core/translate/expr.rs index 349a5f2b3..9022be912 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -899,6 +899,18 @@ pub fn translate_expr( JsonFunc::JsonObject => { let args = expect_arguments_even!(args, j); + translate_function( + program, + &args, + referenced_tables, + resolver, + target_register, + func_ctx, + ) + } + JsonFunc::JsonQuote => { + let args = expect_arguments_exact!(args, 1, j); + translate_function( program, &args, diff --git a/core/vdbe/mod.rs b/core/vdbe/mod.rs index b8c5f9474..e01d2339e 100644 --- a/core/vdbe/mod.rs +++ b/core/vdbe/mod.rs @@ -27,6 +27,7 @@ pub mod sorter; use crate::error::{LimboError, SQLITE_CONSTRAINT_PRIMARYKEY}; use crate::ext::ExtValue; use crate::function::{AggFunc, ExtFunc, FuncCtx, MathFunc, MathFuncArity, ScalarFunc}; +use crate::json::json_quote; use crate::pseudo::PseudoCursor; use crate::result::LimboResult; use crate::storage::sqlite3_ondisk::DatabaseHeader; @@ -1698,6 +1699,14 @@ impl Program { Err(e) => return Err(e), } } + JsonFunc::JsonQuote => { + let json_value = &state.registers[*start_reg]; + + match json_quote(json_value) { + Ok(result) => state.registers[*dest] = result, + Err(e) => return Err(e), + } + } }, crate::function::Func::Scalar(scalar_func) => match scalar_func { ScalarFunc::Cast => { diff --git a/testing/json.test b/testing/json.test index ea1c9bf0f..1984d1c26 100755 --- a/testing/json.test +++ b/testing/json.test @@ -544,3 +544,44 @@ do_execsql_test json_from_json_object { #do_execsql_test json_object_duplicated_keys { # SELECT json_object('key', 'value', 'key', 'value2'); #} {{{"key":"value2"}}} + +# The json_quote() function transforms an SQL value into a JSON value. +# String values are quoted and interior quotes are escaped. NULL values +# are rendered as the unquoted string "null". +# +do_execsql_test json_quote_string_literal { + SELECT json_quote('abc"xyz'); +} {{"abc\"xyz"}} +do_execsql_test json_quote_float { + SELECT json_quote(3.14159); +} {3.14159} +do_execsql_test json_quote_integer { + SELECT json_quote(12345); +} {12345} +do_execsql_test json_quote_null { + SELECT json_quote(null); +} {"null"} +do_execsql_test json_quote_null_caps { + SELECT json_quote(NULL); +} null +do_execsql_test json_quote_json_value { + SELECT json_quote(json('{a:1, b: "test"}')); +} {{{"a":1,"b":"test"}}} + + +# Escape character tests in sqlite source depend on json_valid +# See https://github.com/sqlite/sqlite/blob/255548562b125e6c148bb27d49aaa01b2fe61dba/test/json102.test#L690 +# So for now not all control characters escaped are tested + + +# TODO No catchsql tests function to test these on +#do_catchsql_test json_quote_blob { +# SELECT json_quote(x'3031323334'); +#} {1 {JSON cannot hold BLOB values}} +#do_catchsql_test json_quote_more_than_one_arg { +# SELECT json_quote(123,456) +#} {1 {json_quote function called with not exactly 1 arguments}} +#do_catchsql_test json_quote_no_args { +# SELECT json_quote() +#} {1 {json_quote function with no arguments}} +