Skip to content
Open
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
18 changes: 18 additions & 0 deletions sqlx-core/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,24 @@ pub use bstr::{BStr, BString};
/// enum Color { Red, Green, Blue }
/// ```
///
/// For enums using the variant names, one variant may be marked with `#[sqlx(other)]` to act
/// as a catch-all when decoding. Any value that does not match another variant is captured in
/// this variant instead of returning an error. When encoding, the captured string is written
/// back verbatim, which the database may reject if it is not a valid value for the type.
/// This is not supported for `#[repr(..)]` enums.
///
/// ```rust,ignore
/// #[derive(sqlx::Type)]
/// #[sqlx(type_name = "color", rename_all = "lowercase")]
/// enum Color {
/// Red,
/// Green,
/// Blue,
/// #[sqlx(other)]
/// Unknown(String),
/// }
/// ```
///
/// ### Records
///
/// User-defined composite types are supported through deriving a `struct`.
Expand Down
52 changes: 48 additions & 4 deletions sqlx-macros-core/src/derives/attributes.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use proc_macro2::{Ident, Span, TokenStream};
use quote::quote_spanned;
use quote::{quote_spanned, ToTokens};
use syn::{
parenthesized, punctuated::Punctuated, token::Comma, Attribute, DeriveInput, Field, LitStr,
Meta, Token, Type, Variant,
parenthesized, punctuated::Punctuated, token::Comma, Attribute, DeriveInput, Field, Fields,
FieldsUnnamed, LitStr, Meta, Token, Type, Variant,
};

macro_rules! assert_attribute {
Expand Down Expand Up @@ -73,6 +73,7 @@ pub struct SqlxChildAttributes {
pub try_from: Option<Type>,
pub skip: bool,
pub json: Option<JsonAttribute>,
pub other: bool,
}

pub fn parse_container_attributes(input: &[Attribute]) -> syn::Result<SqlxContainerAttributes> {
Expand Down Expand Up @@ -150,6 +151,7 @@ pub fn parse_child_attributes(input: &[Attribute]) -> syn::Result<SqlxChildAttri
let mut flatten = false;
let mut skip: bool = false;
let mut json = None;
let mut other = false;

for attr in input.iter().filter(|a| a.path().is_ident("sqlx")) {
attr.parse_nested_meta(|meta| {
Expand Down Expand Up @@ -177,6 +179,8 @@ pub fn parse_child_attributes(input: &[Attribute]) -> syn::Result<SqlxChildAttri
} else {
json = Some(JsonAttribute::NonNullable);
}
} else if meta.path.is_ident("other") {
other = true;
}

Ok(())
Expand All @@ -197,6 +201,7 @@ pub fn parse_child_attributes(input: &[Attribute]) -> syn::Result<SqlxChildAttri
try_from,
skip,
json,
other,
})
}

Expand Down Expand Up @@ -257,19 +262,56 @@ pub fn check_weak_enum_attributes(
"unexpected #[sqlx(rename = ..)]",
variant
);

assert_attribute!(
!attributes.other,
"#[sqlx(other)] is not supported for repr enums yet",
variant
);
}

Ok(attributes)
}

pub fn check_strong_enum_attributes(
input: &DeriveInput,
_variants: &Punctuated<Variant, Comma>,
variants: &Punctuated<Variant, Comma>,
) -> syn::Result<SqlxContainerAttributes> {
let attributes = check_enum_attributes(input)?;

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

let mut found_other = false;
for variant in variants {
let attributes = parse_child_attributes(&variant.attrs)?;
if attributes.other {
if found_other {
fail!(variant, "#[sqlx(other)] may only be applied to one variant");
}
found_other = true;

assert_attribute!(
attributes.rename.is_none(),
"unexpected #[sqlx(rename = ..)]: the #[sqlx(other)] variant matches any value",
variant
);

match &variant.fields {
Fields::Unnamed(FieldsUnnamed { unnamed, .. }) if unnamed.len() == 1 => (),
_ => fail!(
{
if matches!(&variant.fields, &Fields::Unit) {
variant.into_token_stream()
} else {
variant.fields.clone().into_token_stream()
}
},
"#[sqlx(other)] requires exactly one unnamed field, e.g. `Other(String)`"
),
}
}
}

Ok(attributes)
}

Expand Down Expand Up @@ -301,6 +343,8 @@ pub fn check_struct_attributes(
"unexpected #[sqlx(rename = ..)]",
field
);

assert_attribute!(!attributes.other, "unexpected #[sqlx(other)]", field)
}

