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

feat: allow opt-out of PgHasArrayType with #[derive(sqlx::Type)] #2619

Merged
merged 1 commit into from
Jul 14, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
32 changes: 31 additions & 1 deletion sqlx-core/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ pub use json::{Json, JsonRawValue, JsonValue};
///
/// ### Transparent
///
/// Rust-only domain or wrappers around SQL types. The generated implementations directly delegate
/// Rust-only domain wrappers around SQL types. The generated implementations directly delegate
/// to the implementation of the inner type.
///
/// ```rust,ignore
Expand All @@ -113,6 +113,35 @@ pub use json::{Json, JsonRawValue, JsonValue};
/// struct UserId(i64);
/// ```
///
/// ##### Note: `PgHasArrayType`
/// If you have the `postgres` feature enabled, this derive also generates a `PgHasArrayType` impl
/// so that you may use it with `Vec` and other types that decode from an array in Postgres:
///
/// ```rust,ignore
/// let user_ids: Vec<UserId> = sqlx::query_scalar("select '{ 123, 456 }'::int8[]")
/// .fetch(&mut pg_connection)
/// .await?;
/// ```
///
/// However, if you are wrapping a type that does not implement `PgHasArrayType`
/// (e.g. `Vec` itself, because we don't currently support multidimensional arrays),
/// you may receive an error:
///
/// ```rust,ignore
/// #[derive(sqlx::Type)] // ERROR: `Vec<i64>` does not implement `PgHasArrayType`
/// #[sqlx(transparent)]
/// struct UserIds(Vec<i64>);
/// ```
///
/// To remedy this, add `#[sqlx(no_pg_array)]`, which disables the generation
/// of the `PgHasArrayType` impl:
///
/// ```rust,ignore
/// #[derive(sqlx::Type)]
/// #[sqlx(transparent, no_pg_array)]
/// struct UserIds(Vec<i64>);
/// ```
///
/// ##### Attributes
///
/// * `#[sqlx(type_name = "<SQL type name>")]` on struct definition: instead of inferring the SQL
Expand All @@ -121,6 +150,7 @@ pub use json::{Json, JsonRawValue, JsonValue};
/// given type is different than that of the inferred type (e.g. if you rename the above to
/// `VARCHAR`). Affects Postgres only.
/// * `#[sqlx(rename_all = "<strategy>")]` on struct definition: See [`derive docs in FromRow`](crate::from_row::FromRow#rename_all)
/// * `#[sqlx(no_pg_array)]`: do not emit a `PgHasArrayType` impl (see above).
///
/// ### Enumeration
///
Expand Down
19 changes: 19 additions & 0 deletions sqlx-macros-core/src/derives/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ pub struct SqlxContainerAttributes {
pub type_name: Option<TypeName>,
pub rename_all: Option<RenameAll>,
pub repr: Option<Ident>,
pub no_pg_array: bool,
}

pub struct SqlxChildAttributes {
Expand All @@ -71,6 +72,7 @@ pub fn parse_container_attributes(input: &[Attribute]) -> syn::Result<SqlxContai
let mut repr = None;
let mut type_name = None;
let mut rename_all = None;
let mut no_pg_array = None;

for attr in input
.iter()
Expand All @@ -88,6 +90,10 @@ pub fn parse_container_attributes(input: &[Attribute]) -> syn::Result<SqlxContai
try_set!(transparent, true, value)
}

Meta::Path(p) if p.is_ident("no_pg_array") => {
try_set!(no_pg_array, true, value);
}

Meta::NameValue(MetaNameValue {
path,
lit: Lit::Str(val),
Expand Down Expand Up @@ -148,6 +154,7 @@ pub fn parse_container_attributes(input: &[Attribute]) -> syn::Result<SqlxContai
repr,
type_name,
rename_all,
no_pg_array: no_pg_array.unwrap_or(false),
})
}

Expand Down Expand Up @@ -229,6 +236,12 @@ pub fn check_enum_attributes(input: &DeriveInput) -> syn::Result<SqlxContainerAt
input
);

assert_attribute!(
!attributes.no_pg_array,
"unused #[sqlx(no_pg_array)]; derive does not emit `PgHasArrayType` impls for enums",
input
);

Ok(attributes)
}

