Skip to content

Commit 1d1095e

Browse files
authored
feat: allow opt-out of PgHasArrayType with #[derive(sqlx::Type)] (#2619)
closes #2611
1 parent 3268698 commit 1d1095e

File tree

5 files changed

+101
-2
lines changed

5 files changed

+101
-2
lines changed

sqlx-core/src/types/mod.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ pub use json::{Json, JsonRawValue, JsonValue};
104104
///
105105
/// ### Transparent
106106
///
107-
/// Rust-only domain or wrappers around SQL types. The generated implementations directly delegate
107+
/// Rust-only domain wrappers around SQL types. The generated implementations directly delegate
108108
/// to the implementation of the inner type.
109109
///
110110
/// ```rust,ignore
@@ -113,6 +113,35 @@ pub use json::{Json, JsonRawValue, JsonValue};
113113
/// struct UserId(i64);
114114
/// ```
115115
///
116+
/// ##### Note: `PgHasArrayType`
117+
/// If you have the `postgres` feature enabled, this derive also generates a `PgHasArrayType` impl
118+
/// so that you may use it with `Vec` and other types that decode from an array in Postgres:
119+
///
120+
/// ```rust,ignore
121+
/// let user_ids: Vec<UserId> = sqlx::query_scalar("select '{ 123, 456 }'::int8[]")
122+
/// .fetch(&mut pg_connection)
123+
/// .await?;
124+
/// ```
125+
///
126+
/// However, if you are wrapping a type that does not implement `PgHasArrayType`
127+
/// (e.g. `Vec` itself, because we don't currently support multidimensional arrays),
128+
/// you may receive an error:
129+
///
130+
/// ```rust,ignore
131+
/// #[derive(sqlx::Type)] // ERROR: `Vec<i64>` does not implement `PgHasArrayType`
132+
/// #[sqlx(transparent)]
133+
/// struct UserIds(Vec<i64>);
134+
/// ```
135+
///
136+
/// To remedy this, add `#[sqlx(no_pg_array)]`, which disables the generation
137+
/// of the `PgHasArrayType` impl:
138+
///
139+
/// ```rust,ignore
140+
/// #[derive(sqlx::Type)]
141+
/// #[sqlx(transparent, no_pg_array)]
142+
/// struct UserIds(Vec<i64>);
143+
/// ```
144+
///
116145
/// ##### Attributes
117146
///
118147
/// * `#[sqlx(type_name = "<SQL type name>")]` on struct definition: instead of inferring the SQL
@@ -121,6 +150,7 @@ pub use json::{Json, JsonRawValue, JsonValue};
121150
/// given type is different than that of the inferred type (e.g. if you rename the above to
122151
/// `VARCHAR`). Affects Postgres only.
123152
/// * `#[sqlx(rename_all = "<strategy>")]` on struct definition: See [`derive docs in FromRow`](crate::from_row::FromRow#rename_all)
153+
/// * `#[sqlx(no_pg_array)]`: do not emit a `PgHasArrayType` impl (see above).
124154
///
125155
/// ### Enumeration
126156
///

sqlx-macros-core/src/derives/attributes.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ pub struct SqlxContainerAttributes {
5656
pub type_name: Option<TypeName>,
5757
pub rename_all: Option<RenameAll>,
5858
pub repr: Option<Ident>,
59+
pub no_pg_array: bool,
5960
}
6061

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

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

93+
Meta::Path(p) if p.is_ident("no_pg_array") => {
94+
try_set!(no_pg_array, true, value);
95+
}
96+
9197
Meta::NameValue(MetaNameValue {
9298
path,
9399
lit: Lit::Str(val),
@@ -148,6 +154,7 @@ pub fn parse_container_attributes(input: &[Attribute]) -> syn::Result<SqlxContai
148154
repr,
149155
type_name,
150156
rename_all,
157+
no_pg_array: no_pg_array.unwrap_or(false),
151158
})
152159
}
153160

@@ -229,6 +236,12 @@ pub fn check_enum_attributes(input: &DeriveInput) -> syn::Result<SqlxContainerAt
229236
input
230237
);
231238

239+
assert_attribute!(
240+
!attributes.no_pg_array,
241+
"unused #[sqlx(no_pg_array)]; derive does not emit `PgHasArrayType` impls for enums",
242+
input
243+
);
244+
232245
Ok(attributes)
233246
}
234247

@@ -288,6 +301,12 @@ pub fn check_struct_attributes<'a>(
288301
input
289302
);
290303

