├── .gitignore ├── postgres-from-row-derive ├── Cargo.toml └── src │ └── lib.rs ├── tests └── integration.rs ├── src └── lib.rs ├── LICENSE ├── Cargo.toml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/Cargo.lock 3 | -------------------------------------------------------------------------------- /postgres-from-row-derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "postgres-from-row-derive" 3 | description = "Internal proc-macro crate for postgres-from-row" 4 | version.workspace = true 5 | authors.workspace = true 6 | edition.workspace = true 7 | repository.workspace = true 8 | homepage.workspace = true 9 | license-file.workspace = true 10 | keywords.workspace = true 11 | categories.workspace = true 12 | 13 | [lib] 14 | proc-macro = true 15 | 16 | [dependencies] 17 | quote = "1.0.28" 18 | syn = "2.0.17" 19 | darling = "0.20.1" 20 | proc-macro2 = "1.0.59" 21 | -------------------------------------------------------------------------------- /tests/integration.rs: -------------------------------------------------------------------------------- 1 | use postgres_from_row::FromRow; 2 | use tokio_postgres::Row; 3 | 4 | #[derive(FromRow)] 5 | #[allow(dead_code)] 6 | pub struct Todo { 7 | todo_id: i32, 8 | text: String, 9 | #[from_row(flatten)] 10 | user: User, 11 | } 12 | 13 | #[derive(FromRow)] 14 | #[allow(dead_code)] 15 | pub struct User { 16 | user_id: i32, 17 | } 18 | 19 | #[allow(dead_code)] 20 | fn from_row(row: &Row) { 21 | let _ = Todo::from_row(row); 22 | let _ = Todo::try_from_row(row).unwrap(); 23 | 24 | let _ = User::from_row(row); 25 | let _ = Todo::try_from_row(row).unwrap(); 26 | } 27 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | #![doc = include_str!("../README.md")] 3 | 4 | pub use postgres_from_row_derive::FromRow; 5 | pub use tokio_postgres; 6 | 7 | /// A trait that allows mapping rows from either [postgres]() or [tokio-postgres](), to other types. 8 | pub trait FromRow: Sized { 9 | /// Performce the conversion 10 | /// 11 | /// # Panics 12 | /// 13 | /// panics if the row does not contain the expected column names. 14 | fn from_row(row: &tokio_postgres::Row) -> Self; 15 | 16 | /// Try's to perform the conversion. 17 | /// 18 | /// Will return an error if the row does not contain the expected column names. 19 | fn try_from_row(row: &tokio_postgres::Row) -> Result; 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Remo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "postgres-from-row" 3 | description = "Derivable trait that allows converting a postgres row to a struct" 4 | documentation = "https://docs.rs/postgres-from-row" 5 | readme = "README.md" 6 | version.workspace = true 7 | authors.workspace = true 8 | edition.workspace = true 9 | repository.workspace = true 10 | homepage.workspace = true 11 | license-file.workspace = true 12 | keywords.workspace = true 13 | categories.workspace = true 14 | 15 | [lib] 16 | doctest = false 17 | 18 | [workspace] 19 | members = ["postgres-from-row-derive"] 20 | 21 | [workspace.package] 22 | version = "0.5.2" 23 | authors = ["Remo Pas "] 24 | edition = "2021" 25 | repository = "https://github.com/remkop22/postgres-from-row" 26 | homepage = "https://github.com/remkop22/postgres-from-row" 27 | license-file = "LICENSE" 28 | keywords = ["postgres", "postgres-tokio", "postgresql", "from-row", "mapper"] 29 | categories = ["database", "parsing", "data-structures"] 30 | 31 | [workspace.dependencies] 32 | postgres-from-row-derive = { path = "postgres-from-row-derive", version = "=0.5.2" } 33 | 34 | [dependencies] 35 | tokio-postgres = { version = "0.7.8", default_features = false } 36 | postgres-from-row-derive.workspace = true 37 | 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # postgres-from-row 2 | 3 | Derive `FromRow` to generate a mapping between a struct and postgres rows. 4 | 5 | This crate is compatible with both [postgres]() and [tokio-postgres](). 6 | 7 | ```toml 8 | [dependencies] 9 | postgres_from_row = "0.5.2" 10 | ``` 11 | 12 | ## Examples 13 | ```rust 14 | use postgres_from_row::FromRow; 15 | 16 | #[derive(FromRow)] 17 | struct Todo { 18 | todo_id: i32, 19 | text: String 20 | author_id: i32, 21 | } 22 | 23 | let row = client.query_one("SELECT todo_id, text, author_id FROM todos", &[]).unwrap(); 24 | 25 | // Pass a row with the correct columns. 26 | let todo = Todo::from_row(&row); 27 | 28 | let row = client.query_one("SELECT foo FROM bar", &[]).unwrap(); 29 | 30 | // Use `try_from_row` if the operation could fail. 31 | let todo = Todo::try_from_row(&row); 32 | assert!(todo.is_err()); 33 | ``` 34 | 35 | Each field need's to implement `postgres::types::FromSql`, as this will be used to convert a 36 | single column to the specified type. If you want to override this behavior and delegate it to a 37 | nested structure that also implements `FromRow`, use `#[from_row(flatten)]`: 38 | 39 | ```rust 40 | use postgres_from_row::FromRow; 41 | 42 | #[derive(FromRow)] 43 | struct Todo { 44 | todo_id: i32, 45 | text: String, 46 | #[from_row(flatten)] 47 | author: User 48 | } 49 | 50 | #[derive(FromRow)] 51 | struct User { 52 | user_id: i32, 53 | username: String 54 | } 55 | 56 | let row = client.query_one("SELECT todo_id, text, user_id, username FROM todos t, users u WHERE t.author_id = u.user_id", &[]).unwrap(); 57 | let todo = Todo::from_row(&row); 58 | ``` 59 | 60 | If a the struct contains a field with a name that differs from the name of the sql column, you can use the `#[from_row(rename = "..")]` attribute. 61 | 62 | When a field in your struct has a type `T` that doesn't implement `FromSql` or `FromRow` but 63 | it does impement `T: From` or `T: TryFrom`, and `C` does implment `FromSql` or `FromRow` 64 | you can use `#[from_row(from = "C")]` or `#[from_row(try_from = "C")]`. This will use type `C` to extract it from the row and 65 | then finally converts it into `T`. 66 | 67 | ```rust 68 | 69 | struct Todo { 70 | // If the postgres column is named `todo_id`. 71 | #[from_row(rename = "todo_id")] 72 | id: i32, 73 | // If the postgres column is `VARCHAR`, it will be decoded to `String`, 74 | // using `FromSql` and then converted to `Vec` using `std::convert::From`. 75 | #[from_row(from = "String")] 76 | todo: Vec 77 | } 78 | 79 | ``` 80 | 81 | 82 | -------------------------------------------------------------------------------- /postgres-from-row-derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | use darling::{ast::Data, Error, FromDeriveInput, FromField, ToTokens}; 2 | use proc_macro::TokenStream; 3 | use proc_macro2::TokenStream as TokenStream2; 4 | use quote::quote; 5 | use syn::{parse_macro_input, DeriveInput, Result}; 6 | 7 | /// Calls the fallible entry point and writes any errors to the tokenstream. 8 | #[proc_macro_derive(FromRow, attributes(from_row))] 9 | pub fn derive_from_row(input: TokenStream) -> TokenStream { 10 | let derive_input = parse_macro_input!(input as DeriveInput); 11 | match try_derive_from_row(&derive_input) { 12 | Ok(result) => result, 13 | Err(err) => err.write_errors().into(), 14 | } 15 | } 16 | 17 | /// Fallible entry point for generating a `FromRow` implementation 18 | fn try_derive_from_row(input: &DeriveInput) -> std::result::Result { 19 | let from_row_derive = DeriveFromRow::from_derive_input(input)?; 20 | Ok(from_row_derive.generate()?) 21 | } 22 | 23 | /// Main struct for deriving `FromRow` for a struct. 24 | #[derive(Debug, FromDeriveInput)] 25 | #[darling( 26 | attributes(from_row), 27 | forward_attrs(allow, doc, cfg), 28 | supports(struct_named) 29 | )] 30 | struct DeriveFromRow { 31 | ident: syn::Ident, 32 | generics: syn::Generics, 33 | data: Data<(), FromRowField>, 34 | } 35 | 36 | impl DeriveFromRow { 37 | /// Validates all fields 38 | fn validate(&self) -> Result<()> { 39 | for field in self.fields() { 40 | field.validate()?; 41 | } 42 | 43 | Ok(()) 44 | } 45 | 46 | /// Generates any additional where clause predicates needed for the fields in this struct. 47 | fn predicates(&self) -> Result> { 48 | let mut predicates = Vec::new(); 49 | 50 | for field in self.fields() { 51 | field.add_predicates(&mut predicates)?; 52 | } 53 | 54 | Ok(predicates) 55 | } 56 | 57 | /// Provides a slice of this struct's fields. 58 | fn fields(&self) -> &[FromRowField] { 59 | match &self.data { 60 | Data::Struct(fields) => &fields.fields, 61 | _ => panic!("invalid shape"), 62 | } 63 | } 64 | 65 | /// Generate the `FromRow` implementation. 66 | fn generate(self) -> Result { 67 | self.validate()?; 68 | 69 | let ident = &self.ident; 70 | 71 | let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl(); 72 | let original_predicates = where_clause.clone().map(|w| &w.predicates).into_iter(); 73 | let predicates = self.predicates()?; 74 | 75 | let from_row_fields = self 76 | .fields() 77 | .iter() 78 | .map(|f| f.generate_from_row()) 79 | .collect::>>()?; 80 | 81 | let try_from_row_fields = self 82 | .fields() 83 | .iter() 84 | .map(|f| f.generate_try_from_row()) 85 | .collect::>>()?; 86 | 87 | Ok(quote! { 88 | impl #impl_generics postgres_from_row::FromRow for #ident #ty_generics where #(#original_predicates),* #(#predicates),* { 89 | 90 | fn from_row(row: &postgres_from_row::tokio_postgres::Row) -> Self { 91 | Self { 92 | #(#from_row_fields),* 93 | } 94 | } 95 | 96 | fn try_from_row(row: &postgres_from_row::tokio_postgres::Row) -> std::result::Result { 97 | Ok(Self { 98 | #(#try_from_row_fields),* 99 | }) 100 | } 101 | } 102 | } 103 | .into()) 104 | } 105 | } 106 | 107 | /// A single field inside of a struct that derives `FromRow` 108 | #[derive(Debug, FromField)] 109 | #[darling(attributes(from_row), forward_attrs(allow, doc, cfg))] 110 | struct FromRowField { 111 | /// The identifier of this field. 112 | ident: Option, 113 | /// The type specified in this field. 114 | ty: syn::Type, 115 | /// Wether to flatten this field. Flattening means calling the `FromRow` implementation 116 | /// of `self.ty` instead of extracting it directly from the row. 117 | #[darling(default)] 118 | flatten: bool, 119 | /// Optionaly use this type as the target for `FromRow` or `FromSql`, and then 120 | /// call `TryFrom::try_from` to convert it the `self.ty`. 121 | try_from: Option, 122 | /// Optionaly use this type as the target for `FromRow` or `FromSql`, and then 123 | /// call `From::from` to convert it the `self.ty`. 124 | from: Option, 125 | /// Override the name of the actual sql column instead of using `self.ident`. 126 | /// Is not compatible with `flatten` since no column is needed there. 127 | rename: Option, 128 | } 129 | 130 | impl FromRowField { 131 | /// Checks wether this field has a valid combination of attributes 132 | fn validate(&self) -> Result<()> { 133 | if self.from.is_some() && self.try_from.is_some() { 134 | return Err(Error::custom( 135 | r#"can't combine `#[from_row(from = "..")]` with `#[from_row(try_from = "..")]`"#, 136 | ) 137 | .into()); 138 | } 139 | 140 | if self.rename.is_some() && self.flatten { 141 | return Err(Error::custom( 142 | r#"can't combine `#[from_row(flatten)]` with `#[from_row(rename = "..")]`"#, 143 | ) 144 | .into()); 145 | } 146 | 147 | Ok(()) 148 | } 149 | 150 | /// Returns a tokenstream of the type that should be returned from either 151 | /// `FromRow` (when using `flatten`) or `FromSql`. 152 | fn target_ty(&self) -> Result { 153 | if let Some(from) = &self.from { 154 | Ok(from.parse()?) 155 | } else if let Some(try_from) = &self.try_from { 156 | Ok(try_from.parse()?) 157 | } else { 158 | Ok(self.ty.to_token_stream()) 159 | } 160 | } 161 | 162 | /// Returns the name that maps to the actuall sql column 163 | /// By default this is the same as the rust field name but can be overwritten by `#[from_row(rename = "..")]`. 164 | fn column_name(&self) -> String { 165 | self.rename 166 | .as_ref() 167 | .map(Clone::clone) 168 | .unwrap_or_else(|| self.ident.as_ref().unwrap().to_string()) 169 | } 170 | 171 | /// Pushes the needed where clause predicates for this field. 172 | /// 173 | /// By default this is `T: for<'__from_row_lifetime> postgres::types::FromSql<'__from_row_lifetime>`, 174 | /// when using `flatten` it's: `T: postgres_from_row::FromRow` 175 | /// and when using either `from` or `try_from` attributes it additionally pushes this bound: 176 | /// `T: std::convert::From`, where `T` is the type specified in the struct and `R` is the 177 | /// type specified in the `[try]_from` attribute. 178 | /// 179 | /// Note: '__from_row_lifetime is used here to avoid conflicts with common user-specified lifetimes like 'a 180 | fn add_predicates(&self, predicates: &mut Vec) -> Result<()> { 181 | let target_ty = &self.target_ty()?; 182 | let ty = &self.ty; 183 | 184 | predicates.push(if self.flatten { 185 | quote! (#target_ty: postgres_from_row::FromRow) 186 | } else { 187 | quote! (#target_ty: for<'__from_row_lifetime> postgres_from_row::tokio_postgres::types::FromSql<'__from_row_lifetime>) 188 | }); 189 | 190 | if self.from.is_some() { 191 | predicates.push(quote!(#ty: std::convert::From<#target_ty>)) 192 | } else if self.try_from.is_some() { 193 | let try_from = quote!(std::convert::TryFrom<#target_ty>); 194 | 195 | predicates.push(quote!(#ty: #try_from)); 196 | predicates.push(quote!(postgres_from_row::tokio_postgres::Error: std::convert::From<<#ty as #try_from>::Error>)); 197 | predicates.push(quote!(<#ty as #try_from>::Error: std::fmt::Debug)); 198 | } 199 | 200 | Ok(()) 201 | } 202 | 203 | /// Generate the line needed to retrievee this field from a row when calling `from_row`. 204 | fn generate_from_row(&self) -> Result { 205 | let ident = self.ident.as_ref().unwrap(); 206 | let column_name = self.column_name(); 207 | let field_ty = &self.ty; 208 | let target_ty = self.target_ty()?; 209 | 210 | let mut base = if self.flatten { 211 | quote!(<#target_ty as postgres_from_row::FromRow>::from_row(row)) 212 | } else { 213 | quote!(postgres_from_row::tokio_postgres::Row::get::<&str, #target_ty>(row, #column_name)) 214 | }; 215 | 216 | if self.from.is_some() { 217 | base = quote!(<#field_ty as std::convert::From<#target_ty>>::from(#base)); 218 | } else if self.try_from.is_some() { 219 | base = quote!(<#field_ty as std::convert::TryFrom<#target_ty>>::try_from(#base).expect("could not convert column")); 220 | }; 221 | 222 | Ok(quote!(#ident: #base)) 223 | } 224 | 225 | /// Generate the line needed to retrieve this field from a row when calling `try_from_row`. 226 | fn generate_try_from_row(&self) -> Result { 227 | let ident = self.ident.as_ref().unwrap(); 228 | let column_name = self.column_name(); 229 | let field_ty = &self.ty; 230 | let target_ty = self.target_ty()?; 231 | 232 | let mut base = if self.flatten { 233 | quote!(<#target_ty as postgres_from_row::FromRow>::try_from_row(row)?) 234 | } else { 235 | quote!(postgres_from_row::tokio_postgres::Row::try_get::<&str, #target_ty>(row, #column_name)?) 236 | }; 237 | 238 | if self.from.is_some() { 239 | base = quote!(<#field_ty as std::convert::From<#target_ty>>::from(#base)); 240 | } else if self.try_from.is_some() { 241 | base = quote!(<#field_ty as std::convert::TryFrom<#target_ty>>::try_from(#base)?); 242 | }; 243 | 244 | Ok(quote!(#ident: #base)) 245 | } 246 | } 247 | --------------------------------------------------------------------------------