Skip to content

Commit 040004d

Browse files
committed
Implement json_quote
1 parent c27427d commit 040004d

File tree

6 files changed

+102
-1
lines changed

6 files changed

+102
-1
lines changed

core/function.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ pub enum JsonFunc {
7979
JsonObject,
8080
JsonType,
8181
JsonErrorPosition,
82+
JsonQuote,
8283
}
8384

8485
#[cfg(feature = "json")]
@@ -97,6 +98,7 @@ impl Display for JsonFunc {
9798
Self::JsonObject => "json_object".to_string(),
9899
Self::JsonType => "json_type".to_string(),
99100
Self::JsonErrorPosition => "json_error_position".to_string(),
101+
Self::JsonQuote => "json_quote".to_string(),
100102
}
101103
)
102104
}
@@ -517,6 +519,8 @@ impl Func {
517519
"json_type" => Ok(Func::Json(JsonFunc::JsonType)),
518520
#[cfg(feature = "json")]
519521
"json_error_position" => Ok(Self::Json(JsonFunc::JsonErrorPosition)),
522+
#[cfg(feature = "json")]
523+
"json_quote" => Ok(Self::Json(JsonFunc::JsonQuote)),
520524
"unixepoch" => Ok(Self::Scalar(ScalarFunc::UnixEpoch)),
521525
"julianday" => Ok(Self::Scalar(ScalarFunc::JulianDay)),
522526
"hex" => Ok(Self::Scalar(ScalarFunc::Hex)),

core/json/mod.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub use crate::json::ser::to_string;
1212
use crate::types::{LimboText, OwnedValue, TextSubtype};
1313
use indexmap::IndexMap;
1414
use jsonb::Error as JsonbError;
15+
use ser::escape;
1516
use serde::{Deserialize, Serialize};
1617

1718
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
@@ -448,6 +449,36 @@ pub fn json_object(values: &[OwnedValue]) -> crate::Result<OwnedValue> {
448449
Ok(OwnedValue::Text(LimboText::json(Rc::new(result))))
449450
}
450451

452+
pub fn json_quote(value: &OwnedValue) -> crate::Result<OwnedValue> {
453+
match value {
454+
OwnedValue::Text(ref t) => {
455+
// If X is a JSON value returned by another JSON function,
456+
// then this function is a no-op
457+
if t.subtype == TextSubtype::Json {
458+
// Should just return the json value with no quotes
459+
return Ok(value.to_owned());
460+
}
461+
462+
let quoted_value = format!("\"{}\"", escape(&t.value));
463+
464+
Ok(OwnedValue::Text(LimboText::new(Rc::new(quoted_value))))
465+
}
466+
// Numbers are unquoted in json
467+
OwnedValue::Integer(ref int) => Ok(OwnedValue::Integer(int.to_owned())),
468+
OwnedValue::Float(ref float) => Ok(OwnedValue::Float(float.to_owned())),
469+
OwnedValue::Blob(_) => crate::bail_constraint_error!("JSON cannot hold BLOB values"),
470+
OwnedValue::Null => {
471+
let null_value = "null".to_string();
472+
473+
Ok(OwnedValue::Text(LimboText::new(Rc::new(null_value))))
474+
}
475+
_ => {
476+
// TODO not too sure what message should be here
477+
crate::bail_parse_error!("Syntax error");
478+
}
479+
}
480+
}
481+
451482
#[cfg(test)]
452483
mod tests {
453484
use super::*;

core/json/ser.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,11 @@ impl ser::SerializeStructVariant for &mut Serializer {
368368
}
369369
}
370370

371-
fn escape(v: &str) -> String {
371+
// TODO not sure if this function can be public
372+
// But wanted to avoid having the same copy of the function in two different places
373+
// Maybe this function could be placed in mod file instead
374+
// As it is used by the json quote funcion
375+
pub fn escape(v: &str) -> String {
372376
v.chars()
373377
.flat_map(|c| match c {
374378
'"' => vec!['\\', c],

core/translate/expr.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,18 @@ pub fn translate_expr(
899899
JsonFunc::JsonObject => {
900900
let args = expect_arguments_even!(args, j);
901901

902+
translate_function(
903+
program,
904+
&args,
905+
referenced_tables,
906+
resolver,
907+
target_register,
908+
func_ctx,
909+
)
910+
}
911+
JsonFunc::JsonQuote => {
912+
let args = expect_arguments_exact!(args, 1, j);
913+
902914
translate_function(
903915
program,
904916
&args,

core/vdbe/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ pub mod sorter;
2727
use crate::error::{LimboError, SQLITE_CONSTRAINT_PRIMARYKEY};
2828
use crate::ext::ExtValue;
2929
use crate::function::{AggFunc, ExtFunc, FuncCtx, MathFunc, MathFuncArity, ScalarFunc};
30+
use crate::json::json_quote;
3031
use crate::pseudo::PseudoCursor;
3132
use crate::result::LimboResult;
3233
use crate::storage::sqlite3_ondisk::DatabaseHeader;
@@ -1698,6 +1699,14 @@ impl Program {
16981699
Err(e) => return Err(e),
16991700
}
17001701
}
1702+
JsonFunc::JsonQuote => {
1703+
let json_value = &state.registers[*start_reg];
1704+
1705+
match json_quote(json_value) {
1706+
Ok(result) => state.registers[*dest] = result,
1707+
Err(e) => return Err(e),
1708+
}
1709+
}
17011710
},
17021711
crate::function::Func::Scalar(scalar_func) => match scalar_func {
17031712
ScalarFunc::Cast => {

testing/json.test

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,3 +544,44 @@ do_execsql_test json_from_json_object {
544544
#do_execsql_test json_object_duplicated_keys {
545545
# SELECT json_object('key', 'value', 'key', 'value2');
546546
#} {{{"key":"value2"}}}
547+
548+
# The json_quote() function transforms an SQL value into a JSON value.
549+
# String values are quoted and interior quotes are escaped. NULL values
550+
# are rendered as the unquoted string "null".
551+
#
552+
do_execsql_test json_quote_string_literal {
553+
SELECT json_quote('abc"xyz');
554+
} {{"abc\"xyz"}}
555+
do_execsql_test json_quote_float {
556+
SELECT json_quote(3.14159);
557+
} {3.14159}
558+
do_execsql_test json_quote_integer {
559+
SELECT json_quote(12345);
560+
} {12345}
561+
do_execsql_test json_quote_null {
562+
SELECT json_quote(null);
563+
} {"null"}
564+
do_execsql_test json_quote_null_caps {
565+
SELECT json_quote(NULL);
566+
} null
567+
do_execsql_test json_quote_json_value {
568+
SELECT json_quote(json('{a:1, b: "test"}'));
569+
} {{{"a":1,"b":"test"}}}
570+
571+
572+
# Escape character tests in sqlite source depend on json_valid
573+
# See https://github.com/sqlite/sqlite/blob/255548562b125e6c148bb27d49aaa01b2fe61dba/test/json102.test#L690
574+
# So for now not all control characters escaped are tested
575+
576+
577+
# TODO No catchsql tests function to test these on
578+
#do_catchsql_test json_quote_blob {
579+
# SELECT json_quote(x'3031323334');
580+
#} {1 {JSON cannot hold BLOB values}}
581+
#do_catchsql_test json_quote_more_than_one_arg {
582+
# SELECT json_quote(123,456)
583+
#} {1 {json_quote function called with not exactly 1 arguments}}
584+
#do_catchsql_test json_quote_no_args {
585+
# SELECT json_quote()
586+
#} {1 {json_quote function with no arguments}}
587+

0 commit comments

Comments
 (0)