diff --git a/COMPAT.md b/COMPAT.md index 83888f87c..4ddabe10f 100644 --- a/COMPAT.md +++ b/COMPAT.md @@ -281,8 +281,8 @@ Feature support of [sqlite expr syntax](https://www.sqlite.org/lang_expr.html). | jsonb_replace(json,path,value,...) | | | | json_set(json,path,value,...) | | | | jsonb_set(json,path,value,...) | | | -| json_type(json) | | | -| json_type(json,path) | | | +| json_type(json) | Yes | | +| json_type(json,path) | Yes | | | json_valid(json) | | | | json_valid(json,flags) | | | | json_quote(value) | | | diff --git a/core/function.rs b/core/function.rs index 7385b237f..94d752eb5 100644 --- a/core/function.rs +++ b/core/function.rs @@ -27,6 +27,7 @@ pub enum JsonFunc { JsonArray, JsonExtract, JsonArrayLength, + JsonType, } #[cfg(feature = "json")] @@ -40,6 +41,7 @@ impl Display for JsonFunc { Self::JsonArray => "json_array".to_string(), Self::JsonExtract => "json_extract".to_string(), Self::JsonArrayLength => "json_array_length".to_string(), + Self::JsonType => "json_type".to_string(), } ) } @@ -371,6 +373,8 @@ impl Func { "json_array" => Ok(Self::Json(JsonFunc::JsonArray)), #[cfg(feature = "json")] "json_extract" => Ok(Func::Json(JsonFunc::JsonExtract)), + #[cfg(feature = "json")] + "json_type" => Ok(Func::Json(JsonFunc::JsonType)), "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 eda97bf2b..8ee36b33a 100644 --- a/core/json/mod.rs +++ b/core/json/mod.rs @@ -121,17 +121,13 @@ pub fn json_array_length( json_value: &OwnedValue, json_path: Option<&OwnedValue>, ) -> crate::Result { - let path = match json_path { - Some(OwnedValue::Text(t)) => Some(t.value.to_string()), - Some(OwnedValue::Integer(i)) => Some(i.to_string()), - Some(OwnedValue::Float(f)) => Some(f.to_string()), - _ => None::, - }; - let json = get_json_value(json_value)?; - let arr_val = if let Some(path) = path { - &json_extract_single(&json, path.as_str())? + let arr_val = if let Some(path) = json_path { + match json_extract_single(&json, path)? { + Some(val) => val, + None => return Ok(OwnedValue::Null), + } } else { &json }; @@ -161,10 +157,13 @@ pub fn json_extract(value: &OwnedValue, paths: &[OwnedValue]) -> crate::Result { - let extracted = json_extract_single(&json, p.value.as_str())?; + OwnedValue::Null => { + return Ok(OwnedValue::Null); + } + _ => { + let extracted = json_extract_single(&json, path)?.unwrap_or_else(|| &Val::Null); - if paths.len() == 1 && extracted == Val::Null { + if paths.len() == 1 && extracted == &Val::Null { return Ok(OwnedValue::Null); } @@ -173,8 +172,6 @@ pub fn json_extract(value: &OwnedValue, paths: &[OwnedValue]) -> crate::Result return Ok(OwnedValue::Null), - _ => crate::bail_constraint_error!("JSON path error near: {:?}", path.to_string()), } } @@ -186,8 +183,49 @@ pub fn json_extract(value: &OwnedValue, paths: &[OwnedValue]) -> crate::Result crate::Result { - let json_path = json_path(path)?; +pub fn json_type(value: &OwnedValue, path: Option<&OwnedValue>) -> crate::Result { + if let OwnedValue::Null = value { + return Ok(OwnedValue::Null); + } + + let json = get_json_value(value)?; + + let json = if let Some(path) = path { + match json_extract_single(&json, path)? { + Some(val) => val, + None => return Ok(OwnedValue::Null), + } + } else { + &json + }; + + let val = match json { + Val::Null => "null", + Val::Bool(v) => { + if *v { + "true" + } else { + "false" + } + } + Val::Integer(_) => "integer", + Val::Float(_) => "real", + Val::String(_) => "text", + Val::Array(_) => "array", + Val::Object(_) => "object", + }; + + Ok(OwnedValue::Text(LimboText::json(Rc::new(val.to_string())))) +} + +/// Returns the value at the given JSON path. If the path does not exist, it returns None. +/// If the path is an invalid path, returns an error. +fn json_extract_single<'a>(json: &'a Val, path: &OwnedValue) -> crate::Result> { + let json_path = match path { + OwnedValue::Text(t) => json_path(t.value.as_str())?, + OwnedValue::Null => return Ok(None), + _ => crate::bail_constraint_error!("JSON path error near: {:?}", path.to_string()), + }; let mut current_element = &Val::Null; @@ -204,12 +242,10 @@ fn json_extract_single(json: &Val, path: &str) -> crate::Result { if let Some(value) = map.get(key) { current_element = value; } else { - return Ok(Val::Null); + return Ok(None); } } - _ => { - return Ok(Val::Null); - } + _ => return Ok(None), } } PathElement::ArrayLocator(idx) => match current_element { @@ -223,16 +259,15 @@ fn json_extract_single(json: &Val, path: &str) -> crate::Result { if idx < array.len() as i32 { current_element = &array[idx as usize]; } else { - return Ok(Val::Null); + return Ok(None); } } - _ => { - return Ok(Val::Null); - } + _ => return Ok(None), }, } } - Ok(current_element.clone()) + + Ok(Some(¤t_element)) } #[cfg(test)] diff --git a/core/translate/expr.rs b/core/translate/expr.rs index 4c4ee72b2..e5abbc233 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -789,7 +789,7 @@ pub fn translate_expr( }); Ok(target_register) } - JsonFunc::JsonArrayLength => { + JsonFunc::JsonArrayLength | JsonFunc::JsonType => { let args = if let Some(args) = args { if args.len() > 2 { crate::bail_parse_error!( @@ -837,7 +837,7 @@ pub fn translate_expr( ScalarFunc::Changes => { if let Some(_) = args { crate::bail_parse_error!( - "{} fucntion with more than 0 arguments", + "{} function with more than 0 arguments", srf ); } diff --git a/core/vdbe/mod.rs b/core/vdbe/mod.rs index c9151e934..73557303e 100644 --- a/core/vdbe/mod.rs +++ b/core/vdbe/mod.rs @@ -41,7 +41,7 @@ use crate::vdbe::insn::Insn; #[cfg(feature = "json")] use crate::{ function::JsonFunc, json::get_json, json::json_array, json::json_array_length, - json::json_extract, + json::json_extract, json::json_type, }; use crate::{Connection, Result, Rows, TransactionState, DATABASE_VERSION}; use datetime::{exec_date, exec_datetime_full, exec_julianday, exec_time, exec_unixepoch}; @@ -1381,17 +1381,25 @@ impl Program { } } #[cfg(feature = "json")] - crate::function::Func::Json(JsonFunc::JsonArrayLength) => { + crate::function::Func::Json( + func @ (JsonFunc::JsonArrayLength | JsonFunc::JsonType), + ) => { let json_value = &state.registers[*start_reg]; let path_value = if arg_count > 1 { Some(&state.registers[*start_reg + 1]) } else { None }; - let json_array_length = json_array_length(json_value, path_value); + let func_result = match func { + JsonFunc::JsonArrayLength => { + json_array_length(json_value, path_value) + } + JsonFunc::JsonType => json_type(json_value, path_value), + _ => unreachable!(), + }; - match json_array_length { - Ok(length) => state.registers[*dest] = length, + match func_result { + Ok(result) => state.registers[*dest] = result, Err(e) => return Err(e), } } diff --git a/testing/json.test b/testing/json.test index 5340f7049..fd2cdb54c 100755 --- a/testing/json.test +++ b/testing/json.test @@ -221,3 +221,51 @@ do_execsql_test json_array_length_via_bad_prop { do_execsql_test json_array_length_nested { SELECT json_array_length('{"one":[[1,2,3],2,3]}', '$.one[0]'); } {{3}} + +do_execsql_test json_type_no_path { + select json_type('{"a":[2,3.5,true,false,null,"x"]}') +} {{object}} + +do_execsql_test json_type_root_path { + select json_type('{"a":[2,3.5,true,false,null,"x"]}','$') +} {{object}} + +do_execsql_test json_type_array { + select json_type('{"a":[2,3.5,true,false,null,"x"]}','$.a') +} {{array}} + +do_execsql_test json_type_integer { + select json_type('{"a":[2,3.5,true,false,null,"x"]}','$.a[0]') +} {{integer}} + +do_execsql_test json_type_real { + select json_type('{"a":[2,3.5,true,false,null,"x"]}','$.a[1]') +} {{real}} + +do_execsql_test json_type_true { + select json_type('{"a":[2,3.5,true,false,null,"x"]}','$.a[2]') +} {{true}} + +do_execsql_test json_type_false { + select json_type('{"a":[2,3.5,true,false,null,"x"]}','$.a[3]') +} {{false}} + +do_execsql_test json_type_null { + select json_type('{"a":[2,3.5,true,false,null,"x"]}','$.a[4]') +} {{null}} + +do_execsql_test json_type_text { + select json_type('{"a":[2,3.5,true,false,null,"x"]}','$.a[5]') +} {{text}} + +do_execsql_test json_type_NULL { + select json_type('{"a":[2,3.5,true,false,null,"x"]}','$.a[6]') +} {{}} + +do_execsql_test json_type_cast { + select json_type(1) +} {{integer}} + +do_execsql_test json_type_null_arg { + select json_type(null) +} {{}}