Expand Down Expand Up @@ -288,6 +301,12 @@ pub fn check_struct_attributes<'a>(
input
);

assert_attribute!(
!attributes.no_pg_array,
"unused #[sqlx(no_pg_array)]; derive does not emit `PgHasArrayType` impls for custom structs",
input
);

assert_attribute!(attributes.repr.is_none(), "unexpected #[repr(..)]", input);

for field in fields {
Expand Down
2 changes: 1 addition & 1 deletion sqlx-macros-core/src/derives/type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ fn expand_derive_has_sql_type_transparent(
}
);

if cfg!(feature = "postgres") {
if cfg!(feature = "postgres") && !attr.no_pg_array {
tokens.extend(quote!(
#[automatically_derived]
impl #array_impl_generics ::sqlx::postgres::PgHasArrayType for #ident #ty_generics
Expand Down
38 changes: 38 additions & 0 deletions sqlx-postgres/src/types/array.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,44 @@ use crate::types::Oid;
use crate::types::Type;
use crate::{PgArgumentBuffer, PgTypeInfo, PgValueFormat, PgValueRef, Postgres};

/// Provides information necessary to encode and decode Postgres arrays as compatible Rust types.
///
/// Implementing this trait for some type `T` enables relevant `Type`,`Encode` and `Decode` impls
/// for `Vec<T>`, `&[T]` (slices), `[T; N]` (arrays), etc.
///
/// ### Note: `#[derive(sqlx::Type)]`
/// If you have the `postgres` feature enabled, `#[derive(sqlx::Type)]` will also generate
/// an impl of this trait for your type if your wrapper is marked `#[sqlx(transparent)]`:
///
/// ```rust,ignore
/// #[derive(sqlx::Type)]
/// #[sqlx(transparent)]
/// struct UserId(i64);
///
/// let user_ids: Vec<UserId> = sqlx::query_scalar("select '{ 123, 456 }'::int8[]")
/// .fetch(&mut pg_connection)
/// .await?;
/// ```
///
/// However, this may cause an error if the type being wrapped does not implement `PgHasArrayType`,
/// e.g. `Vec` itself, because we don't currently support multidimensional arrays:
///
/// ```rust,ignore
/// #[derive(sqlx::Type)] // ERROR: `Vec<i64>` does not implement `PgHasArrayType`
/// #[sqlx(transparent)]
/// struct UserIds(Vec<i64>);
/// ```
///
/// To remedy this, add `#[sqlx(no_pg_array)]`, which disables the generation
/// of the `PgHasArrayType` impl:
///
/// ```rust,ignore
/// #[derive(sqlx::Type)]
/// #[sqlx(transparent, no_pg_array)]
/// struct UserIds(Vec<i64>);
/// ```
///
/// See [the documentation of `Type`][Type] for more details.
pub trait PgHasArrayType {
fn array_type_info() -> PgTypeInfo;
fn array_compatible(ty: &PgTypeInfo) -> bool {
Expand Down
12 changes: 12 additions & 0 deletions tests/postgres/derives.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ use std::ops::Bound;
#[sqlx(transparent)]
struct Transparent(i32);

#[derive(PartialEq, Debug, sqlx::Type)]
// https://github.com/launchbadge/sqlx/issues/2611
// Previously, the derive would generate a `PgHasArrayType` impl that errored on an
// impossible-to-satisfy `where` bound. This attribute allows the user to opt-out.
#[sqlx(transparent, no_pg_array)]
struct TransparentArray(Vec<i64>);

#[sqlx_macros::test]
async fn test_transparent_slice_to_array() -> anyhow::Result<()> {
let mut conn = new::<Postgres>().await?;
Expand Down Expand Up @@ -139,6 +146,11 @@ test_type!(transparent<Transparent>(Postgres,
"23523" == Transparent(23523)
));

test_type!(transparent_array<TransparentArray>(Postgres,
"'{}'::int8[]" == TransparentArray(vec![]),
"'{ 23523, 123456, 789 }'::int8[]" == TransparentArray(vec![23523, 123456, 789])
));

test_type!(weak_enum<Weak>(Postgres,
"0::int4" == Weak::One,
"2::int4" == Weak::Two,
Expand Down
Loading