304+
assert_attribute!(
305+
!attributes.no_pg_array,
306+
"unused #[sqlx(no_pg_array)]; derive does not emit `PgHasArrayType` impls for custom structs",
307+
input
308+
);
309+
291310
assert_attribute!(attributes.repr.is_none(), "unexpected #[repr(..)]", input);
292311

293312
for field in fields {

sqlx-macros-core/src/derives/type.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ fn expand_derive_has_sql_type_transparent(
9090
}
9191
);
9292

93-
if cfg!(feature = "postgres") {
93+
if cfg!(feature = "postgres") && !attr.no_pg_array {
9494
tokens.extend(quote!(
9595
#[automatically_derived]
9696
impl #array_impl_generics ::sqlx::postgres::PgHasArrayType for #ident #ty_generics

sqlx-postgres/src/types/array.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,44 @@ use crate::types::Oid;
99
use crate::types::Type;
1010
use crate::{PgArgumentBuffer, PgTypeInfo, PgValueFormat, PgValueRef, Postgres};
1111

12+
/// Provides information necessary to encode and decode Postgres arrays as compatible Rust types.
13+
///
14+
/// Implementing this trait for some type `T` enables relevant `Type`,`Encode` and `Decode` impls
15+
/// for `Vec<T>`, `&[T]` (slices), `[T; N]` (arrays), etc.
16+
///
17+
/// ### Note: `#[derive(sqlx::Type)]`
18+
/// If you have the `postgres` feature enabled, `#[derive(sqlx::Type)]` will also generate
19+
/// an impl of this trait for your type if your wrapper is marked `#[sqlx(transparent)]`:
20+
///
21+
/// ```rust,ignore
22+
/// #[derive(sqlx::Type)]
23+
/// #[sqlx(transparent)]
24+
/// struct UserId(i64);
25+
///
26+
/// let user_ids: Vec<UserId> = sqlx::query_scalar("select '{ 123, 456 }'::int8[]")
27+
/// .fetch(&mut pg_connection)
28+
/// .await?;
29+
/// ```
30+
///
31+
/// However, this may cause an error if the type being wrapped does not implement `PgHasArrayType`,
32+
/// e.g. `Vec` itself, because we don't currently support multidimensional arrays:
33+
///
34+
/// ```rust,ignore
35+
/// #[derive(sqlx::Type)] // ERROR: `Vec<i64>` does not implement `PgHasArrayType`
36+
/// #[sqlx(transparent)]
37+
/// struct UserIds(Vec<i64>);
38+
/// ```
39+
///
40+
/// To remedy this, add `#[sqlx(no_pg_array)]`, which disables the generation
41+
/// of the `PgHasArrayType` impl:
42+
///
43+
/// ```rust,ignore
44+
/// #[derive(sqlx::Type)]
45+
/// #[sqlx(transparent, no_pg_array)]
46+
/// struct UserIds(Vec<i64>);
47+
/// ```
48+
///
49+
/// See [the documentation of `Type`][Type] for more details.
1250
pub trait PgHasArrayType {
1351
fn array_type_info() -> PgTypeInfo;
1452
fn array_compatible(ty: &PgTypeInfo) -> bool {

tests/postgres/derives.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ use std::ops::Bound;
1010
#[sqlx(transparent)]
1111
struct Transparent(i32);
1212

13+
#[derive(PartialEq, Debug, sqlx::Type)]
14+
// https://github.com/launchbadge/sqlx/issues/2611
15+
// Previously, the derive would generate a `PgHasArrayType` impl that errored on an
16+
// impossible-to-satisfy `where` bound. This attribute allows the user to opt-out.
17+
#[sqlx(transparent, no_pg_array)]
18+
struct TransparentArray(Vec<i64>);
19+
1320
#[sqlx_macros::test]
1421
async fn test_transparent_slice_to_array() -> anyhow::Result<()> {
1522
let mut conn = new::<Postgres>().await?;
@@ -139,6 +146,11 @@ test_type!(transparent<Transparent>(Postgres,
139146
"23523" == Transparent(23523)
140147
));
141148

149+
test_type!(transparent_array<TransparentArray>(Postgres,
150+
"'{}'::int8[]" == TransparentArray(vec![]),
151+
"'{ 23523, 123456, 789 }'::int8[]" == TransparentArray(vec![23523, 123456, 789])
152+
));
153+
142154
test_type!(weak_enum<Weak>(Postgres,
143155
"0::int4" == Weak::One,
144156
"2::int4" == Weak::Two,

0 commit comments

Comments
 (0)