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

impl pg array append #4138

Merged
merged 8 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
12 changes: 9 additions & 3 deletions diesel/src/pg/expression/expression_methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2730,10 +2730,16 @@ pub(in crate::pg) mod private {
message = "`{Self}` is neither `diesel::sql_types::Array<_>` nor `diesel::sql_types::Nullable<Array<_>>`",
note = "try to provide an expression that produces one of the expected sql types"
)]
pub trait ArrayOrNullableArray {}
pub trait ArrayOrNullableArray {
type Inner;
}

impl<T> ArrayOrNullableArray for Array<T> {}
impl<T> ArrayOrNullableArray for Nullable<Array<T>> {}
impl<T> ArrayOrNullableArray for Array<T> {
type Inner = T;
}
impl<T> ArrayOrNullableArray for Nullable<Array<T>> {
type Inner = T;
}

/// Marker trait used to implement `PgNetExpressionMethods` on the appropriate types.
#[diagnostic::on_unimplemented(
Expand Down
26 changes: 26 additions & 0 deletions diesel/src/pg/expression/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use super::expression_methods::InetOrCidr;
use super::expression_methods::RangeHelper;
use crate::expression::functions::define_sql_function;
use crate::pg::expression::expression_methods::ArrayOrNullableArray;
use crate::sql_types::*;

define_sql_function! {
Expand Down Expand Up @@ -687,3 +688,28 @@ define_sql_function! {
#[cfg(feature = "postgres_backend")]
fn daterange(lower: Nullable<Date>, upper: Nullable<Date>, bound: RangeBoundEnum) -> Daterange;
}

#[cfg(feature = "postgres_backend")]
define_sql_function! {
/// Append an element to the end of an array.
/// # Example
///
/// ```rust
/// # include!("../../doctest_setup.rs");
/// #
/// # fn main() {
/// # run_test().unwrap();
/// # }
/// #
/// # fn run_test() -> QueryResult<()> {
/// # use diesel::dsl::array_append;
/// # use diesel::sql_types::{Integer, Array};
/// # let connection = &mut establish_connection();
/// let ints = diesel::select(array_append::<Integer, Array<_>, _, _>(vec![1, 2], 3))
/// .get_result::<Vec<i32>>(connection)?;
/// assert_eq!(vec![1, 2, 3], ints);
/// # Ok(())
/// # }
/// ```
fn array_append<T: SingleValue, Arr: ArrayOrNullableArray<Inner=T> + SingleValue>(a: Arr, e: T) -> Array<T>;
Copy link
Member

@Ten0 Ten0 Aug 1, 2024

Choose a reason for hiding this comment

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

We should have the following two properties (unless I'm mistaken on how postgresql behaves)

  1. The SQL type of the resulting expression is Nullable depending on whether Arr is Nullable (assuming postgres returns NULL if passing a NULL array)
  2. The SQL type of the resulting expression is not Nullable depending on whether T is Nullable (assuming that postgres would append a NULL to the array, not make the whole resulting expression NULL)

Can you add tests that showcase that both these things work as expected?

This should be doable by duplicating the existing test and explicitly specifying the types corresponding to each scenario.

Thanks 😊🙏

Copy link
Contributor Author

@guissalustiano guissalustiano Aug 1, 2024

Choose a reason for hiding this comment

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

Thanks for the review!

The pg behavior for this functions with NULL is listed bellow:

  1. SELECT array_append(ARRAY[1,2], 3) returns ARRAY[1,2,3]
  2. SELECT array_append(ARRAY[1,2], NULL) returns ARRAY[1, 2, NULL]
  3. SELECT array_append(NULL, 3) returns ARRAY[3]
  4. SELECT array_append(NULL, NULL) returns ARRAY[NULL]
  1. The SQL type of the resulting expression is Nullable depending on whether Arr is Nullable (assuming postgres returns NULL if passing a NULL array)

If passing NULL, postgres apparently handles it as an empty array, so I can't think of a way that this function returns NULL.

  1. The SQL type of the resulting expression is not Nullable depending on whether T is Nullable (assuming that postgres would append a NULL to the array, not make the whole resulting expression NULL)

I see, thanks for the clarification. Can you help me on how to do that? That's why I did with Nullable<T> before, I don't know how to express this with types.

Can you add tests that showcase that both these things work as expected?

Sure! Should I create doc tests or test in diesel_test?

Thanks!

Copy link
Member

@Ten0 Ten0 Aug 2, 2024

Choose a reason for hiding this comment

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

Ah! Thanks for investigating the behavior! 😊

I thought that we may have to involve the nullability propagation helpers traits here to construct the return type, which would maybe have involved either declaring it without the macro or adding a feature to do so in the macro, however given what you're saying (3 and 4) it seems that we only ever want to return an array, regardless of original nullability of the array, so it seems that the current signature is correct. 😊
(Which is furtunate because that turns out to be significantly simpler.)

It's fine as a first version if a user needs the input array to already have its elements be Nullable to be able to append potentially null values to it. That may be incomplete (although I doubt there would be many use cases), but most importantly it's correct 😊.
What wouldn't have been fine is if it could return nulls but that wasn't expressed in the signature (1), or if by fixing (1) we accidentally made (2) wrong (by considering the arguments in the same way, in a mode where if any argument is Nullable resulting value is Nullable, which IIRC is already our default behavior for operators but apparently not for functions - what I meant is in that case we couldn't just "enable" that behavior for functions as well).

OK so overall, current implementation looks good, we can probably just add tests to make sure that we get the correct type for all 4 scenarios, with somewhat explicit sql types. 🙂

I have no strong opinion on where each test for the 4 scenarios you mentioned should live, but maybe another reviewer will. By default unless too verbose I'd probably just have them all in the one doctest.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks! I added the test as a docstring

}
2 changes: 1 addition & 1 deletion diesel/src/serialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ where
/// match *self {
/// // these string values need to match the labels used in your
/// // enum definition in SQL. So this expects that you defined the
/// /// relevat enum type as`ENUM('one', 'two')` in your `CREATE TABLE` statement
/// /// relevant enum type as`ENUM('one', 'two')` in your `CREATE TABLE` statement
/// Post::FirstValue => out.write_all(b"one")?,
/// Post::SecondValue => out.write_all(b"two")?,
/// }
Expand Down
Loading