Skip to content
This repository was archived by the owner on Jan 27, 2026. It is now read-only.

Commit c29482a

Browse files
committed
one2opt relationship
1 parent c119e8c commit c29482a

10 files changed

Lines changed: 92 additions & 75 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ through auto-generated REST API.
1919

2020
- ✅ Querying and ranging by secondary index
2121
- ✅ Optional dictionaries for low cardinality fields
22-
- ✅ One-to-One and One-to-Many entities with cascade read/write/delete
22+
- ✅ One-to-One / One-to-Option / One-to-Many entities with cascade read/write/delete
2323
- ✅ All goodies including intuitive data ordering without writing custom codecs
24-
- ✅ Macro derived http rest API at http://127.0.0.1:8000/swagger-ui/
24+
- ✅ Macro derived http rest API at http://127.0.0.1:8000/swagger-ui/ with examples
2525
- ✅ Macro derived unit tests and integration tests on axum test server
2626

2727
### Limitations

examples/utxo/src/lib.rs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ pub use redbit::*;
1717
#[index] pub struct Datum(pub Vec<u8>);
1818
#[index] pub struct AssetName(pub String);
1919

20-
2120
#[index]
2221
#[derive(Copy, Hash)]
2322
pub struct Timestamp(pub u32);
@@ -26,9 +25,7 @@ pub struct Timestamp(pub u32);
2625
pub struct Block {
2726
#[pk(range)]
2827
pub id: Height,
29-
#[one2one]
3028
pub header: BlockHeader,
31-
#[one2many]
3229
pub transactions: Vec<Transaction>,
3330
}
3431

@@ -52,9 +49,7 @@ pub struct Transaction {
5249
pub id: TxPointer,
5350
#[column(index)]
5451
pub hash: Hash,
55-
#[one2many]
5652
pub utxos: Vec<Utxo>,
57-
#[one2many]
5853
pub inputs: Vec<InputRef>,
5954
}
6055

@@ -68,15 +63,13 @@ pub struct Utxo {
6863
pub datum: Datum,
6964
#[column(index, dictionary)]
7065
pub address: Address,
71-
#[one2many]
7266
pub assets: Vec<Asset>,
73-
#[one2one]
7467
pub tree: Option<Tree>,
7568
}
7669

