Skip to content

Commit 6d3e410

Browse files
authored
Add support for specifying a different database name for fields (#174)
1 parent 90c9a89 commit 6d3e410

File tree

28 files changed

+292
-100
lines changed

28 files changed

+292
-100
lines changed

crates/toasty-codegen/src/expand/schema.rs

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use super::{util, Expand};
2-
use crate::schema::{ColumnType, FieldTy, Name};
2+
use crate::schema::{Column, FieldTy, Name};
33

44
use proc_macro2::TokenStream;
55
use quote::quote;
@@ -50,18 +50,30 @@ impl Expand<'_> {
5050

5151
let fields = self.model.fields.iter().enumerate().map(|(index, field)| {
5252
let index_tokenized = util::int(index);
53-
let name = field.name.ident.to_string();
5453
let field_ty;
5554
let nullable;
5655

56+
let name = {
57+
let app_name = field.name.ident.to_string();
58+
let storage_name = match field.attrs.column.as_ref().and_then(|column| column.name.as_ref()) {
59+
Some(name) => quote! { Some(#name.to_string()) },
60+
None => quote! { None },
61+
};
62+
quote! {
63+
FieldName {
64+
app_name: #app_name.to_string(),
65+
storage_name: #storage_name,
66+
}
67+
}
68+
};
69+
5770
match &field.ty {
5871
FieldTy::Primitive(ty) => {
59-
let storage_ty = match &field.attrs.db {
60-
Some(ColumnType::VarChar(size)) => {
61-
let size = util::int(*size);
62-
quote!(Some(db::Type::VarChar(#size)))
72+
let storage_ty = match &field.attrs.column {
73+
Some(Column { ty: Some(ty), ..}) => {
74+
quote!(Some(#ty))
6375
}
64-
None => quote!(None),
76+
_ => quote!(None),
6577
};
6678

6779
nullable = quote!(<#ty as #toasty::stmt::Primitive>::NULLABLE);
@@ -144,7 +156,7 @@ impl Expand<'_> {
144156
model: #model_ident::id(),
145157
index: #index_tokenized,
146158
},
147-
name: #name.to_string(),
159+
name: #name,
148160
ty: #field_ty,
149161
nullable: #nullable,
150162
primary_key: #primary_key,

crates/toasty-codegen/src/schema.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,5 @@ pub(crate) use name::Name;
3434
mod pk;
3535
pub(crate) use pk::PrimaryKey;
3636

37-
mod ty;
38-
pub(crate) use ty::ColumnType;
37+
mod column;
38+
pub(crate) use column::Column;
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
use quote::quote;
2+
use syn::parenthesized;
3+
4+
#[derive(Debug)]
5+
pub(crate) struct Column {
6+
pub(crate) name: Option<syn::LitStr>,
7+
pub(crate) ty: Option<ColumnType>,
8+
}
9+
10+
impl Column {
11+
pub(super) fn from_ast(attr: &syn::Attribute) -> syn::Result<Column> {
12+
attr.parse_args()
13+
}
14+
}
15+
16+
impl syn::parse::Parse for Column {
17+
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
18+
let mut result = Self {
19+
name: None,
20+
ty: None,
21+
};
22+
23+
// Loop through the list of comma separated arguments to fill in the result one by one.
24+
//
25+
// Allowed syntax:
26+
//
27+
// #[column("name")]
28+
// #[column(type = "type")]
29+
// #[column("name", type = "type")]
30+
// #[column(type = "type", "name")]
31+
loop {
32+
let lookahead = input.lookahead1();
33+
34+
if lookahead.peek(syn::LitStr) {
35+
if result.name.is_some() {
36+
return Err(syn::Error::new(input.span(), "duplicate column name"));
37+
}
38+
result.name = Some(input.parse()?);
39+
} else if lookahead.peek(syn::Token![type]) {
40+
if result.ty.is_some() {
41+
return Err(syn::Error::new(input.span(), "duplicate column type"));
42+
}
43+
let _type_token: syn::Token![type] = input.parse()?;
44+
let _eq_token: syn::Token![=] = input.parse()?;
45+
result.ty = Some(input.parse()?);
46+
} else {
47+
return Err(lookahead.error());
48+
}
49+
50+
if input.is_empty() {
51+
break;
52+
}
53+
let _comma_token: syn::Token![,] = input.parse()?;
54+
}
55+
56+
Ok(result)
57+
}
58+
}
59+
60+
mod kw {
61+
syn::custom_keyword!(varchar);
62+
}
63+
64+
#[derive(Debug)]
65+
pub enum ColumnType {
66+
VarChar(u64),
67+
Custom(syn::LitStr),
68+
}
69+
70+
impl syn::parse::Parse for ColumnType {
71+
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
72+
let lookahead = input.lookahead1();
73+
if lookahead.peek(syn::LitStr) {
74+
Ok(Self::Custom(input.parse()?))
75+
} else if lookahead.peek(kw::varchar) {
76+
let _kw: kw::varchar = input.parse()?;
77+
let content;
78+
parenthesized!(content in input);
79+
let lit: syn::LitInt = content.parse()?;
80+
Ok(Self::VarChar(lit.base10_parse()?))
81+
} else {
82+
Err(lookahead.error())
83+
}
84+
}
85+
}
86+
87+
impl quote::ToTokens for ColumnType {
88+
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
89+
match self {
90+
Self::VarChar(size) => quote! { db::Type::VarChar(#size) },
91+
Self::Custom(custom) => quote! { db::Type::Custom(#custom) },
92+
}
93+
.to_tokens(tokens);
94+
}
95+
}

crates/toasty-codegen/src/schema/field.rs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::{BelongsTo, ColumnType, ErrorSet, HasMany, HasOne, Name};
1+
use super::{BelongsTo, Column, ErrorSet, HasMany, HasOne, Name};
22

33
use syn::spanned::Spanned;
44

@@ -34,8 +34,8 @@ pub(crate) struct FieldAttr {
3434
/// True if the field is indexed
3535
pub(crate) index: bool,
3636

37-
/// Optional database column type
38-
pub(crate) db: Option<ColumnType>,
37+
/// Optional database column name and / or type
38+
pub(crate) column: Option<Column>,
3939
}
4040

4141
#[derive(Debug)]
@@ -66,7 +66,7 @@ impl Field {
6666
unique: false,
6767
auto: false,
6868
index: false,
69-
db: None,
69+
column: None,
7070
};
7171

7272
let mut ty = None;
@@ -136,18 +136,21 @@ impl Field {
136136
} else {
137137
ty = Some(FieldTy::HasOne(HasOne::from_ast(&field.ty, field.span())?));
138138
}
139-
} else if attr.path().is_ident("db") {
140-
if attrs.db.is_some() {
141-
errs.push(syn::Error::new_spanned(attr, "duplicate #[db] attribute"));
139+
} else if attr.path().is_ident("column") {
140+
if attrs.column.is_some() {
141+
errs.push(syn::Error::new_spanned(
142+
attr,
143+
"duplicate #[column] attribute",
144+
));
142145
} else {
143-
attrs.db = Some(ColumnType::from_ast(attr)?);
146+
attrs.column = Some(Column::from_ast(attr)?);
144147
}
145148
} else if attr.path().is_ident("toasty") {
146149
// todo
147150
}
148151
}
149152

150-
if ty.is_some() && attrs.db.is_some() {
153+
if ty.is_some() && attrs.column.is_some() {
151154
errs.push(syn::Error::new_spanned(
152155
field,
153156
"relation fields cannot have a database type",

crates/toasty-codegen/src/schema/ty.rs

Lines changed: 0 additions & 33 deletions
This file was deleted.

crates/toasty-core/src/driver/capability.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ impl Capability {
4646
// These types shouldn't be used as default string types, but handle them gracefully
4747
None
4848
}
49+
db::Type::Custom(_) => None, // Custom types are not known to have a limited length
4950
}
5051
}
5152

crates/toasty-core/src/schema/app.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ mod constraint;
1010
pub use constraint::{Constraint, ConstraintLength};
1111

1212
mod field;
13-
pub use field::{Field, FieldId, FieldPrimitive, FieldTy};
13+
pub use field::{Field, FieldId, FieldName, FieldPrimitive, FieldTy};
1414

1515
mod fk;
1616
pub use fk::{ForeignKey, ForeignKeyField};

crates/toasty-core/src/schema/app/field.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ pub struct Field {
1111
pub id: FieldId,
1212

1313
/// The field name
14-
pub name: String,
14+
pub name: FieldName,
1515

1616
/// Primitive, relation, composite, ...
1717
pub ty: FieldTy,
@@ -35,6 +35,18 @@ pub struct FieldId {
3535
pub index: usize,
3636
}
3737

38+
#[derive(Debug, Clone)]
39+
pub struct FieldName {
40+
pub app_name: String,
41+
pub storage_name: Option<String>,
42+
}
43+
44+
impl FieldName {
45+
pub fn storage_name(&self) -> &str {
46+
self.storage_name.as_ref().unwrap_or(&self.app_name)
47+
}
48+
}
49+
3850
#[derive(Clone)]
3951
pub enum FieldTy {
4052
Primitive(FieldPrimitive),
@@ -50,7 +62,7 @@ impl Field {
5062
}
5163

5264
/// Gets the name.
53-
pub fn name(&self) -> &str {
65+
pub fn name(&self) -> &FieldName {
5466
&self.name
5567
}
5668

@@ -81,7 +93,7 @@ impl Field {
8193
/// Returns a fully qualified name for the field.
8294
pub fn full_name(&self, schema: &Schema) -> String {
8395
let model = schema.model(self.id.model);
84-
format!("{}::{}", model.name.upper_camel_case(), self.name)
96+
format!("{}::{}", model.name.upper_camel_case(), self.name.app_name)
8597
}
8698

8799
/// If the field is a relation, return the relation's target ModelId.

crates/toasty-core/src/schema/app/model.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,13 @@ impl Model {
4343
}
4444

4545
pub fn field_by_name(&self, name: &str) -> Option<&Field> {
46-
self.fields.iter().find(|field| field.name == name)
46+
self.fields.iter().find(|field| field.name.app_name == name)
4747
}
4848

4949
pub fn field_by_name_mut(&mut self, name: &str) -> Option<&mut Field> {
50-
self.fields.iter_mut().find(|field| field.name == name)
50+
self.fields
51+
.iter_mut()
52+
.find(|field| field.name.app_name == name)
5153
}
5254

5355
pub fn find_by_id(&self, mut input: impl stmt::Input) -> stmt::Query {

crates/toasty-core/src/schema/app/schema.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ impl Builder {
103103
panic!(
104104
"no relation pair for {}::{}",
105105
model.name.upper_camel_case(),
106-
model.fields[index].name
106+
model.fields[index].name.app_name
107107
);
108108
}
109109
};

0 commit comments

Comments
 (0)