Ok(attributes)
Expand Down
73 changes: 50 additions & 23 deletions sqlx-macros-core/src/derives/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ use super::attributes::{
};
use super::rename_all;
use proc_macro2::TokenStream;
use quote::quote;
use quote::{quote, quote_spanned};
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::token::Comma;
use syn::{
parse_quote, Arm, Data, DataEnum, DataStruct, DeriveInput, Field, Fields, FieldsNamed, Stmt,
TypeParamBound, Variant,
parse_quote, Arm, Data, DataEnum, DataStruct, DeriveInput, Field, Fields, FieldsNamed,
FieldsUnnamed, Stmt, TypeParamBound, Variant,
};

pub fn expand_derive_decode(input: &DeriveInput) -> syn::Result<TokenStream> {
Expand Down Expand Up @@ -152,29 +153,46 @@ fn expand_derive_decode_strong_enum(

let ident = &input.ident;
let ident_s = ident.to_string();
let mut other_variant = None;

let value_arms = variants.iter().map(|v| -> Arm {
let id = &v.ident;
let attributes = parse_child_attributes(&v.attrs).unwrap();
let value_arms = variants
.iter()
.map(|v| -> Option<Arm> {
let id = &v.ident;
let attributes = parse_child_attributes(&v.attrs).unwrap();

if let Some(rename) = attributes.rename {
parse_quote!(#rename => ::std::result::Result::Ok(#ident :: #id),)
} else if let Some(pattern) = cattr.rename_all {
let name = rename_all(&id.to_string(), pattern);
if attributes.other {
other_variant = Some(v);
return None;
}

parse_quote!(#name => ::std::result::Result::Ok(#ident :: #id),)
} else {
let name = id.to_string();
parse_quote!(#name => ::std::result::Result::Ok(#ident :: #id),)
}
});
Some(if let Some(rename) = attributes.rename {
parse_quote!(#rename => ::std::result::Result::Ok(#ident :: #id),)
} else if let Some(pattern) = cattr.rename_all {
let name = rename_all(&id.to_string(), pattern);

let values = quote! {
match value {
#(#value_arms)*
parse_quote!(#name => ::std::result::Result::Ok(#ident :: #id),)
} else {
let name = id.to_string();
parse_quote!(#name => ::std::result::Result::Ok(#ident :: #id),)
})
})
.collect::<Vec<_>>();

_ => Err(format!("invalid value {:?} for enum {}", value, #ident_s).into())
let default_arm = match other_variant {
Some(variant) => {
let span = match &variant.fields {
Fields::Unnamed(FieldsUnnamed { unnamed, .. }) => {
unnamed.first().expect("unreachable").span()
}
_ => unreachable!(),
};
let id = &variant.ident;
quote_spanned! { span => other => ::std::result::Result::Ok(#ident :: #id (String::from(other))) }
}
None => quote! {
_ => Err(format!("invalid value {:?} for enum {}", value, #ident_s).into())
},
};

let mut tts = TokenStream::new();
Expand All @@ -199,7 +217,10 @@ fn expand_derive_decode_strong_enum(
::sqlx::mysql::MySql,
>>::decode(value)?;

#values
match value {
#(#value_arms)*
#default_arm
}
}
}
));
Expand All @@ -225,7 +246,10 @@ fn expand_derive_decode_strong_enum(
::sqlx::postgres::Postgres,
>>::decode(value)?;

#values
match value {
#(#value_arms)*
#default_arm
}
}
}
));
Expand All @@ -251,7 +275,10 @@ fn expand_derive_decode_strong_enum(
::sqlx::sqlite::Sqlite,
>>::decode(value)?;

#values
match value {
#(#value_arms)*
#default_arm
}
}
}
));
Expand Down
22 changes: 17 additions & 5 deletions sqlx-macros-core/src/derives/encode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ use super::attributes::{
};
use super::rename_all;
use proc_macro2::{Span, TokenStream};
use quote::quote;
use quote::{quote, quote_spanned};
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::token::Comma;
use syn::{
parse_quote, Data, DataEnum, DataStruct, DeriveInput, Expr, Field, Fields, FieldsNamed,
Lifetime, LifetimeParam, Stmt, TypeParamBound, Variant,
FieldsUnnamed, Lifetime, LifetimeParam, Stmt, TypeParamBound, Variant,
};

pub fn expand_derive_encode(input: &DeriveInput) -> syn::Result<TokenStream> {
Expand Down Expand Up @@ -158,6 +159,17 @@ fn expand_derive_encode_strong_enum(
let id = &v.ident;
let attributes = parse_child_attributes(&v.attrs)?;

if attributes.other {
let span = match &v.fields {
Fields::Unnamed(FieldsUnnamed { unnamed, .. }) => {
unnamed.first().expect("unreachable").span()
}
_ => unreachable!(),
};
value_arms.push(quote_spanned!(span => #ident :: #id (other) => other,));
continue;
}

if let Some(rename) = attributes.rename {
value_arms.push(quote!(#ident :: #id => #rename,));
} else if let Some(pattern) = cattr.rename_all {
Expand All @@ -174,7 +186,7 @@ fn expand_derive_encode_strong_enum(
#[automatically_derived]
impl<'q, DB: ::sqlx::Database> ::sqlx::encode::Encode<'q, DB> for #ident
where
&'q ::std::primitive::str: ::sqlx::encode::Encode<'q, DB>,
for<'a> &'a ::std::primitive::str: ::sqlx::encode::Encode<'a, DB>,
{
fn encode_by_ref(
&self,
Expand All @@ -184,15 +196,15 @@ fn expand_derive_encode_strong_enum(
#(#value_arms)*
};

<&::std::primitive::str as ::sqlx::encode::Encode<'q, DB>>::encode(val, buf)
<&::std::primitive::str as ::sqlx::encode::Encode<'_, DB>>::encode(val, buf)
}

fn size_hint(&self) -> ::std::primitive::usize {
let val = match self {
#(#value_arms)*
};

<&::std::primitive::str as ::sqlx::encode::Encode<'q, DB>>::size_hint(&val)
<&::std::primitive::str as ::sqlx::encode::Encode<'_, DB>>::size_hint(&val)
}
}
))
Expand Down
48 changes: 48 additions & 0 deletions tests/postgres/derives.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,17 @@ enum Mood {
Sad,
}

// Stale enum type that can absorb any unknown variants
#[derive(PartialEq, Debug, sqlx::Type)]
#[sqlx(type_name = "operation")]
#[sqlx(rename_all = "lowercase")]
enum SafeOperation {
Add,
Subtract,
#[sqlx(other)]
Unknown(String),
}

// Records must map to a custom type
// Note that all types are types in Postgres
#[derive(PartialEq, Debug, sqlx::Type)]
Expand Down Expand Up @@ -206,8 +217,12 @@ async fn test_enum_type() -> anyhow::Result<()> {
r#"
DROP TABLE IF EXISTS people;

DROP TABLE IF EXISTS ops;

DROP TYPE IF EXISTS mood CASCADE;

DROP TYPE IF EXISTS operation CASCADE;

CREATE TYPE mood AS ENUM ( 'ok', 'happy', 'sad' );

DROP TYPE IF EXISTS color_lower CASCADE;
Expand All @@ -227,11 +242,17 @@ CREATE TYPE color_kebab_case AS ENUM ( 'red-green', 'blue-black' );
CREATE TYPE color_mixed_case AS ENUM ( 'redGreen', 'blueBlack' );
CREATE TYPE color_camel_case AS ENUM ( 'RedGreen', 'BlueBlack' );

CREATE TYPE operation AS ENUM ( 'add', 'subtract', 'multiply' );

CREATE TABLE people (
id serial PRIMARY KEY,
mood mood not null
);

CREATE TABLE ops (
id serial PRIMARY KEY,
op operation NOT NULL
);
"#,
)
.await?;
Expand Down Expand Up @@ -389,6 +410,33 @@ SELECT id, mood FROM people WHERE id = $1
assert!(rec.0);
assert_eq!(rec.1, ColorPascalCase::RedGreen);

let id: i32 = sqlx::query_scalar("INSERT INTO ops (op) VALUES ('multiply') RETURNING id")
.fetch_one(&mut conn)
.await?;

#[derive(sqlx::FromRow)]
struct OpRow {
id: i32,
op: SafeOperation,
}

let row: OpRow = sqlx::query_as("SELECT id, op FROM ops WHERE id = $1")
.bind(id)
.fetch_one(&mut conn)
.await?;

assert_eq!(row.id, id);
assert_eq!(row.op, SafeOperation::Unknown("multiply".to_owned()));

let id: Option<i32> =
sqlx::query_scalar("UPDATE ops SET op = $2 WHERE id = $1 AND op <> $2 RETURNING id")
.bind(id)
.bind(row.op)
.fetch_optional(&mut conn)
.await?;

assert!(id.is_none());

Ok(())
}

Expand Down
Loading
Loading