├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── rusqlite-from-row-derive ├── Cargo.toml └── src │ └── lib.rs ├── src └── lib.rs └── tests └── integration.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rusqlite-from-row" 3 | description = "Derivable trait that allows converting a rusqlite row to a struct" 4 | documentation = "https://docs.rs/rusqlite-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 = ["rusqlite-from-row-derive"] 20 | 21 | [workspace.package] 22 | version = "0.2.4" 23 | authors = ["Remo Pas "] 24 | edition = "2021" 25 | repository = "https://github.com/remkop22/rusqlite-from-row" 26 | homepage = "https://github.com/remkop22/rusqlite-from-row" 27 | license-file = "LICENSE" 28 | keywords = ["rusqlite", "sqlite", "from-row", "mapper"] 29 | categories = ["database", "parsing", "data-structures"] 30 | 31 | [workspace.dependencies] 32 | rusqlite-from-row-derive = { path = "rusqlite-from-row-derive", version = "=0.2.4" } 33 | 34 | [dependencies] 35 | rusqlite-from-row-derive.workspace = true 36 | rusqlite = ">=0.27,<=0.31" 37 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rusqlite-from-row 2 | 3 | Derive `FromRow` to generate a mapping between a struct and rusqlite rows. 4 | 5 | ```toml 6 | [dependencies] 7 | rusqlite_from_row = "0.2.4" 8 | ``` 9 | 10 | ## Usage 11 | 12 | Derive `FromRow` and execute a query that selects columns with the same names and types. 13 | 14 | ```rust 15 | use rusqlite_from_row::FromRow; 16 | 17 | #[derive(FromRow)] 18 | struct Todo { 19 | todo_id: i32, 20 | text: String, 21 | author_id: i32, 22 | } 23 | 24 | let row = connection.query_row("SELECT todo_id, text, author_id FROM todos", [], Todo::try_from_row).unwrap(); 25 | ``` 26 | 27 | ### Nesting, Joins and Flattening 28 | 29 | You might want to represent a join between two tables as nested structs. This is possible using the `#[from_row(flatten)]` on the nested field. 30 | This will delegate the creation of that field to `FromRow::from_row` with the same row, instead of to `FromSql`. 31 | 32 | Because tables might have naming collisions when joining them, you can specify a `prefix = ".."` to retrieve the columns uniquely. This prefix should match the prefix you specify when renaming the column in a select, like `select as `. Nested prefixing is supported. 33 | 34 | One can also use the `#[from_row(prefix)]` without a value. In this case the field name following a underscore will be used. 35 | 36 | Outer joins can be supported by wrapping the flattened type in an `Option`. The `FromRow` implementation of `Option` will still require all columns to present, but will produce a `None` if all the columns are an SQL `null` value. 37 | 38 | ```rust 39 | use rusqlite_from_row::FromRow; 40 | 41 | #[derive(FromRow)] 42 | struct Todo { 43 | id: i32, 44 | name: String, 45 | text: String, 46 | #[from_row(flatten, prefix = "user_")] 47 | author: User 48 | #[from_row(flatten, prefix)] 49 | editor: User 50 | } 51 | 52 | #[derive(FromRow)] 53 | struct User { 54 | id: i32, 55 | name: String 56 | } 57 | 58 | // Rename all `User` fields to have `user_` or `editor_` prefixes. 59 | let row = client 60 | .query_one( 61 | " 62 | SELECT 63 | t.id, 64 | t.name, 65 | t.text, 66 | u.name as user_name, 67 | u.id as user_id, 68 | e.name as editor_name, 69 | e.id as editor_id 70 | FROM 71 | todos t 72 | JOIN 73 | user u ON t.author_id = u.id 74 | JOIN 75 | user e ON t.editor_id = e.id 76 | ", 77 | [], 78 | Todo::try_from_row, 79 | ) 80 | .unwrap(); 81 | ``` 82 | 83 | ### Renaming and Converting 84 | 85 | If a struct contains a field with a name that differs from the name of the sql column, you can use the `#[from_row(rename = "..")]` attribute. 86 | 87 | Normally if you have a custom wrapper type like `struct DbId(i32)`, you'd need to implement `FromSql` in order to use it in a query. A simple alternative is to implement `From` or `TryFrom` for `DbId` and annotating a field with `#[from_row(from = "i32")]` or `#[from_row(try_from = "i32")]`. 88 | 89 | This will delegate the sql conversion to `` and subsequently convert it to `DbId`. 90 | 91 | ```rust 92 | 93 | use rusqlite_from_row::FromRow; 94 | 95 | struct DbId(i32); 96 | 97 | impl From for DbId { 98 | fn from(value: i32) -> Self { 99 | Self(value) 100 | } 101 | } 102 | 103 | #[derive(FromRow)] 104 | struct Todo { 105 | // If the sqlite column is named `todo_id`. 106 | #[from_row(rename = "todo_id", from = "i32")] 107 | id: i32, 108 | // If the sqlite column is `TEXT`, it will be decoded to `String`, 109 | // using `FromSql` and then converted to `Vec` using `std::convert::From`. 110 | #[from_row(from = "String")] 111 | todo: Vec 112 | } 113 | ``` 114 | 115 | 116 | -------------------------------------------------------------------------------- /rusqlite-from-row-derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rusqlite-from-row-derive" 3 | description = "Internal proc-macro crate for rusqlite-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.36" 18 | syn = "2.0.60" 19 | proc-macro2 = "1.0.81" 20 | -------------------------------------------------------------------------------- /rusqlite-from-row-derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use proc_macro::TokenStream; 4 | use proc_macro2::TokenStream as TokenStream2; 5 | use quote::quote; 6 | use syn::{ 7 | parse_macro_input, parse_str, spanned::Spanned, Attribute, Data, DataStruct, DeriveInput, 8 | Error, ExprPath, Field, Fields, LitStr, Result, Type, 9 | }; 10 | 11 | /// Calls the fallible entry point and writes any errors to the tokenstream. 12 | #[proc_macro_derive(FromRow, attributes(from_row))] 13 | pub fn derive_from_row(input: TokenStream) -> TokenStream { 14 | let derive_input = parse_macro_input!(input as DeriveInput); 15 | 16 | try_derive_from_row(derive_input) 17 | .unwrap_or_else(Error::into_compile_error) 18 | .into() 19 | } 20 | 21 | /// Fallible entry point for generating a `FromRow` implementation 22 | fn try_derive_from_row(input: DeriveInput) -> Result { 23 | let from_row_derive = DeriveFromRow::parse(input)?; 24 | 25 | Ok(from_row_derive.generate()) 26 | } 27 | 28 | /// Main struct for deriving `FromRow` for a struct. 29 | struct DeriveFromRow { 30 | ident: syn::Ident, 31 | generics: syn::Generics, 32 | data: Vec, 33 | } 34 | 35 | impl DeriveFromRow { 36 | fn parse(input: DeriveInput) -> Result { 37 | let DeriveInput { 38 | ident, 39 | generics, 40 | data: 41 | Data::Struct(DataStruct { 42 | fields: Fields::Named(fields), 43 | .. 44 | }), 45 | .. 46 | } = input 47 | else { 48 | return Err(Error::new( 49 | input.span(), 50 | "expected struct with named fields", 51 | )); 52 | }; 53 | 54 | let mut data = Vec::new(); 55 | 56 | for field in fields.named { 57 | data.push(FromRowField::parse(field)?); 58 | } 59 | 60 | Ok(Self { 61 | ident, 62 | generics, 63 | data, 64 | }) 65 | } 66 | 67 | fn predicates(&self) -> Vec { 68 | let mut predicates = Vec::new(); 69 | 70 | for field in &self.data { 71 | field.add_predicates(&mut predicates); 72 | } 73 | 74 | predicates 75 | } 76 | 77 | /// Generate the `FromRow` implementation. 78 | fn generate(self) -> TokenStream2 { 79 | let ident = &self.ident; 80 | 81 | let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl(); 82 | let original_predicates = where_clause.map(|w| &w.predicates).into_iter(); 83 | let predicates = self.predicates(); 84 | 85 | let is_all_null_fields = self.data.iter().filter_map(|f| f.generate_is_all_null()); 86 | 87 | let try_from_row_fields = self.data.iter().map(|f| f.generate_try_from_row()); 88 | 89 | quote! { 90 | impl #impl_generics rusqlite_from_row::FromRow for #ident #ty_generics where #(#original_predicates),* #(#predicates),* { 91 | fn try_from_row_prefixed( 92 | row: &rusqlite_from_row::rusqlite::Row, 93 | prefix: Option<&str> 94 | ) -> std::result::Result { 95 | Ok(Self { 96 | #(#try_from_row_fields),* 97 | }) 98 | } 99 | 100 | fn is_all_null( 101 | row: &rusqlite_from_row::rusqlite::Row, 102 | prefix: Option<&str> 103 | ) -> std::result::Result { 104 | Ok(#(#is_all_null_fields)&&*) 105 | } 106 | } 107 | } 108 | } 109 | } 110 | 111 | /// A single field inside of a struct that derives `FromRow` 112 | struct FromRowField { 113 | /// The identifier of this field. 114 | ident: syn::Ident, 115 | /// The type specified in this field. 116 | ty: syn::Type, 117 | attrs: FromRowAttrs, 118 | } 119 | 120 | impl FromRowField { 121 | pub fn parse(field: Field) -> Result { 122 | let attrs = FromRowAttrs::parse(field.attrs)?; 123 | 124 | Ok(Self { 125 | ident: field.ident.expect("should be named"), 126 | ty: field.ty, 127 | attrs, 128 | }) 129 | } 130 | 131 | /// Returns a tokenstream of the type that should be returned from either 132 | /// `FromRow` (when using `flatten`) or `FromSql`. 133 | fn target_ty(&self) -> Option<&Type> { 134 | match &self.attrs { 135 | FromRowAttrs::Field { 136 | convert: Some(Convert::From(ty) | Convert::TryFrom(ty)), 137 | .. 138 | } => Some(ty), 139 | FromRowAttrs::Field { 140 | convert: Some(Convert::FromFn(_)), 141 | .. 142 | } => None, 143 | _ => Some(&self.ty), 144 | } 145 | } 146 | 147 | /// Returns the name that maps to the actuall sql column 148 | /// By default this is the same as the rust field name but can be overwritten by `#[from_row(rename = "..")]`. 149 | fn column_name(&self) -> Cow { 150 | match &self.attrs { 151 | FromRowAttrs::Field { 152 | rename: Some(name), .. 153 | } => name.as_str().into(), 154 | _ => self.ident.to_string().into(), 155 | } 156 | } 157 | 158 | /// Pushes the needed where clause predicates for this field. 159 | /// 160 | /// By default this is `T: rusqlite::types::FromSql`, 161 | /// when using `flatten` it's: `T: rusqlite_from_row::FromRow` 162 | /// and when using either `from` or `try_from` attributes it additionally pushes this bound: 163 | /// `T: std::convert::From`, where `T` is the type specified in the struct and `R` is the 164 | /// type specified in the `[try]_from` attribute. 165 | fn add_predicates(&self, predicates: &mut Vec) { 166 | match &self.attrs { 167 | FromRowAttrs::Field { 168 | default, convert, .. 169 | } => { 170 | let target_ty = self.target_ty(); 171 | let ty = &self.ty; 172 | 173 | if let Some(target_ty) = target_ty { 174 | predicates 175 | .push(quote! (#target_ty: rusqlite_from_row::rusqlite::types::FromSql)); 176 | 177 | if *default { 178 | predicates.push(quote! (#target_ty: ::std::default::Default)); 179 | } 180 | } 181 | 182 | match convert { 183 | Some(Convert::From(target_ty)) => { 184 | predicates.push(quote!(#target_ty: std::convert::From<#target_ty>)) 185 | } 186 | Some(Convert::TryFrom(target_ty)) => { 187 | let try_from = quote!(std::convert::TryFrom<#target_ty>); 188 | 189 | predicates.push(quote!(#ty: #try_from)); 190 | predicates.push(quote!(rusqlite_from_row::rusqlite::Error: std::convert::From<<#ty as #try_from>::Error>)); 191 | predicates.push(quote!(<#ty as #try_from>::Error: std::fmt::Debug)); 192 | } 193 | _ => {} 194 | } 195 | } 196 | FromRowAttrs::Flatten { default, .. } => { 197 | let ty = &self.ty; 198 | 199 | predicates.push(quote! (#ty: rusqlite_from_row::FromRow)); 200 | 201 | if *default { 202 | predicates.push(quote! (#ty: ::std::default::Default)); 203 | } 204 | } 205 | FromRowAttrs::Skip => { 206 | let ty = &self.ty; 207 | 208 | predicates.push(quote! (#ty: ::std::default::Default)); 209 | } 210 | } 211 | } 212 | 213 | fn generate_is_all_null(&self) -> Option { 214 | let is_all_null = match &self.attrs { 215 | FromRowAttrs::Flatten { prefix, .. } => { 216 | let ty = &self.ty; 217 | 218 | let prefix = match &prefix { 219 | Some(Prefix::Value(prefix)) => { 220 | quote!(Some(&(prefix.unwrap_or("").to_string() + #prefix))) 221 | } 222 | Some(Prefix::Field) => { 223 | let ident_str = format!("{}_", self.ident); 224 | quote!(Some(&(prefix.unwrap_or("").to_string() + #ident_str))) 225 | } 226 | None => quote!(prefix), 227 | }; 228 | 229 | quote!(<#ty as rusqlite_from_row::FromRow>::is_all_null(row, #prefix)?) 230 | } 231 | FromRowAttrs::Field { .. } => { 232 | let column_name = self.column_name(); 233 | 234 | quote! { 235 | rusqlite_from_row::rusqlite::Row::get_ref::<&str>( 236 | row, 237 | &(prefix.unwrap_or("").to_string() + #column_name) 238 | )? == rusqlite_from_row::rusqlite::types::ValueRef::Null 239 | } 240 | } 241 | FromRowAttrs::Skip => return None, 242 | }; 243 | 244 | Some(is_all_null) 245 | } 246 | 247 | /// Generate the line needed to retrieve this field from a row when calling `try_from_row`. 248 | fn generate_try_from_row(&self) -> TokenStream2 { 249 | let ident = &self.ident; 250 | let column_name = self.column_name(); 251 | let field_ty = &self.ty; 252 | 253 | let base = match &self.attrs { 254 | FromRowAttrs::Flatten { prefix, default } => { 255 | let ty = &self.ty; 256 | 257 | let prefix = match &prefix { 258 | Some(Prefix::Value(prefix)) => { 259 | quote!(Some(&(prefix.unwrap_or("").to_string() + #prefix))) 260 | } 261 | Some(Prefix::Field) => { 262 | let ident_str = format!("{}_", self.ident); 263 | quote!(Some(&(prefix.unwrap_or("").to_string() + #ident_str))) 264 | } 265 | None => quote!(prefix), 266 | }; 267 | 268 | if *default { 269 | let value = quote!( as rusqlite_from_row::FromRow>::try_from_row_prefixed(row, #prefix)?); 270 | 271 | quote! { 272 | match #value { 273 | Some(value) => value, 274 | None => <#ty as ::std::default::Default>::default(), 275 | } 276 | } 277 | } else { 278 | quote!(<#ty as rusqlite_from_row::FromRow>::try_from_row_prefixed(row, #prefix)?) 279 | } 280 | } 281 | FromRowAttrs::Field { 282 | convert, default, .. 283 | } => { 284 | let column_name = quote!(&(prefix.unwrap_or("").to_string() + #column_name)); 285 | let target_ty = self 286 | .target_ty() 287 | .cloned() 288 | .unwrap_or_else(|| parse_str("_").unwrap()); 289 | 290 | let base = if *default { 291 | quote! { 292 | match rusqlite_from_row::rusqlite::Row::get_ref::<&str>(row, #column_name)? { 293 | ::rusqlite::types::ValueRef::Null => <#target_ty as ::std::default::Default>::default(), 294 | value => <#target_ty as ::rusqlite::types::FromSql>::column_result(value)?, 295 | } 296 | } 297 | } else { 298 | quote!(rusqlite_from_row::rusqlite::Row::get::<&str, #target_ty>(row, #column_name)?) 299 | }; 300 | 301 | match convert { 302 | Some(Convert::From(_)) => { 303 | quote!(<#field_ty as std::convert::From<#target_ty>>::from(#base)) 304 | } 305 | Some(Convert::TryFrom(_)) => { 306 | quote!(<#field_ty as std::convert::TryFrom<#target_ty>>::try_from(#base)?) 307 | } 308 | Some(Convert::FromFn(func)) => { 309 | quote!(#func(#base)) 310 | } 311 | _ => base, 312 | } 313 | } 314 | FromRowAttrs::Skip => { 315 | let ty = &self.ty; 316 | 317 | quote!(<#ty as std::default::Default>::default()) 318 | } 319 | }; 320 | 321 | quote!(#ident: #base) 322 | } 323 | } 324 | 325 | enum FromRowAttrs { 326 | Flatten { 327 | prefix: Option, 328 | default: bool, 329 | }, 330 | Field { 331 | rename: Option, 332 | convert: Option, 333 | default: bool, 334 | }, 335 | Skip, 336 | } 337 | 338 | enum Convert { 339 | From(Type), 340 | TryFrom(Type), 341 | FromFn(ExprPath), 342 | } 343 | 344 | enum Prefix { 345 | Value(String), 346 | Field, 347 | } 348 | 349 | impl FromRowAttrs { 350 | fn parse(attrs: Vec) -> Result { 351 | let Some(span) = attrs.first().map(|attr| attr.span()) else { 352 | return Ok(Self::Field { 353 | rename: None, 354 | convert: None, 355 | default: false, 356 | }); 357 | }; 358 | 359 | let mut flatten = false; 360 | let mut prefix = None; 361 | let mut try_from = None; 362 | let mut from = None; 363 | let mut from_fn = None; 364 | let mut rename = None; 365 | let mut skip = false; 366 | let mut default = false; 367 | 368 | for attr in attrs { 369 | if !attr.meta.path().is_ident("from_row") { 370 | continue; 371 | } 372 | 373 | attr.parse_nested_meta(|meta| { 374 | if meta.path.is_ident("flatten") { 375 | flatten = true; 376 | } else if meta.path.is_ident("prefix") { 377 | let prefix_value = if let Ok(value) = meta.value() { 378 | Prefix::Value(value.parse::()?.value()) 379 | } else { 380 | Prefix::Field 381 | }; 382 | 383 | prefix = Some(prefix_value); 384 | } else if meta.path.is_ident("try_from") { 385 | let try_from_str: LitStr = meta.value()?.parse()?; 386 | try_from = Some(parse_str(&try_from_str.value())?); 387 | } else if meta.path.is_ident("from") { 388 | let from_str: LitStr = meta.value()?.parse()?; 389 | from = Some(parse_str(&from_str.value())?); 390 | } else if meta.path.is_ident("from_fn") { 391 | let from_fn_str: LitStr = meta.value()?.parse()?; 392 | from_fn = Some(parse_str(&from_fn_str.value())?); 393 | } else if meta.path.is_ident("rename") { 394 | let rename_str: LitStr = meta.value()?.parse()?; 395 | rename = Some(rename_str.value()); 396 | } else if meta.path.is_ident("skip") { 397 | skip = true; 398 | } else if meta.path.is_ident("default") { 399 | default = true; 400 | } 401 | 402 | Ok(()) 403 | })?; 404 | } 405 | 406 | let attrs = if skip { 407 | let other_attrs = flatten 408 | || default 409 | || prefix.is_some() 410 | || try_from.is_some() 411 | || from_fn.is_some() 412 | || from.is_some() 413 | || rename.is_some(); 414 | 415 | if other_attrs { 416 | return Err(Error::new( 417 | span, 418 | "can't combine `skip` with other attributes", 419 | )); 420 | } 421 | 422 | Self::Skip 423 | } else if flatten { 424 | if rename.is_some() || from.is_some() || try_from.is_some() || from_fn.is_some() { 425 | return Err(Error::new( 426 | span, 427 | "can't combine `skip` with other attributes", 428 | )); 429 | } 430 | 431 | Self::Flatten { default, prefix } 432 | } else { 433 | if prefix.is_some() { 434 | return Err(Error::new( 435 | span, 436 | "`prefix` attribute is only valid in combination with `flatten`", 437 | )); 438 | } 439 | 440 | let convert = match (try_from, from, from_fn) { 441 | (Some(try_from), None, None) => Some(Convert::TryFrom(try_from)), 442 | (None, Some(from), None) => Some(Convert::From(from)), 443 | (None, None, Some(from_fn)) => Some(Convert::FromFn(from_fn)), 444 | (None, None, None) => None, 445 | _ => { 446 | return Err(Error::new( 447 | span, 448 | "can't combine `try_from`, `from` or `from_fn`", 449 | )) 450 | } 451 | }; 452 | 453 | Self::Field { 454 | rename, 455 | convert, 456 | default, 457 | } 458 | }; 459 | 460 | Ok(attrs) 461 | } 462 | } 463 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | #![doc = include_str!("../README.md")] 3 | 4 | pub use rusqlite; 5 | pub use rusqlite_from_row_derive::FromRow; 6 | 7 | /// A trait that allows mapping a [`rusqlite::Row`] to other types. 8 | pub trait FromRow: Sized { 9 | /// Performs the conversion. 10 | /// 11 | /// # Panics 12 | /// 13 | /// Panics if the row does not contain the expected column names. 14 | fn from_row(row: &rusqlite::Row) -> Self { 15 | Self::from_row_prefixed(row, None) 16 | } 17 | 18 | /// Try's to perform the conversion. 19 | /// 20 | /// Will return an error if the row does not contain the expected column names. 21 | fn try_from_row(row: &rusqlite::Row) -> Result { 22 | Self::try_from_row_prefixed(row, None) 23 | } 24 | 25 | /// Perform the conversion. Each row will be extracted using it's name prefixed with 26 | /// `prefix`. 27 | /// 28 | /// # Panics 29 | /// 30 | /// Panics if the row does not contain the expected column names. 31 | fn from_row_prefixed(row: &rusqlite::Row, prefix: Option<&str>) -> Self { 32 | Self::try_from_row_prefixed(row, prefix).expect("from row failed") 33 | } 34 | 35 | /// Try's to perform the conversion. Each row will be extracted using it's name prefixed with 36 | /// `prefix`. 37 | /// 38 | /// Will return an error if the row does not contain the expected column names. 39 | fn try_from_row_prefixed( 40 | row: &rusqlite::Row, 41 | prefix: Option<&str>, 42 | ) -> Result; 43 | 44 | /// Try's to check if all the columns that are needed by this struct are sql 'null' values. 45 | /// 46 | /// Will return an error if the row does not contain the expected column names. 47 | fn is_all_null(row: &rusqlite::Row, prefix: Option<&str>) -> Result; 48 | } 49 | 50 | impl FromRow for Option { 51 | fn try_from_row_prefixed( 52 | row: &rusqlite::Row, 53 | prefix: Option<&str>, 54 | ) -> Result { 55 | if T::is_all_null(row, prefix)? { 56 | Ok(None) 57 | } else { 58 | Ok(Some(T::try_from_row_prefixed(row, prefix)?)) 59 | } 60 | } 61 | 62 | fn is_all_null(row: &rusqlite::Row, prefix: Option<&str>) -> Result { 63 | T::is_all_null(row, prefix) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/integration.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi::OsStr, marker::PhantomData, path::PathBuf}; 2 | 3 | use rusqlite::{params, Connection}; 4 | use rusqlite_from_row::FromRow; 5 | 6 | #[derive(Debug, FromRow)] 7 | pub struct Todo { 8 | id: i32, 9 | text: String, 10 | #[from_row(flatten, prefix = "author_")] 11 | author: User, 12 | #[from_row(flatten, prefix)] 13 | editor: User, 14 | #[from_row(flatten, default)] 15 | status: Status, 16 | #[from_row(default)] 17 | views: i32, 18 | #[from_row(from_fn = ">::from")] 19 | file: PathBuf, 20 | #[from_row(skip)] 21 | empty: PhantomData<()>, 22 | } 23 | 24 | #[derive(Debug, FromRow, PartialEq, Eq, Default)] 25 | pub struct Status { 26 | is_done: bool, 27 | } 28 | 29 | #[derive(Debug, FromRow)] 30 | #[allow(dead_code)] 31 | pub struct User { 32 | id: i32, 33 | name: String, 34 | #[from_row(flatten, prefix = "role_")] 35 | role: Option, 36 | } 37 | 38 | #[derive(Debug, FromRow)] 39 | pub struct Role { 40 | id: i32, 41 | kind: String, 42 | } 43 | 44 | #[test] 45 | fn from_row() { 46 | let connection = Connection::open_in_memory().unwrap(); 47 | 48 | connection 49 | .execute_batch( 50 | " 51 | 52 | CREATE TABLE role ( 53 | id INTEGER PRIMARY KEY, 54 | kind TEXT NOT NULL 55 | ); 56 | 57 | CREATE TABLE user ( 58 | id INTEGER PRIMARY KEY, 59 | name TEXT NOT NULL, 60 | role_id INTEGER NULL REFERENCES role(id) 61 | ); 62 | 63 | CREATE TABLE status ( 64 | id INTEGER PRIMARY KEY, 65 | is_done BOOL NOT NULL 66 | ); 67 | 68 | CREATE TABLE todo ( 69 | id INTEGER PRIMARY KEY, 70 | text TEXT NOT NULL, 71 | author_id INTEGER NOT NULL REFERENCES user(id), 72 | editor_id INTEGER NOT NULL REFERENCES user(id), 73 | views INTEGER NULL DEFAULT NULL, 74 | status_id INTEGER NULL REFERENCES status(id), 75 | file TEXT NOT NULL 76 | ); 77 | ", 78 | ) 79 | .unwrap(); 80 | 81 | let role_id: i32 = connection 82 | .prepare("INSERT INTO role(kind) VALUES ('admin') RETURNING id") 83 | .unwrap() 84 | .query_row(params![], |r| r.get(0)) 85 | .unwrap(); 86 | 87 | let user_ids = connection 88 | .prepare("INSERT INTO user(name, role_id) VALUES ('john', ?1), ('jack', null) RETURNING id") 89 | .unwrap() 90 | .query_map([role_id], |r| r.get(0)) 91 | .unwrap() 92 | .collect::, _>>() 93 | .unwrap(); 94 | 95 | let todo_id: i32 = connection 96 | .prepare( 97 | "INSERT INTO todo(text, author_id, editor_id, file) VALUES ('laundry', ?1, ?2, 'foo/bar.txt') RETURNING id", 98 | ) 99 | .unwrap() 100 | .query_row(params![user_ids[0], user_ids[1]], |r| r.get(0)) 101 | .unwrap(); 102 | 103 | let todo = connection 104 | .query_row( 105 | " 106 | SELECT 107 | t.id, 108 | t.text, 109 | t.views, 110 | t.file, 111 | a.id as author_id, 112 | a.name as author_name, 113 | ar.id as author_role_id, 114 | ar.kind as author_role_kind, 115 | e.id as editor_id, 116 | e.name as editor_name, 117 | er.id as editor_role_id, 118 | er.kind as editor_role_kind, 119 | st.is_done as is_done 120 | FROM 121 | todo t 122 | JOIN user a ON 123 | a.id = t.author_id 124 | LEFT JOIN role ar ON 125 | a.role_id = ar.id 126 | JOIN user e ON 127 | e.id = t.editor_id 128 | LEFT JOIN role er ON 129 | e.role_id = er.id 130 | LEFT JOIN status st ON 131 | t.status_id = st.id 132 | WHERE 133 | t.id = ?1", 134 | params![todo_id], 135 | Todo::try_from_row, 136 | ) 137 | .unwrap(); 138 | 139 | assert_eq!(todo.id, todo_id); 140 | assert_eq!(todo.text, "laundry"); 141 | assert_eq!(todo.status, Status { is_done: false }); 142 | assert_eq!(todo.views, 0); 143 | assert_eq!(todo.file.file_name(), Some(OsStr::new("bar.txt"))); 144 | } 145 | --------------------------------------------------------------------------------