7770
#[entity]
7871
pub struct Tree {
79-
#[fk(one2one, range)]
72+
#[fk(one2opt, range)]
8073
pub id: UtxoPointer,
8174
#[column(index)]
8275
pub hash: Hash,

macros/src/field_parser.rs

Lines changed: 70 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ fn parse_entity_field(field: &syn::Field) -> Result<ParsingResult, syn::Error> {
9494
fk = Some(Multiplicity::OneToMany);
9595
} else if nested.path.is_ident("one2one") {
9696
fk = Some(Multiplicity::OneToOne);
97+
} else if nested.path.is_ident("one2opt") {
98+
fk = Some(Multiplicity::OneToOption);
9799
}
98100
Ok(())
99101
});
@@ -120,63 +122,87 @@ fn parse_entity_field(field: &syn::Field) -> Result<ParsingResult, syn::Error> {
120122
} else if attr.path().is_ident("transient") {
121123
let field = FieldDef { name: column_name.clone(), tpe: column_type.clone() };
122124
return Ok(ParsingResult::Transient(TransientDef {field}))
123-
} else if let Type::Path(type_path) = &column_type {
124-
if let Some(segment) = type_path.path.segments.last() {
125-
if attr.path().is_ident("one2many") && segment.ident == "Vec" {
125+
}
126+
}
127+
if let Type::Path(type_path) = &column_type {
128+
if let Some(segment) = type_path.path.segments.last() {
129+
match segment.ident.to_string().as_str() {
130+
"Vec" => {
131+
// one-to-many
126132
if let PathArguments::AngleBracketed(args) = &segment.arguments {
127133
if let Some(GenericArgument::Type(Type::Path(inner_type_path))) = args.args.first() {
128-
let inner_type =
129-
&inner_type_path.path.segments.last()
130-
.ok_or_else(|| syn::Error::new(field.span(), "Parent field missing"))?.ident;
131-
let type_path = Type::Path(syn::TypePath { qself: None, path: syn::Path::from(inner_type.clone()) });
132-
let field = FieldDef { name: column_name.clone(), tpe: type_path };
133-
return Ok(ParsingResult::RelationShip(RelationshipDef { field, multiplicity: Multiplicity::OneToMany }));
134+
let inner_type = inner_type_path
135+
.path
136+
.segments
137+
.last()
138+
.ok_or_else(|| syn::Error::new(field.span(), "Parent field missing"))?
139+
.ident
140+
.clone();
141+
let type_path = Type::Path(syn::TypePath {
142+
qself: None,
143+
path: syn::Path::from(inner_type),
144+
});
145+
let field = FieldDef {
146+
name: column_name.clone(),
147+
tpe: type_path,
148+
};
149+
return Ok(ParsingResult::RelationShip(RelationshipDef {
150+
field,
151+
multiplicity: Multiplicity::OneToMany,
152+
}));
134153
}
135154
}
136-
} else if attr.path().is_ident("one2one") {
137-
// Default to OneToOne
138-
let mut multiplicity = Multiplicity::OneToOne;
139-
let mut actual_type = &field.ty;
140-
141-
// Check if the type is Option<T>
142-
if let Type::Path(type_path) = &field.ty {
143-
if let Some(segment) = type_path.path.segments.first() {
144-
if segment.ident == "Option" {
145-
// It is Option<T>, now get T
146-
if let PathArguments::AngleBracketed(angle_bracketed) = &segment.arguments {
147-
if let Some(GenericArgument::Type(inner_type)) = angle_bracketed.args.first() {
148-
actual_type = inner_type;
149-
multiplicity = Multiplicity::OneToOption;
150-
}
151-
}
152-
}
155+
}
156+
"Option" => {
157+
// one-to-option
158+
if let PathArguments::AngleBracketed(args) = &segment.arguments {
159+
if let Some(GenericArgument::Type(Type::Path(inner_type_path))) = args.args.first() {
160+
let inner_type = inner_type_path
161+
.path
162+
.segments
163+
.last()
164+
.ok_or_else(|| syn::Error::new(field.span(), "Parent field missing"))?
165+
.ident
166+
.clone();
167+
let type_path = Type::Path(syn::TypePath {
168+
qself: None,
169+
path: syn::Path::from(inner_type),
170+
});
171+
let field = FieldDef {
172+
name: column_name.clone(),
173+
tpe: type_path,
174+
};
175+
return Ok(ParsingResult::RelationShip(RelationshipDef {
176+
field,
177+
multiplicity: Multiplicity::OneToOption,
178+
}));
153179
}
154180
}
155-
156-
// Now get the segment from the actual type
157-
if let Type::Path(type_path) = actual_type {
158-
if let Some(segment) = type_path.path.segments.last() {
159-
if segment.arguments.is_empty() {
160-
let struct_type = &segment.ident;
161-
let type_path = Type::Path(syn::TypePath { qself: None, path: syn::Path::from(struct_type.clone()) });
162-
let field = FieldDef {
163-
name: column_name.clone(),
164-
tpe: type_path,
165-
};
166-
return Ok(ParsingResult::RelationShip(RelationshipDef {
167-
field,
168-
multiplicity,
169-
}));
170-
}
171-
}
181+
}
182+
_ => {
183+
// one-to-one (plain type)
184+
let struct_type = &segment.ident;
185+
if segment.arguments.is_empty() {
186+
let type_path = Type::Path(syn::TypePath {
187+
qself: None,
188+
path: syn::Path::from(struct_type.clone()),
189+
});
190+
let field = FieldDef {
191+
name: column_name.clone(),
192+
tpe: type_path,
193+
};
194+
return Ok(ParsingResult::RelationShip(RelationshipDef {
195+
field,
196+
multiplicity: Multiplicity::OneToOne,
197+
}));
172198
}
173199
}
174200
}
175201
}
176202
}
177203
Err(syn::Error::new(
178204
field.span(),
179-
"Field must have one of #[pk(...)] / #[fk(...)] / #[column(...)] / #[one2one] / #[one2many] / #[transient] annotations of expected underlying types",
205+
"Field must have one of #[pk(...)] / #[fk(...)] / #[column(...)] / #[transient] annotations or it is a one2one, one2opt, or one2many relationship (e.g., `Vec<Transaction>` or `Option<Transaction>`).",
180206
))
181207
}
182208
}

macros/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ pub fn entity(_attr: TokenStream, item: TokenStream) -> TokenStream {
200200
quote!(#s).into()
201201
}
202202

203-
#[proc_macro_derive(Entity, attributes(pk, fk, column, one2many, one2one, transient))]
203+
#[proc_macro_derive(Entity, attributes(pk, fk, column, transient))]
204204
#[proc_macro_error]
205205
pub fn derive_entity(input: TokenStream) -> TokenStream {
206206
let item_struct = parse_macro_input!(input as ItemStruct);

macros/src/relationship.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,13 @@ impl DbRelationshipMacros {
4040
Multiplicity::OneToOption => {
4141
DbRelationshipMacros {
4242
definition: definition.clone(),
43-
struct_init: init::one2option_relation_init(child_name, child_type),
44-
struct_default_init: init::one2option_relation_default_init(child_name, child_type),
45-
store_statement: store::one2option_store_def(child_name, child_type),
46-
store_many_statement: store::one2option_store_many_def(child_name, child_type),
47-
delete_statement: delete::one2option_delete_def(child_type),
48-
delete_many_statement: delete::one2option_delete_many_def(child_type),
49-
function_def: get::one2option_def(entity_ident, child_name, child_type, &pk_name, &pk_type)
43+
struct_init: init::one2opt_relation_init(child_name, child_type),
44+
struct_default_init: init::one2opt_relation_default_init(child_name, child_type),
45+
store_statement: store::one2opt_store_def(child_name, child_type),
46+
store_many_statement: store::one2opt_store_many_def(child_name, child_type),
47+
delete_statement: delete::one2opt_delete_def(child_type),
48+
delete_many_statement: delete::one2opt_delete_many_def(child_type),
49+
function_def: get::one2opt_def(entity_ident, child_name, child_type, &pk_name, &pk_type)
5050
}
5151
}
5252
Multiplicity::OneToMany => {

macros/src/relationship/delete.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ pub fn one2one_delete_many_def(child_type: &Type) -> TokenStream {
1515
}
1616

1717

18-
pub fn one2option_delete_def(child_type: &Type) -> TokenStream {
18+
pub fn one2opt_delete_def(child_type: &Type) -> TokenStream {
1919
quote! {
2020
#child_type::delete(&tx, pk)?;
2121
}
2222
}
2323

24-
pub fn one2option_delete_many_def(child_type: &Type) -> TokenStream {
24+
pub fn one2opt_delete_many_def(child_type: &Type) -> TokenStream {
2525
quote! {
2626
#child_type::delete_many(&tx, pks)?;
2727
}

macros/src/relationship/get.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ pub fn one2one_def(entity_name: &Ident, child_name: &Ident, child_type: &Type, p
3535
}
3636
}
3737

38-
pub fn one2option_def(
38+
pub fn one2opt_def(
3939
entity_name: &Ident,
4040
child_name: &Ident,
4141
child_type: &Type,

macros/src/relationship/init.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,25 @@ use proc_macro2::{Ident, TokenStream};
22
use quote::quote;
33
use syn::Type;
44

5-
pub fn one2one_relation_init(child_name: &Ident, child_type: &Type) -> TokenStream {
5+
pub fn one2one_relation_init(child_name: &Ident, child_type: &Type) -> TokenStream {
66
quote! {
77
#child_name: #child_type::get(tx, pk)?.ok_or_else(|| AppError::NotFound(format!("Missing one-to-one child {:?}", pk)))?
88
}
99
}
1010

11-
pub fn one2one_relation_default_init(child_name: &Ident, child_type: &Type) -> TokenStream {
11+
pub fn one2one_relation_default_init(child_name: &Ident, child_type: &Type) -> TokenStream {
1212
quote! {
1313
#child_name: #child_type::sample_with(pk, sample_index)
1414
}
1515
}
1616

17-
pub fn one2option_relation_init(child_name: &Ident, child_type: &Type) -> TokenStream {
17+
pub fn one2opt_relation_init(child_name: &Ident, child_type: &Type) -> TokenStream {
1818
quote! {
1919
#child_name: #child_type::get(tx, pk)?
2020
}
2121
}
2222

23-
pub fn one2option_relation_default_init(child_name: &Ident, child_type: &Type) -> TokenStream {
23+
pub fn one2opt_relation_default_init(child_name: &Ident, child_type: &Type) -> TokenStream {
2424
quote! {
2525
#child_name: Some(#child_type::sample_with(pk, sample_index))
2626
}

macros/src/relationship/store.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,29 @@ use proc_macro2::{Ident, TokenStream};
22
use quote::quote;
33
use syn::Type;
44

5-
pub fn one2one_store_def(child_name: &Ident, child_type: &Type) -> TokenStream {
5+
pub fn one2one_store_def(child_name: &Ident, child_type: &Type) -> TokenStream {
66
quote! {
77
let child = &instance.#child_name;
88
#child_type::store(&tx, child)?;
99
}
1010
}
1111

12-
pub fn one2one_store_many_def(child_name: &Ident, child_type: &Type) -> TokenStream {
12+
pub fn one2one_store_many_def(child_name: &Ident, child_type: &Type) -> TokenStream {
1313
quote! {
1414
let children = instances.iter().map(|instance| instance.#child_name.clone()).collect();
1515
#child_type::store_many(&tx, &children)?;
1616
}
1717
}
1818

19-
pub fn one2option_store_def(child_name: &Ident, child_type: &Type) -> TokenStream {
19+
pub fn one2opt_store_def(child_name: &Ident, child_type: &Type) -> TokenStream {
2020
quote! {
2121
if let Some(child) = &instance.#child_name {
2222
#child_type::store(&tx, child)?;
2323
}
2424
}
2525
}
2626

27-
pub fn one2option_store_many_def(child_name: &Ident, child_type: &Type) -> TokenStream {
27+
pub fn one2opt_store_many_def(child_name: &Ident, child_type: &Type) -> TokenStream {
2828
quote! {
2929
let mut children = Vec::with_capacity(instances.len());
3030
for instance in instances {

tests/passing_test.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,7 @@ pub struct FullStruct {
6060
struct MultipleOne2ManyAnnotationsStruct {
6161
#[pk(range)]
6262
id: ParentPK,
63-
#[one2many]
6463
foos: Vec<MinimalStruct>,
65-
#[one2many]
6664
bars: Vec<MinimalStruct>,
6765
}
6866

0 commit comments

Comments
 (0)