use rupg_common::{Error, Result, SqlState};
use rupg_types::{JsonItem, JsonPathOp, JsonTree, Value, json_escape, jsonb_in};
use crate::json::{invalid, text, tree};
use crate::{Call, Kernel, bad_value, type_error};
pub(crate) fn by_src(src: &str) -> Option<Kernel> {
Some(match src {
"jsonb_concat" => jsonb_concat,
"jsonb_delete" => jsonb_delete,
"jsonb_delete_array" => jsonb_delete_array,
"jsonb_delete_idx" => jsonb_delete_idx,
"jsonb_delete_path" => jsonb_delete_path,
"jsonb_set" => jsonb_set,
"jsonb_set_lax" => jsonb_set_lax,
"jsonb_insert" => jsonb_insert,
_ => return None,
})
}
fn finish(text: &str) -> Result<Value> {
jsonb_in(text).map(Value::Text).map_err(type_error)
}
fn same(tree: &JsonTree<'_>) -> Value {
Value::text(tree.source())
}
fn is_scalar(tree: &JsonTree<'_>) -> bool {
!matches!(tree.item(0), JsonItem::Array(_) | JsonItem::Object(_))
}
fn is_object(tree: &JsonTree<'_>) -> bool {
matches!(tree.item(0), JsonItem::Object(_))
}
fn root_count(tree: &JsonTree<'_>) -> usize {
match tree.item(0) {
JsonItem::Array(items) => items.len(),
JsonItem::Object(fields) => fields.len(),
_ => 1,
}
}
fn comma(out: &mut String) {
if !out.ends_with(['[', '{']) {
out.push_str(", ");
}
}
fn put_fields(out: &mut String, tree: &JsonTree<'_>, keep: impl Fn(&str) -> bool) {
if let JsonItem::Object(fields) = tree.item(0) {
for (name, at) in fields.iter().filter(|f| keep(&f.0)) {
comma(out);
json_escape(name, out);
out.push_str(": ");
out.push_str(tree.text(*at));
}
}
}
fn put_parts(out: &mut String, tree: &JsonTree<'_>) {
match tree.item(0) {
JsonItem::Array(items) => {
for &at in items {
comma(out);
out.push_str(tree.text(at));
}
}
_ => {
comma(out);
out.push_str(tree.source());
}
}
}
fn jsonb_concat(_: &Call<'_>, args: &[Value]) -> Result<Value> {
let (left, right) = (tree(args, 0)?, tree(args, 1)?);
if is_object(&left) == is_object(&right) {
if root_count(&left) == 0 && !is_scalar(&right) {
return Ok(same(&right));
}
if root_count(&right) == 0 && !is_scalar(&left) {
return Ok(same(&left));
}
}
let mut out = String::with_capacity(left.source().len() + right.source().len() + 4);
if is_object(&left) && is_object(&right) {
out.push('{');
put_fields(&mut out, &left, |_| true);
put_fields(&mut out, &right, |_| true);
out.push('}');
} else {
out.push('[');
put_parts(&mut out, &left);
put_parts(&mut out, &right);
out.push(']');
}
finish(&out)
}
fn delete_keys(tree: &JsonTree<'_>, keys: &[&str]) -> Result<Value> {
let mut out = String::with_capacity(tree.source().len());
match tree.item(0) {
JsonItem::Object(_) => {
out.push('{');
put_fields(&mut out, tree, |name| !keys.contains(&name));
out.push('}');
}
JsonItem::Array(items) => {
out.push('[');
for &at in items {
if matches!(tree.item(at), JsonItem::String(s) if keys.contains(&s.as_str())) {
continue;
}
comma(&mut out);
out.push_str(tree.text(at));
}
out.push(']');
}
_ => return Ok(same(tree)),
}
finish(&out)
}
fn jsonb_delete(_: &Call<'_>, args: &[Value]) -> Result<Value> {
let tree = tree(args, 0)?;
if is_scalar(&tree) {
return Err(invalid("cannot delete from scalar"));
}
if root_count(&tree) == 0 {
return Ok(same(&tree));
}
delete_keys(&tree, &[text(args, 1)?])
}
fn subscripts() -> Error {
Error::new(SqlState::ARRAY_SUBSCRIPT_ERROR, "wrong number of array subscripts")
}
fn jsonb_delete_array(_: &Call<'_>, args: &[Value]) -> Result<Value> {
let Some(Value::Array(array)) = args.get(1) else { return Err(bad_value()) };
if array.dims.len() > 1 {
return Err(subscripts());
}
let tree = tree(args, 0)?;
if is_scalar(&tree) {
return Err(invalid("cannot delete from scalar"));
}
let keys: Vec<&str> = array
.values
.iter()
.flatten()
.map(|v| v.as_str().ok_or_else(bad_value))
.collect::<Result<_>>()?;
if root_count(&tree) == 0 || keys.is_empty() {
return Ok(same(&tree));
}
delete_keys(&tree, &keys)
}
fn jsonb_delete_idx(_: &Call<'_>, args: &[Value]) -> Result<Value> {
let tree = tree(args, 0)?;
let index = args.get(1).and_then(Value::as_i64).ok_or_else(bad_value)?;
let items = match tree.item(0) {
JsonItem::Array(items) => items,
JsonItem::Object(_) => {
return Err(invalid("cannot delete from object using integer index"));
}
_ => return Err(invalid("cannot delete from scalar")),
};
let count = items.len() as i64;
let index = if index < 0 && -index <= count { index + count } else { index };
if index < 0 || index >= count {
return Ok(same(&tree));
}
let mut out = String::with_capacity(tree.source().len());
out.push('[');
for (i, &at) in items.iter().enumerate() {
if i as i64 != index {
comma(&mut out);
out.push_str(tree.text(at));
}
}
out.push(']');
finish(&out)
}
fn path(args: &[Value], at: usize) -> Result<Vec<Option<&str>>> {
let Some(Value::Array(array)) = args.get(at) else { return Err(bad_value()) };
if array.dims.len() > 1 {
return Err(subscripts());
}
array
.values
.iter()
.map(|v| match v {
Some(v) => v.as_str().map(Some).ok_or_else(bad_value),
None => Ok(None),
})
.collect()
}
fn set_path(
tree: &JsonTree<'_>,
path: &[Option<&str>],
new: &str,
op: JsonPathOp,
) -> Result<Value> {
finish(&tree.set_path(path, new, op).map_err(type_error)?)
}
fn set(args: &[Value], new: &str) -> Result<Value> {
let path = path(args, 1)?;
let create = args.get(3).and_then(Value::as_bool).unwrap_or(true);
let tree = tree(args, 0)?;
if is_scalar(&tree) {
return Err(invalid("cannot set path in scalar"));
}
if (root_count(&tree) == 0 && !create) || path.is_empty() {
return Ok(same(&tree));
}
set_path(&tree, &path, new, if create { JsonPathOp::Create } else { JsonPathOp::Replace })
}
fn jsonb_set(_: &Call<'_>, args: &[Value]) -> Result<Value> {
set(args, text(args, 2)?)
}
fn treatment_error() -> Error {
invalid(
"null_value_treatment must be \"delete_key\", \"return_target\", \"use_json_null\", or \"raise_exception\"",
)
}
fn jsonb_set_lax(call: &Call<'_>, args: &[Value]) -> Result<Value> {
if [0, 1, 3].iter().any(|&at| matches!(args.get(at), None | Some(Value::Null))) {
return Ok(Value::Null);
}
let Some(treatment) = args.get(4).and_then(Value::as_str) else {
return Err(treatment_error());
};
if !matches!(args.get(2), None | Some(Value::Null)) {
return jsonb_set(call, args);
}
match treatment {
"raise_exception" => Err(Error::new(SqlState::NULL_VALUE_NOT_ALLOWED, "JSON value must not be null")
.with_detail("Exception was raised because null_value_treatment is \"raise_exception\".")
.with_hint("To avoid, either change the null_value_treatment argument or ensure that an SQL NULL is not passed.")),
"use_json_null" => set(args, "null"),
"delete_key" => jsonb_delete_path(call, args),
"return_target" => Ok(args[0].clone()),
_ => Err(treatment_error()),
}
}
fn jsonb_delete_path(_: &Call<'_>, args: &[Value]) -> Result<Value> {
let path = path(args, 1)?;
let tree = tree(args, 0)?;
if is_scalar(&tree) {
return Err(invalid("cannot delete path in scalar"));
}
if root_count(&tree) == 0 || path.is_empty() {
return Ok(same(&tree));
}
set_path(&tree, &path, "", JsonPathOp::Delete)
}
fn jsonb_insert(_: &Call<'_>, args: &[Value]) -> Result<Value> {
let path = path(args, 1)?;
let after = args.get(3).and_then(Value::as_bool).unwrap_or(false);
let tree = tree(args, 0)?;
if is_scalar(&tree) {
return Err(invalid("cannot set path in scalar"));
}
if path.is_empty() {
return Ok(same(&tree));
}
let op = if after { JsonPathOp::InsertAfter } else { JsonPathOp::InsertBefore };
set_path(&tree, &path, text(args, 2)?, op)
}