Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for postgre jsonb_set_lax #4396

Merged
merged 3 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions diesel/src/pg/expression/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use crate::pg::expression::expression_methods::MultirangeOrRangeMaybeNullable;
use crate::pg::expression::expression_methods::RangeOrNullableRange;
use crate::pg::expression::expression_methods::RecordOrNullableRecord;
use crate::pg::expression::expression_methods::TextArrayOrNullableTextArray;
use crate::pg::expression::expression_methods::TextOrNullableText;
use crate::sql_types::*;

define_sql_function! {
Expand Down Expand Up @@ -2578,3 +2579,101 @@ define_sql_function! {
Arr: TextArrayOrNullableTextArray + CombinedNullableValue<E,Jsonb>
>(base: E, path: Arr, new_value: E, create_if_missing: Bool) -> Arr::Out;
}

#[cfg(feature = "postgres_backend")]
define_sql_function! {
/// Returns target with the item designated by path replaced by new_value,
/// or with new_value added and the item designated by path does not exist.
///
/// It can't set path in scalar
///
/// All earlier steps in the path must exist, or the target is returned unchanged.
/// As with the path oriented operators, negative integers that appear in the path count from the end of JSON arrays.
/// If the last path step is an array index that is out of range,
/// the new value is added at the beginning of the array if the index is negative,
/// or at the end of the array if it is positive.
///
/// If new_value is not NULL, behaves identically to jsonb_set.
/// Otherwise behaves according to the value of null_value_treatment
/// which must be one of 'raise_exception', 'use_json_null', 'delete_key', or 'return_target'.
/// The default is 'use_json_null'.
Comment on lines +2595 to +2598
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rather prefer having an enum with all allowed variants instead of allowing a text input here.

That would require something like this:

/// This is a wrapper for [`RangeBound`] to represent range bounds: '[]', '(]', '[)', '()',
/// used in functions int4range, int8range, numrange, tsrange, tstzrange, daterange.
#[derive(Debug, Clone, Copy, QueryId, SqlType)]
#[cfg(feature = "postgres_backend")]
#[diesel(postgres_type(name = "text"))]
pub struct RangeBoundEnum;
/// Represent postgres range bounds: '[]', '(]', '[)', '()',
/// used in functions int4range, int8range, numrange, tsrange, tstzrange, daterange.
#[derive(Debug, Clone, Copy, diesel_derives::AsExpression)]
#[diesel(sql_type = RangeBoundEnum)]
#[allow(clippy::enum_variant_names)]
pub enum RangeBound {
/// postgres '[]'
LowerBoundInclusiveUpperBoundInclusive,
/// postgres '[)'
LowerBoundInclusiveUpperBoundExclusive,
/// postgres '(]'
LowerBoundExclusiveUpperBoundInclusive,
/// postgres '()'
LowerBoundExclusiveUpperBoundExclusive,
}

and

#[cfg(feature = "postgres_backend")]
impl ToSql<RangeBoundEnum, Pg> for RangeBound {
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> serialize::Result {
let literal = match self {
Self::LowerBoundInclusiveUpperBoundInclusive => "[]",
Self::LowerBoundInclusiveUpperBoundExclusive => "[)",
Self::LowerBoundExclusiveUpperBoundInclusive => "(]",
Self::LowerBoundExclusiveUpperBoundExclusive => "()",
};
out.write_all(literal.as_bytes())
.map(|_| IsNull::No)
.map_err(|e| Box::new(e) as Box<dyn Error + Send + Sync>)
}
}

with the corresponding enum variants.

Afterwards the function signature needs to change so that it accepts only the SQL type (RangeBoundEnum in that example) instead of arbitrary text. (See here for an example usage:

fn int4range(lower: Nullable<Integer>, upper: Nullable<Integer>, bound: RangeBoundEnum) -> Int4range;
)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your kind instructions.
I impl to_sql in a new mod called json_function_enum, do you think it's ok?
I'm ready to make more changes if you think it's not fit to make a new mod for it.

Copy link
Member

@weiznich weiznich Dec 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it's good that way, thanks for implementing this change ❤️

///
/// # Example
///
/// ```rust
/// # include!("../../doctest_setup.rs");
/// #
/// # fn main() {
/// # #[cfg(feature = "serde_json")]
/// # run_test().unwrap();
/// # }
/// #
/// # #[cfg(feature = "serde_json")]
/// # fn run_test() -> QueryResult<()> {
/// # use diesel::dsl::jsonb_set_lax;
/// # use diesel::sql_types::{Jsonb,Array, Json, Nullable, Text};
/// # use serde_json::{json,Value};
/// # let connection = &mut establish_connection();
///
/// let null_value_treatment : String = "use_json_null".into();
/// let result = diesel::select(jsonb_set_lax::<Jsonb, Array<Text>, Text, _, _, _, _, _>(
/// json!([{"f1":1,"f2":null},2,null,3]),
/// vec!["0","f1"],
/// json!([2,3,4]),
/// true,
/// null_value_treatment
/// )).get_result::<Value>(connection)?;
/// let expected: Value = json!([{"f1": [2, 3, 4], "f2": null}, 2, null, 3]);
/// assert_eq!(result, expected);
///
/// let null_value_treatment : String = "return_target".into();
/// let result = diesel::select(jsonb_set_lax::<Nullable<Jsonb>, Array<Nullable<Text>>, Text, _, _, _, _, _>(
/// json!([{"f1":99,"f2":null},2]),
/// vec!["0","f3"],
/// None::<Value>,
/// true,
/// null_value_treatment
/// )).get_result::<Option<Value>>(connection)?;
/// assert_eq!(result, Some(json!([{"f1":99,"f2":null},2])));
///
/// let null_value_treatment : String = "use_json_null".into();
/// let empty:Vec<String> = Vec::new();
/// let result = diesel::select(jsonb_set_lax::<Jsonb, Array<Nullable<Text>>, Text, _, _, _, _, _>(
/// // cannot be json!(null)
/// json!([]),
/// empty,
/// json!(null),
/// true,
/// null_value_treatment
/// )).get_result::<Value>(connection)?;
/// let expected = json!([]);
/// assert_eq!(result, expected);
///
/// let null_value_treatment : String = "use_json_null".into();
/// let result = diesel::select(jsonb_set_lax::<Jsonb, Nullable<Array<Nullable<Text>>>, Text, _, _, _, _, _,>(
/// json!(null),
/// None::<Vec<String>>,
/// json!({"foo": 42}),
/// true,
/// null_value_treatment
/// )).get_result::<Option<Value>>(connection)?;
/// assert!(result.is_none());
///
/// let result = diesel::select(jsonb_set_lax::<Jsonb, Nullable<Array<Nullable<Text>>>, Nullable<Text>, _, _, _, _, _,>(
/// json!(null),
/// None::<Vec<String>>,
/// json!({"foo": 42}),
/// true,
/// None::<String>
/// )).get_result::<Option<Value>>(connection)?;
/// assert!(result.is_none());
///
/// # Ok(())
/// # }
/// ```
fn jsonb_set_lax<
E: JsonbOrNullableJsonb + SingleValue,
Arr: TextArrayOrNullableTextArray + CombinedNullableValue<E,Jsonb>,
T: TextOrNullableText + SingleValue,
>(base: E, path: Arr, new_value: E, create_if_missing: Bool, null_value_treatment: T) -> Arr::Out;
}
6 changes: 6 additions & 0 deletions diesel/src/pg/expression/helper_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -603,3 +603,9 @@ pub type jsonb_set<B, J, R> = super::functions::jsonb_set<SqlTypeOf<B>, SqlTypeO
#[cfg(feature = "postgres_backend")]
pub type jsonb_set_create_if_missing<B, J, R, C> =
super::functions::jsonb_set_create_if_missing<SqlTypeOf<B>, SqlTypeOf<J>, B, J, R, C>;

/// Return type of [`jsonb_set_lax(base, path, new_value, create_if_missing, null_value_treatment)`](super::functions::jsonb_set_lax())
#[allow(non_camel_case_types)]
#[cfg(feature = "postgres_backend")]
pub type jsonb_set_lax<B, J, R, C, E> =
super::functions::jsonb_set_lax<SqlTypeOf<B>, SqlTypeOf<J>, SqlTypeOf<E>, B, J, R, C, E>;
7 changes: 7 additions & 0 deletions diesel_derives/tests/auto_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,13 @@ fn postgres_functions() -> _ {
pg_extras::jsonb,
pg_extras::boolean,
),
jsonb_set_lax(
pg_extras::jsonb,
pg_extras::text_array,
pg_extras::jsonb,
pg_extras::boolean,
pg_extras::name,
),
)
}

Expand Down
Loading