diff --git a/COMPAT.md b/COMPAT.md index fb8a815b9..86ab19751 100644 --- a/COMPAT.md +++ b/COMPAT.md @@ -235,7 +235,7 @@ Modifiers: | Modifier | Status| Comment | |----------------|-------|---------------------------------| -| Days | Yes | | +| Days | Yes | | | Hours | Yes | | | Minutes | Yes | | | Seconds | Yes | | @@ -274,7 +274,7 @@ Modifiers: | json ->> path | Yes | | | json_insert(json,path,value,...) | | | | jsonb_insert(json,path,value,...) | | | -| json_object(label1,value1,...) | | | +| json_object(label1,value1,...) | Yes | When keys are duplicated, only the last one processed is returned. This differs from sqlite, where the keys in the output can be duplicated | | jsonb_object(label1,value1,...) | | | | json_patch(json1,json2) | | | | jsonb_patch(json1,json2) | | | diff --git a/core/function.rs b/core/function.rs index e61eff826..5023c3c94 100644 --- a/core/function.rs +++ b/core/function.rs @@ -76,6 +76,7 @@ pub enum JsonFunc { JsonArrowExtract, JsonArrowShiftExtract, JsonExtract, + JsonObject, JsonType, JsonErrorPosition, } @@ -93,6 +94,7 @@ impl Display for JsonFunc { Self::JsonArrayLength => "json_array_length".to_string(), Self::JsonArrowExtract => "->".to_string(), Self::JsonArrowShiftExtract => "->>".to_string(), + Self::JsonObject => "json_object".to_string(), Self::JsonType => "json_type".to_string(), Self::JsonErrorPosition => "json_error_position".to_string(), } @@ -510,6 +512,8 @@ impl Func { #[cfg(feature = "json")] "json_extract" => Ok(Func::Json(JsonFunc::JsonExtract)), #[cfg(feature = "json")] + "json_object" => Ok(Func::Json(JsonFunc::JsonObject)), + #[cfg(feature = "json")] "json_type" => Ok(Func::Json(JsonFunc::JsonType)), #[cfg(feature = "json")] "json_error_position" => Ok(Self::Json(JsonFunc::JsonErrorPosition)), diff --git a/core/json/mod.rs b/core/json/mod.rs index 01d32075f..fee87e3df 100644 --- a/core/json/mod.rs +++ b/core/json/mod.rs @@ -85,6 +85,8 @@ pub fn json_array(values: &[OwnedValue]) -> crate::Result { let mut s = String::new(); s.push('['); + // TODO: use `convert_db_type_to_json` and map each value with that function, + // so we can construct a `Val::Array` with each value and then serialize it directly. for (idx, value) in values.iter().enumerate() { match value { OwnedValue::Blob(_) => crate::bail_constraint_error!("JSON cannot hold BLOB values"), @@ -256,6 +258,29 @@ fn convert_json_to_db_type(extracted: &Val, all_as_db: bool) -> crate::Result crate::Result { + let val = match value { + OwnedValue::Null => Val::Null, + OwnedValue::Float(f) => Val::Float(*f), + OwnedValue::Integer(i) => Val::Integer(*i), + OwnedValue::Text(t) => match t.subtype { + // Convert only to json if the subtype is json (if we got it from another json function) + TextSubtype::Json => get_json_value(value)?, + TextSubtype::Text => Val::String(t.value.to_string()), + }, + OwnedValue::Blob(_) => crate::bail_constraint_error!("JSON cannot hold BLOB values"), + unsupported_value => crate::bail_constraint_error!( + "JSON cannot hold this type of value: {unsupported_value:?}" + ), + }; + Ok(val) +} + pub fn json_type(value: &OwnedValue, path: Option<&OwnedValue>) -> crate::Result { if let OwnedValue::Null = value { return Ok(OwnedValue::Null); @@ -399,6 +424,30 @@ pub fn json_error_position(json: &OwnedValue) -> crate::Result { } } +/// Constructs a JSON object from a list of values that represent key-value pairs. +/// The number of values must be even, and the first value of each pair (which represents the map key) +/// must be a TEXT value. The second value of each pair can be any JSON value (which represents the map value) +pub fn json_object(values: &[OwnedValue]) -> crate::Result { + let value_map = values + .chunks(2) + .map(|chunk| match chunk { + [key, value] => { + let key = match key { + OwnedValue::Text(t) => t.value.to_string(), + _ => crate::bail_constraint_error!("labels must be TEXT"), + }; + let json_val = convert_db_type_to_json(value)?; + + Ok((key, json_val)) + } + _ => crate::bail_constraint_error!("json_object requires an even number of values"), + }) + .collect::, _>>()?; + + let result = crate::json::to_string(&value_map).unwrap(); + Ok(OwnedValue::Text(LimboText::json(Rc::new(result)))) +} + #[cfg(test)] mod tests { use super::*; @@ -783,4 +832,155 @@ mod tests { let result = json_error_position(&input).unwrap(); assert_eq!(result, OwnedValue::Integer(16)); } + + #[test] + fn test_json_object_simple() { + let key = OwnedValue::build_text(Rc::new("key".to_string())); + let value = OwnedValue::build_text(Rc::new("value".to_string())); + let input = vec![key, value]; + + let result = json_object(&input).unwrap(); + let OwnedValue::Text(json_text) = result else { + panic!("Expected OwnedValue::Text"); + }; + assert_eq!(json_text.value.as_str(), r#"{"key":"value"}"#); + } + + #[test] + fn test_json_object_multiple_values() { + let text_key = OwnedValue::build_text(Rc::new("text_key".to_string())); + let text_value = OwnedValue::build_text(Rc::new("text_value".to_string())); + let json_key = OwnedValue::build_text(Rc::new("json_key".to_string())); + let json_value = OwnedValue::Text(LimboText::json(Rc::new( + r#"{"json":"value","number":1}"#.to_string(), + ))); + let integer_key = OwnedValue::build_text(Rc::new("integer_key".to_string())); + let integer_value = OwnedValue::Integer(1); + let float_key = OwnedValue::build_text(Rc::new("float_key".to_string())); + let float_value = OwnedValue::Float(1.1); + let null_key = OwnedValue::build_text(Rc::new("null_key".to_string())); + let null_value = OwnedValue::Null; + + let input = vec![ + text_key, + text_value, + json_key, + json_value, + integer_key, + integer_value, + float_key, + float_value, + null_key, + null_value, + ]; + + let result = json_object(&input).unwrap(); + let OwnedValue::Text(json_text) = result else { + panic!("Expected OwnedValue::Text"); + }; + assert_eq!( + json_text.value.as_str(), + r#"{"text_key":"text_value","json_key":{"json":"value","number":1},"integer_key":1,"float_key":1.1,"null_key":null}"# + ); + } + + #[test] + fn test_json_object_json_value_is_rendered_as_json() { + let key = OwnedValue::build_text(Rc::new("key".to_string())); + let value = OwnedValue::Text(LimboText::json(Rc::new(r#"{"json":"value"}"#.to_string()))); + let input = vec![key, value]; + + let result = json_object(&input).unwrap(); + let OwnedValue::Text(json_text) = result else { + panic!("Expected OwnedValue::Text"); + }; + assert_eq!(json_text.value.as_str(), r#"{"key":{"json":"value"}}"#); + } + + #[test] + fn test_json_object_json_text_value_is_rendered_as_regular_text() { + let key = OwnedValue::build_text(Rc::new("key".to_string())); + let value = OwnedValue::Text(LimboText::new(Rc::new(r#"{"json":"value"}"#.to_string()))); + let input = vec![key, value]; + + let result = json_object(&input).unwrap(); + let OwnedValue::Text(json_text) = result else { + panic!("Expected OwnedValue::Text"); + }; + assert_eq!( + json_text.value.as_str(), + r#"{"key":"{\"json\":\"value\"}"}"# + ); + } + + #[test] + fn test_json_object_nested() { + let key = OwnedValue::build_text(Rc::new("key".to_string())); + let value = OwnedValue::build_text(Rc::new("value".to_string())); + let input = vec![key, value]; + + let parent_key = OwnedValue::build_text(Rc::new("parent_key".to_string())); + let parent_value = json_object(&input).unwrap(); + let parent_input = vec![parent_key, parent_value]; + + let result = json_object(&parent_input).unwrap(); + + let OwnedValue::Text(json_text) = result else { + panic!("Expected OwnedValue::Text"); + }; + assert_eq!( + json_text.value.as_str(), + r#"{"parent_key":{"key":"value"}}"# + ); + } + + #[test] + fn test_json_object_duplicated_keys() { + let key = OwnedValue::build_text(Rc::new("key".to_string())); + let value = OwnedValue::build_text(Rc::new("value".to_string())); + let input = vec![key.clone(), value.clone(), key, value]; + + let result = json_object(&input).unwrap(); + let OwnedValue::Text(json_text) = result else { + panic!("Expected OwnedValue::Text"); + }; + assert_eq!(json_text.value.as_str(), r#"{"key":"value"}"#); + } + + #[test] + fn test_json_object_empty() { + let input = vec![]; + + let result = json_object(&input).unwrap(); + let OwnedValue::Text(json_text) = result else { + panic!("Expected OwnedValue::Text"); + }; + assert_eq!(json_text.value.as_str(), r#"{}"#); + } + + #[test] + fn test_json_object_non_text_key() { + let key = OwnedValue::Integer(1); + let value = OwnedValue::build_text(Rc::new("value".to_string())); + let input = vec![key, value]; + + match json_object(&input) { + Ok(_) => panic!("Expected error for non-TEXT key"), + Err(e) => assert!(e.to_string().contains("labels must be TEXT")), + } + } + + #[test] + fn test_json_odd_number_of_values() { + let key = OwnedValue::build_text(Rc::new("key".to_string())); + let value = OwnedValue::build_text(Rc::new("value".to_string())); + let input = vec![key.clone(), value, key]; + + match json_object(&input) { + Ok(_) => panic!("Expected error for odd number of values"), + Err(e) => assert!(e + .to_string() + .contains("json_object requires an even number of values")), + } + } } diff --git a/core/translate/expr.rs b/core/translate/expr.rs index da03cd92f..c16ee8ae3 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -126,6 +126,24 @@ macro_rules! expect_arguments_min { }}; } +macro_rules! expect_arguments_even { + ( + $args:expr, + $func:ident + ) => {{ + let args = $args.as_deref().unwrap_or_default(); + if args.len() % 2 != 0 { + crate::bail_parse_error!( + "{} function requires an even number of arguments", + $func.to_string() + ); + }; + // The only function right now that requires an even number is `json_object` and it allows + // to have no arguments, so thats why in this macro we do not bail with teh `function with no arguments` error + args + }}; +} + pub fn translate_condition_expr( program: &mut ProgramBuilder, referenced_tables: &[TableReference], @@ -864,6 +882,18 @@ pub fn translate_expr( }); Ok(target_register) } + JsonFunc::JsonObject => { + let args = expect_arguments_even!(args, j); + + translate_function( + program, + &args, + referenced_tables, + resolver, + target_register, + func_ctx, + ) + } }, Func::Scalar(srf) => { match srf { diff --git a/core/vdbe/mod.rs b/core/vdbe/mod.rs index cc690bd42..44d192f18 100644 --- a/core/vdbe/mod.rs +++ b/core/vdbe/mod.rs @@ -41,7 +41,7 @@ use crate::vdbe::insn::Insn; use crate::{ function::JsonFunc, json::get_json, json::json_array, json::json_array_length, json::json_arrow_extract, json::json_arrow_shift_extract, json::json_error_position, - json::json_extract, json::json_type, + json::json_extract, json::json_object, json::json_type, }; use crate::{resolve_ext_path, Connection, Result, Rows, TransactionState, DATABASE_VERSION}; use datetime::{exec_date, exec_datetime_full, exec_julianday, exec_time, exec_unixepoch}; @@ -1571,13 +1571,18 @@ impl Program { Err(e) => return Err(e), } } - JsonFunc::JsonArray => { + JsonFunc::JsonArray | JsonFunc::JsonObject => { let reg_values = &state.registers[*start_reg..*start_reg + arg_count]; - let json_array = json_array(reg_values); + let json_func = match json_func { + JsonFunc::JsonArray => json_array, + JsonFunc::JsonObject => json_object, + _ => unreachable!(), + }; + let json_result = json_func(reg_values); - match json_array { + match json_result { Ok(json) => state.registers[*dest] = json, Err(e) => return Err(e), } diff --git a/testing/json.test b/testing/json.test index 758243bcd..ea1c9bf0f 100755 --- a/testing/json.test +++ b/testing/json.test @@ -506,3 +506,41 @@ do_execsql_test json_error_position_null { do_execsql_test json_error_position_complex { SELECT json_error_position('{a:null,{"h":[1,[1,2,3]],"j":"abc"}:true}'); } {{9}} + +do_execsql_test json_object_simple { + SELECT json_object('key', 'value'); +} {{{"key":"value"}}} + +do_execsql_test json_object_nested { + SELECT json_object('grandparent',json_object('parent', json_object('child', 'value'))); +} {{{"grandparent":{"parent":{"child":"value"}}}}} + +do_execsql_test json_object_quoted_json { + SELECT json_object('parent', '{"child":"value"}'); +} {{{"parent":"{\"child\":\"value\"}"}}} + +do_execsql_test json_object_unquoted_json { + SELECT json_object('parent', json('{"child":"value"}')); +} {{{"parent":{"child":"value"}}}} + +do_execsql_test json_object_multiple_values { + SELECT json_object('text', 'value', 'json', json_object('key', 'value'), 'int', 1, 'float', 1.5, 'null', null); +} {{{"text":"value","json":{"key":"value"},"int":1,"float":1.5,"null":null}}} + +do_execsql_test json_object_empty { + SELECT json_object(); +} {{{}}} + +do_execsql_test json_object_json_array { + SELECT json_object('ex',json('[52,3]')); +} {{{"ex":[52,3]}}} + +do_execsql_test json_from_json_object { + SELECT json(json_object('key','value')); +} {{{"key":"value"}}} + +# FIXME: this behaviour differs from sqlite. Although, sqlite docs states +# that this could change in a "future enhancement" (https://www.sqlite.org/json1.html#jobj) +#do_execsql_test json_object_duplicated_keys { +# SELECT json_object('key', 'value', 'key', 'value2'); +#} {{{"key":"value2"}}}