Skip to content

Commit

Permalink
Merge 'Implement json_error_position' from Peter Sooley
Browse files Browse the repository at this point in the history
Furthering work for JSON compatibility as described in #127, this change
adds support for the `json_error_position` function used to find the
error in invalid JSON values.
![image](https://github.com/user-attachments/assets/d61403ef-
bccf-41b0-9128-199066608fac)

Closes #564
  • Loading branch information
penberg committed Jan 13, 2025
2 parents 0dceb02 + 1e94dbf commit 9d42a48
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 1 deletion.
2 changes: 1 addition & 1 deletion COMPAT.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ Feature support of [sqlite expr syntax](https://www.sqlite.org/lang_expr.html).
| jsonb_array(value1,value2,...) | | |
| json_array_length(json) | Yes | |
| json_array_length(json,path) | Yes | |
| json_error_position(json) | | |
| json_error_position(json) | Yes | |
| json_extract(json,path,...) | Partial | Does not fully support unicode literal syntax and does not allow numbers > 2^127 - 1 (which SQLite truncates to i32), does not support BLOBs |
| jsonb_extract(json,path,...) | | |
| json -> path | Yes | |
Expand Down
4 changes: 4 additions & 0 deletions core/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub enum JsonFunc {
JsonArrowShiftExtract,
JsonExtract,
JsonType,
JsonErrorPosition,
}

#[cfg(feature = "json")]
Expand All @@ -46,6 +47,7 @@ impl Display for JsonFunc {
Self::JsonArrowExtract => "->".to_string(),
Self::JsonArrowShiftExtract => "->>".to_string(),
Self::JsonType => "json_type".to_string(),
Self::JsonErrorPosition => "json_error_position".to_string(),
}
)
}
Expand Down Expand Up @@ -379,6 +381,8 @@ impl Func {
"json_extract" => Ok(Func::Json(JsonFunc::JsonExtract)),
#[cfg(feature = "json")]
"json_type" => Ok(Func::Json(JsonFunc::JsonType)),
#[cfg(feature = "json")]
"json_error_position" => Ok(Self::Json(JsonFunc::JsonErrorPosition)),
"unixepoch" => Ok(Self::Scalar(ScalarFunc::UnixEpoch)),
"julianday" => Ok(Self::Scalar(ScalarFunc::JulianDay)),
"hex" => Ok(Self::Scalar(ScalarFunc::Hex)),
Expand Down
84 changes: 84 additions & 0 deletions core/json/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ mod ser;
use std::rc::Rc;

pub use crate::json::de::from_str;
use crate::json::error::Error as JsonError;
use crate::json::json_path::{json_path, JsonPath, PathElement};
pub use crate::json::ser::to_string;
use crate::types::{LimboText, OwnedValue, TextSubtype};
use indexmap::IndexMap;
use jsonb::Error as JsonbError;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
Expand Down Expand Up @@ -370,6 +372,32 @@ fn json_extract_single<'a>(
Ok(Some(&current_element))
}

