Skip to content

Commit edd145e

Browse files
committed
Allow #[derive(Identifiable)] to work with composite primary keys
This is a tad bit magic in the macro piece. We're expecting the primary keys *not to* have a trailing comma, so we can rely on the fact that `(T)` is equivalent to `T`, but if there's more than one element it would be a tuple. Beyond that everything was quite straightforward with the ground work that we've laid. Fixes diesel-rs#42.
1 parent 9819ed4 commit edd145e

11 files changed

Lines changed: 259 additions & 92 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ for Rust libraries in [RFC #1105](https://github.com/rust-lang/rfcs/blob/master/
4444

4545
[exists]: http://docs.diesel.rs/diesel/expression/dsl/fn.sql.html
4646

47+
* `#[derive(Identifiable)]` can be used with structs that have primary keys
48+
other than `id`, as well as structs with composite primary keys. You can now
49+
annotate the struct with `#[primary_key(nonstandard)]` or `#[primary_key(foo,
50+
bar)]`.
51+
4752
### Changed
4853

4954
* All macros with the same name as traits we can derive (e.g. `Queryable!`) have

diesel/src/macros/identifiable.rs

Lines changed: 44 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,12 @@ macro_rules! impl_Identifiable {
6565
body = $($body:tt)*
6666
),
6767
found_option_with_name = primary_key,
68-
value = ($primary_key_name:ident),
68+
value = $primary_key_names:tt,
6969
) => {
7070
impl_Identifiable! {
7171
(
7272
table_name = $table_name,
73-
primary_key_name = $primary_key_name,
73+
primary_key_names = $primary_key_names,
7474
)
7575
$($body)*
7676
}
@@ -83,13 +83,14 @@ macro_rules! impl_Identifiable {
8383
struct_ty = $struct_ty:ty,
8484
lifetimes = ($($lifetimes:tt),*),
8585
),
86-
primary_key_field = {
86+
found_fields_with_field_names,
87+
fields = [$({
8788
field_name: $field_name:ident,
8889
column_name: $column_name:ident,
8990
field_ty: $field_ty:ty,
9091
field_kind: $field_kind:ident,
9192
$($rest:tt)*
92-
},
93+
})*],
9394
) => {
9495
impl<$($lifetimes),*> $crate::associations::HasTable for $struct_ty {
9596
type Table = $table_name::table;
@@ -100,48 +101,28 @@ macro_rules! impl_Identifiable {
100101
}
101102

102103
impl<'ident $(,$lifetimes)*> $crate::associations::Identifiable for &'ident $struct_ty {
103-
type Id = &'ident $field_ty;
104+
type Id = ($(&'ident $field_ty),*);
104105

105106
fn id(self) -> Self::Id {
106-
&self.$field_name
107+
($(&self.$field_name),*)
107108
}
108109
}
109110
};
110111

111-
// Search for the primary key field and continue
112+
// Search for the primary key fields and continue
112113
(
113114
(
114115
table_name = $table_name:ident,
115-
primary_key_name = $primary_key_name:ident,
116+
primary_key_names = $primary_key_names:tt,
116117
$($args:tt)*
117118
),
118-
fields = [{
119-
field_name: $field_name:ident,
120-
$($rest:tt)*
121-
} $($fields:tt)*],
119+
fields = $fields:tt,
122120
) => {
123-
static_cond! {
124-
if $primary_key_name == $field_name {
125-
impl_Identifiable! {
126-
(
127-
table_name = $table_name,
128-
$($args)*
129-
),
130-
primary_key_field = {
131-
field_name: $field_name,
132-
$($rest)*
133-
},
134-
}
135-
} else {
136-
impl_Identifiable! {
137-
(
138-
table_name = $table_name,
139-
primary_key_name = $primary_key_name,
140-
$($args)*
141-
),
142-
fields = [$($fields)*],
143-
}
144-
}
121+
__diesel_fields_with_field_names! {
122+
(table_name = $table_name, $($args)*),
123+
callback = impl_Identifiable,
124+
targets = $primary_key_names,
125+
fields = $fields,
145126
}
146127
};
147128

@@ -345,3 +326,32 @@ fn derive_identifiable_with_non_standard_pk_given_before_table_name() {
345326
assert_eq!(&"hi", foo1.id());
346327
assert_eq!(&"there", foo2.id());
347328
}
329+
330+
#[test]
331+
fn derive_identifiable_with_composite_pk() {
332+
use associations::Identifiable;
333+
334+
#[allow(missing_debug_implementations, missing_copy_implementations, dead_code)]
335+
struct Foo {
336+
id: i32,
337+
foo_id: i32,
338+
bar_id: i32,
339+
foo: i32,
340+
}
341+
342+
impl_Identifiable! {
343+
#[primary_key(foo_id, bar_id)]
344+
#[table_name(bars)]
345+
struct Foo {
346+
id: i32,
347+
foo_id: i32,
348+
bar_id: i32,
349+
foo: i32,
350+
}
351+
}
352+
353+
let foo1 = Foo { id: 1, foo_id: 2, bar_id: 3, foo: 4 };
354+
let foo2 = Foo { id: 5, foo_id: 6, bar_id: 7, foo: 8 };
355+
assert_eq!((&2, &3), foo1.id());
356+
assert_eq!((&6, &7), foo2.id());
357+
}

diesel/src/macros/parse.rs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,105 @@ macro_rules! __diesel_field_with_column_name {
333333
};
334334
}
335335

336+
#[doc(hidden)]
337+
#[macro_export]
338+
macro_rules! __diesel_field_with_field_name {
339+
(
340+
$headers:tt,
341+
callback = $callback:ident,
342+
target = $target_field_name:ident,
343+
fields = [$({
344+
field_name: $field_name:ident,
345+
$($field_info:tt)*
346+
})*],
347+
) => {
348+
$(
349+
static_cond! {
350+
if $target_field_name == $field_name {
351+
$callback! {
352+
$headers,
353+
found_field_with_field_name = $field_name,
354+
field = {
355+
field_name: $field_name,
356+
$($field_info)*
357+
},
358+
}
359+
}
360+
}
361+
)*
362+
};
363+
}
364+
365+
#[doc(hidden)]
366+
#[macro_export]
367+
macro_rules! __diesel_fields_with_field_names {
368+
// Entrypoint, start search
369+
(
370+
$headers:tt,
371+
callback = $callback:ident,
372+
targets = ($target_field_name:ident $(,$rest:ident)*),
373+
fields = $fields:tt,
374+
) => {
375+
__diesel_field_with_field_name! {
376+
(
377+
targets = ($($rest),*),
378+
fields = $fields,
379+
headers = $headers,
380+
callback = $callback,
381+
found_fields = [],
382+
),
383+
callback = __diesel_fields_with_field_names,
384+
target = $target_field_name,
385+
fields = $fields,
386+
}
387+
};
388+
389+
// Found field, more to search for
390+
(
391+
(
392+
targets = ($target_field_name:ident $(,$rest:ident)*),
393+
fields = $fields:tt,
394+
headers = $headers:tt,
395+
callback = $callback:ident,
396+
found_fields = [$($found_fields:tt)*],
397+
),
398+
found_field_with_field_name = $ignore:tt,
399+
field = $field:tt,
400+
) => {
401+
__diesel_field_with_field_name! {
402+
(
403+
targets = ($($rest),*),
404+
fields = $fields,
405+
headers = $headers,
406+
callback = $callback,
407+
found_fields = [$($found_fields)* $field],
408+
),
409+
callback = __diesel_fields_with_field_names,
410+
target = $target_field_name,
411+
fields = $fields,
412+
}
413+
};
414+
415+
// Found field, no more to search for
416+
(
417+
(
418+
targets = (),
419+
fields = $fields:tt,
420+
headers = $headers:tt,
421+
callback = $callback:ident,
422+
found_fields = [$($found_fields:tt)*],
423+
),
424+
found_field_with_field_name = $ignore:tt,
425+
field = $field:tt,
426+
) => {
427+
$callback! {
428+
$headers,
429+
found_fields_with_field_names,
430+
fields = [$($found_fields)* $field],
431+
}
432+
}
433+
}
434+
336435
#[doc(hidden)]
337436
#[macro_export]
338437
macro_rules! __diesel_find_option_with_name {

diesel_codegen/src/identifiable.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,18 @@ pub fn derive_identifiable(item: syn::MacroInput) -> Tokens {
88
let table_name = model.table_name();
99
let struct_ty = &model.ty;
1010
let lifetimes = model.generics.lifetimes;
11-
let primary_key_name = model.primary_key_name;
11+
let primary_key_names = model.primary_key_names;
1212
let fields = model.attrs;
13-
if !fields.iter().any(|f| f.field_name.as_ref() == Some(&primary_key_name)) {
14-
panic!("Could not find a field named `{}` on `{}`", primary_key_name, &model.name);
13+
for pk in &primary_key_names {
14+
if !fields.iter().any(|f| f.field_name.as_ref() == Some(pk)) {
15+
panic!("Could not find a field named `{}` on `{}`", pk, &model.name);
16+
}
1517
}
1618

1719
quote!(impl_Identifiable! {
1820
(
1921
table_name = #table_name,
20-
primary_key_name = #primary_key_name,
22+
primary_key_names = (#(#primary_key_names),*),
2123
struct_ty = #struct_ty,
2224
lifetimes = (#(#lifetimes),*),
2325
),

diesel_codegen/src/model.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ pub struct Model {
88
pub attrs: Vec<Attr>,
99
pub name: syn::Ident,
1010
pub generics: syn::Generics,
11-
pub primary_key_name: syn::Ident,
11+
pub primary_key_names: Vec<syn::Ident>,
1212
table_name_from_annotation: Option<syn::Ident>,
1313
}
1414

@@ -23,9 +23,9 @@ impl Model {
2323
let ty = struct_ty(item.ident.clone(), &item.generics);
2424
let name = item.ident.clone();
2525
let generics = item.generics.clone();
26-
let primary_key_name = ident_value_of_attr_with_name(&item.attrs, "primary_key")
27-
.map(Clone::clone)
28-
.unwrap_or(syn::Ident::new("id"));
26+
let primary_key_names = list_value_of_attr_with_name(&item.attrs, "primary_key")
27+
.map(|v| v.into_iter().map(Clone::clone).collect())
28+
.unwrap_or_else(|| vec![syn::Ident::new("id")]);
2929
let table_name_from_annotation = str_value_of_attr_with_name(
3030
&item.attrs, "table_name").map(syn::Ident::new);
3131

@@ -34,7 +34,7 @@ impl Model {
3434
attrs: attrs,
3535
name: name,
3636
generics: generics,
37-
primary_key_name: primary_key_name,
37+
primary_key_names: primary_key_names,
3838
table_name_from_annotation: table_name_from_annotation,
3939
})
4040
}

diesel_codegen/src/util.rs

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,19 @@ pub fn ident_value_of_attr_with_name<'a>(
3535
attrs: &'a [Attribute],
3636
name: &str,
3737
) -> Option<&'a Ident> {
38-
attr_with_name(attrs, name).map(|attr| single_arg_value_of_attr(attr, name))
38+
list_value_of_attr_with_name(attrs, name).map(|idents| {
39+
if idents.len() != 1 {
40+
panic!(r#"`{}` must be in the form `#[{}(something)]`"#, name, name);
41+
}
42+
idents[0]
43+
})
44+
}
45+
46+
pub fn list_value_of_attr_with_name<'a>(
47+
attrs: &'a [Attribute],
48+
name: &str,
49+
) -> Option<Vec<&'a Ident>> {
50+
attr_with_name(attrs, name).map(|attr| list_value_of_attr(attr, name))
3951
}
4052

4153
pub fn attr_with_name<'a>(
@@ -56,19 +68,15 @@ pub fn str_value_of_meta_item<'a>(item: &'a MetaItem, name: &str) -> &'a str {
5668
}
5769
}
5870

59-
fn single_arg_value_of_attr<'a>(attr: &'a Attribute, name: &str) -> &'a Ident {
60-
let usage_err = || panic!(r#"`{}` must be in the form `#[{}(something)]`"#, name, name);
71+
fn list_value_of_attr<'a>(attr: &'a Attribute, name: &str) -> Vec<&'a Ident> {
6172
match attr.value {
6273
MetaItem::List(_, ref items) => {
63-
if items.len() != 1 {
64-
return usage_err();
65-
}
66-
match items[0] {
74+
items.iter().map(|item| match *item {
6775
NestedMetaItem::MetaItem(MetaItem::Word(ref name)) => name,
68-
_ => usage_err(),
69-
}
76+
_ => panic!(r#"`{}` must be in the form `#[{}(something)]`"#, name, name),
77+
}).collect()
7078
}
71-
_ => usage_err(),
79+
_ => panic!(r#"`{}` must be in the form `#[{}(something)]`"#, name, name),
7280
}
7381
}
7482

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
use syntax::ast;
22
use syntax::codemap::Span;
33
use syntax::ext::base::{Annotatable, ExtCtxt};
4+
use syntax::parse::token;
45

56
use model::Model;
6-
use util::lifetime_list_tokens;
7+
use util::{lifetime_list_tokens, comma_delimited_tokens};
78

89
pub fn expand_derive_identifiable(
910
cx: &mut ExtCtxt,
@@ -16,20 +17,25 @@ pub fn expand_derive_identifiable(
1617
let table_name = model.table_name();
1718
let struct_ty = &model.ty;
1819
let lifetimes = lifetime_list_tokens(&model.generics.lifetimes, span);
19-
let primary_key_name = model.primary_key_name;
20+
let primary_key_names = model.primary_key_names();
2021
let fields = model.field_tokens_for_stable_macro(cx);
21-
if model.attr_named(primary_key_name).is_some() {
22-
push(Annotatable::Item(quote_item!(cx, impl_Identifiable! {
23-
(
24-
table_name = $table_name,
25-
primary_key_name = $primary_key_name,
26-
struct_ty = $struct_ty,
27-
lifetimes = ($lifetimes),
28-
),
29-
fields = [$fields],
30-
}).unwrap()));
31-
} else {
32-
cx.span_err(span, &format!("Could not find a field named `{}` on `{}`", primary_key_name, model.name));
22+
for name in primary_key_names {
23+
if model.attr_named(*name).is_none() {
24+
cx.span_err(span, &format!("Could not find a field named `{}` on `{}`", name, model.name));
25+
return;
26+
}
3327
}
28+
29+
let primary_key_names = comma_delimited_tokens(
30+
primary_key_names.into_iter().map(|n| token::Ident(*n)), span);
31+
push(Annotatable::Item(quote_item!(cx, impl_Identifiable! {
32+
(
33+
table_name = $table_name,
34+
primary_key_names = ($primary_key_names),
35+
struct_ty = $struct_ty,
36+
lifetimes = ($lifetimes),
37+
),
38+
fields = [$fields],
39+
}).unwrap()));
3440
}
3541
}

0 commit comments

Comments
 (0)