pub fn json_error_position(json: &OwnedValue) -> crate::Result<OwnedValue> {
match json {
OwnedValue::Text(t) => match crate::json::from_str::<Val>(&t.value) {
Ok(_) => Ok(OwnedValue::Integer(0)),
Err(JsonError::Message { location, .. }) => {
if let Some(loc) = location {
Ok(OwnedValue::Integer(loc.column as i64))
} else {
Err(crate::error::LimboError::InternalError(
"failed to determine json error position".into(),
))
}
}
},
OwnedValue::Blob(b) => match jsonb::from_slice(b) {
Ok(_) => Ok(OwnedValue::Integer(0)),
Err(JsonbError::Syntax(_, pos)) => Ok(OwnedValue::Integer(pos as i64)),
_ => Err(crate::error::LimboError::InternalError(
"failed to determine json error position".into(),
)),
},
OwnedValue::Null => Ok(OwnedValue::Null),
_ => Ok(OwnedValue::Integer(0)),
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -698,4 +726,60 @@ mod tests {
Err(e) => assert!(e.to_string().contains("JSON path error")),
}
}

#[test]
fn test_json_error_position_no_error() {
let input = OwnedValue::build_text(Rc::new("[1,2,3]".to_string()));
let result = json_error_position(&input).unwrap();
assert_eq!(result, OwnedValue::Integer(0));
}

#[test]
fn test_json_error_position_no_error_more() {
let input = OwnedValue::build_text(Rc::new(r#"{"a":55,"b":72 , }"#.to_string()));
let result = json_error_position(&input).unwrap();
assert_eq!(result, OwnedValue::Integer(0));
}

#[test]
fn test_json_error_position_object() {
let input = OwnedValue::build_text(Rc::new(r#"{"a":55,"b":72,,}"#.to_string()));
let result = json_error_position(&input).unwrap();
assert_eq!(result, OwnedValue::Integer(16));
}

#[test]
fn test_json_error_position_array() {
let input = OwnedValue::build_text(Rc::new(r#"["a",55,"b",72,,]"#.to_string()));
let result = json_error_position(&input).unwrap();
assert_eq!(result, OwnedValue::Integer(16));
}

#[test]
fn test_json_error_position_null() {
let input = OwnedValue::Null;
let result = json_error_position(&input).unwrap();
assert_eq!(result, OwnedValue::Null);
}

#[test]
fn test_json_error_position_integer() {
let input = OwnedValue::Integer(5);
let result = json_error_position(&input).unwrap();
assert_eq!(result, OwnedValue::Integer(0));
}

#[test]
fn test_json_error_position_float() {
let input = OwnedValue::Float(-5.5);
let result = json_error_position(&input).unwrap();
assert_eq!(result, OwnedValue::Integer(0));
}

#[test]
fn test_json_error_position_blob() {
let input = OwnedValue::Blob(Rc::new(r#"["a",55,"b",72,,]"#.as_bytes().to_owned()));
let result = json_error_position(&input).unwrap();
assert_eq!(result, OwnedValue::Integer(16));
}
}
25 changes: 25 additions & 0 deletions core/translate/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,31 @@ pub fn translate_expr(
func_ctx,
)
}
JsonFunc::JsonErrorPosition => {
let args = if let Some(args) = args {
if args.len() != 1 {
crate::bail_parse_error!(
"{} function with not exactly 1 argument",
j.to_string()
);
}
args
} else {
crate::bail_parse_error!(
"{} function with no arguments",
j.to_string()
);
};
let json_reg = program.alloc_register();
translate_expr(program, referenced_tables, &args[0], json_reg, resolver)?;
program.emit_insn(Insn::Function {
constant_mask: 0,
start_reg: json_reg,
dest: target_register,
func: func_ctx,
});
Ok(target_register)
}
},
Func::Scalar(srf) => {
match srf {
Expand Down
9 changes: 9 additions & 0 deletions core/vdbe/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,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_extract, json::json_type,
json::json_error_position,
};
use crate::{Connection, Result, Rows, TransactionState, DATABASE_VERSION};
use datetime::{exec_date, exec_datetime_full, exec_julianday, exec_time, exec_unixepoch};
Expand Down Expand Up @@ -1421,6 +1422,14 @@ impl Program {
Err(e) => return Err(e),
}
}
#[cfg(feature = "json")]
crate::function::Func::Json(JsonFunc::JsonErrorPosition) => {
let json_value = &state.registers[*start_reg];
match json_error_position(json_value) {
Ok(pos) => state.registers[*dest] = pos,
Err(e) => return Err(e),
}
}
crate::function::Func::Scalar(scalar_func) => match scalar_func {
ScalarFunc::Cast => {
assert!(arg_count == 2);
Expand Down
32 changes: 32 additions & 0 deletions testing/json.test
Original file line number Diff line number Diff line change
Expand Up @@ -474,3 +474,35 @@ do_execsql_test json_type_cast {
do_execsql_test json_type_null_arg {
select json_type(null)
} {{}}

do_execsql_test json_error_position_valid {
SELECT json_error_position('{"a":55,"b":72,}');
} {{0}}

do_execsql_test json_error_position_valid_ws {
SELECT json_error_position('{"a":55,"b":72 , }');
} {{0}}

do_execsql_test json_error_position_object {
SELECT json_error_position('{"a":55,"b":72,,}');
} {{16}}

do_execsql_test json_error_position_array_valid {
SELECT json_error_position('["a",55,"b",72,]');
} {{0}}

do_execsql_test json_error_position_array_valid_ws {
SELECT json_error_position('["a",55,"b",72 , ]');
} {{0}}

do_execsql_test json_error_position_array {
SELECT json_error_position('["a",55,"b",72,,]');
} {{16}}

do_execsql_test json_error_position_null {
SELECT 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}}

0 comments on commit 9d42a48

Please sign in to comment.