Brendonovich / prisma-client-rust

Type-safe database access for Rust
https://prisma.brendonovich.dev
Apache License 2.0
1.76k stars 104 forks source link

Use proc macros for include/select #250

Closed tbillington closed 11 months ago

tbillington commented 1 year ago

Because the generated include! and select! macros are output on a single line, errors that occur when using the macros cause sub-optimal error messages.

It looks "okay" in github because github doesn't wrap, but in a terminal it's a giant wall of the macro code :)

error snippet (scroll horizontally to see full error) ``` > cargo c Blocking waiting for file lock on build directory Compiling schema v0.1.0 (/Users/choc/code/prisma-rust-fun/schema) Checking prisma-rust-fun v0.1.0 (/Users/choc/code/prisma-rust-fun) error[E0433]: failed to resolve: `crate` in paths can only be used in start position --> src/tick.rs:28:22 | 28 | .include(cultivator::include!({ mission })) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `crate` in paths can only be used in start position | = note: this error originates in the macro `cultivator::include` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0412]: cannot find type `Data` in this scope --> src/tick.rs:28:22 | 28 | .include(cultivator::include!({ mission })) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: this error originates in the macro `cultivator::include` (in Nightly builds, run with -Z macro-backtrace for more info) help: you might have meant to use the associated type --> /Users/choc/code/prisma-rust-fun/schema/src/schema.rs:2752:1937 | 275| macro_rules ! _include_cultivator { ($ (($ ($ func_arg : ident : $ func_arg_ty : ty) , +) =>) ? $ module_name : ident { $ ($ field : ident $ (($ ($ filters : tt) +) $ (. $ arg : ident ($ ($ arg_params : tt) *)) *) ? $ (: $ selection_mode : ident { $ ($ selections : tt) + }) ?) + }) => { # [allow (warnings)] pub mod $ module_name { $ crate :: crate :: schema :: cultivator :: include ! (@ definitions ; $ module_name ; $ ($ field $ (($ ($ filters) +) $ (. $ arg ($ ($ arg_params) *)) *) ? $ (: $ selection_mode { $ ($ selections) + }) ?) +) ; use super :: * ; pub struct Selection (Vec < :: prisma_client_rust :: Selection >) ; impl :: prisma_client_rust :: IncludeType for Selection { type Data = Data ; type ModelData = $ crate :: crate :: schema :: cultivator :: Data ; fn to_selections (self) -> Vec < :: prisma_client_rust :: Selection > { self . 0 } } pub fn include ($ ($ ($ func_arg : $ func_arg_ty) , +) ?) -> Selection { Selection ([$ crate :: crate :: schema :: cultivator :: include ! (@ selections_to_params ; : include { $ ($ field $ (($ ($ filters) +) $ (. $ arg ($ ($ arg_params) *)) *) ? $ (: $ selection_mode { $ ($ selections) + }) ?) + }) . into_iter () . map (| p | p . to_selection ()) . collect :: < Vec < _ >> () , < $ crate :: crate :: schema :: cultivator :: Types as :: prisma_client_rust :: ModelTypes > :: scalar_selections ()] . into_iter () . flatten () . collect :: < Vec < _ >> ()) } } } ; ({ $ ($ field : ident $ (($ ($ filters : tt) +) $ (. $ arg : ident ($ ($ arg_params : tt) *)) *) ? $ (: $ selection_mode : ident { $ ($ selections : tt) + }) ?) + }) => { { $ crate :: crate :: schema :: cultivator :: include ! (@ definitions ; ; $ ($ field $ (($ ($ filters) +) $ (. $ arg ($ ($ arg_params) *)) *) ? $ (: $ selection_mode { $ ($ selections) + }) ?) +) ; pub struct Selection (Vec < :: prisma_client_rust :: Selection >) ; impl :: prisma_client_rust :: IncludeType for Selection { type Data = Self::Data ; type ModelData = $ crate :: crate :: schema :: cultivator :: Data ; fn to_selections (self) -> Vec < :: prisma_client_rust :: Selection > { self . 0 } } Selection ([$ crate :: crate :: schema :: cultivator :: include ! (@ selections_to_params ; : include { $ ($ field $ (($ ($ filters) +) $ (. $ arg ($ ($ arg_params) *)) *) ? $ (: $ selection_mode { $ ($ selections) + }) ?) + }) . into_iter () . map (| p | p . to_selection ()) . collect :: < Vec < _ >> () , < $ crate :: crate :: schema :: cultivator :: Types as :: prisma_client_rust :: ModelTypes > :: scalar_selections ()] . into_iter () . flatten () . collect :: < Vec < _ >> ()) } } ; (@ definitions ; $ ($ module_name : ident) ? ; $ ($ field : ident $ (($ ($ filters : tt) +) $ (. $ arg : ident ($ ($ arg_params : tt) *)) *) ? $ (: $ selection_mode : ident { $ ($ selections : tt) + }) ?) +) => { # [allow (warnings)] enum Fields { sect , mission } # [allow (warnings)] impl Fields { fn selections () { $ (let _ = Fields :: $ field ;) + } } # [allow (warnings)] # [derive (std :: fmt :: Debug , Clone)] pub struct Data { pub id : String , pub created_at : :: prisma_client_rust :: chrono :: DateTime < :: prisma_client_rust :: chrono :: FixedOffset , > , pub updated_at : :: prisma_client_rust :: chrono :: DateTime < :: prisma_client_rust :: chrono :: FixedOffset , > , pub sect_id : String , pub mission_id : Option < String > , $ (pub $ field : $ crate :: crate :: schema :: cultivator :: include ! (@ field_type ; $ field $ (: $ selection_mode { $ ($ selections) + }) ?) ,) + } impl :: serde :: Serialize for Data { fn serialize < S > (& self , serializer : S) -> Result < S :: Ok , S :: Error > where S : :: serde :: Serializer , { use :: serde :: ser :: SerializeStruct ; let mut state = serializer . serialize_struct ("Data" , [$ (stringify ! ($ field) ,) + stringify ! (id) , stringify ! (created_at) , stringify ! (updated_at) , stringify ! (sect_id) , stringify ! (mission_id)] . len ()) ? ; $ (state . serialize_field ($ crate :: crate :: schema :: cultivator :: include ! (@ field_serde_name ; $ field) , & self . $ field) ? ;) * state . serialize_field ($ crate :: crate :: schema :: cultivator :: include ! (@ field_serde_name ; id) , & self . id) ? ; state . serialize_field ($ crate :: crate :: schema :: cultivator :: include ! (@ field_serde_name ; created_at) , & self . created_at) ? ; state . serialize_field ($ crate :: crate :: schema :: cultivator :: include ! (@ field_serde_name ; updated_at) , & self . updated_at) ? ; state . serialize_field ($ crate :: crate :: schema :: cultivator :: include ! (@ field_serde_name ; sect_id) , & self . sect_id) ? ; state . serialize_field ($ crate :: crate :: schema :: cultivator :: include ! (@ field_serde_name ; mission_id) , & self . mission_id) ? ; state . end () } } impl < 'de > :: serde :: Deserialize < 'de > for Data { fn deserialize < D > (deserializer : D) -> Result < Self , D :: Error > where D : :: serde :: Deserializer < 'de > , { # [allow (warnings)] enum Field { $ ($ field) , + , id , created_at , updated_at , sect_id , mission_id } impl < 'de > :: serde :: Deserialize < 'de > for Field { fn deserialize < D > (deserializer : D) -> Result < Field , D :: Error > where D : :: serde :: Deserializer < 'de > , { struct FieldVisitor ; impl < 'de > :: serde :: de :: Visitor < 'de > for FieldVisitor { type Value = Field ; fn expecting (& self , formatter : & mut :: std :: fmt :: Formatter) -> :: std :: fmt :: Result { formatter . write_str (concat ! ($ ($ crate :: crate :: schema :: cultivator :: include ! (@ field_serde_name ; $ field) , ", ") , + , $ crate :: crate :: schema :: cultivator :: include ! (@ field_serde_name ; id) , ", " , $ crate :: crate :: schema :: cultivator :: include ! (@ field_serde_name ; created_at) , ", " , $ crate :: crate :: schema :: cultivator :: include ! (@ field_serde_name ; updated_at) , ", " , $ crate :: crate :: schema :: cultivator :: include ! (@ field_serde_name ; sect_id) , ", " , $ crate :: crate :: schema :: cultivator :: include ! (@ field_serde_name ; mission_id) , ", ")) } fn visit_str < E > (self , value : & str) -> Result < Field , E > where E : :: serde :: de :: Error , { match value { $ ($ crate :: crate :: schema :: cultivator :: include ! (@ field_serde_name ; $ field) => Ok (Field :: $ field)) , * , $ crate :: crate :: schema :: cultivator :: include ! (@ field_serde_name ; id) => Ok (Field :: id) , $ crate :: crate :: schema :: cultivator :: include ! (@ field_serde_name ; created_at) => Ok (Field :: created_at) , $ crate :: crate :: schema :: cultivator :: include ! (@ field_serde_name ; updated_at) => Ok (Field :: updated_at) , $ crate :: crate :: schema :: cultivator :: include ! (@ field_serde_name ; sect_id) => Ok (Field :: sect_id) , $ crate :: crate :: schema :: cultivator :: include ! (@ field_serde_name ; mission_id) => Ok (Field :: mission_id) , _ => Err (:: serde :: de :: Error :: unknown_field (value , FIELDS)) , } } } deserializer . deserialize_identifier (FieldVisitor) } } struct DataVisitor ; impl < 'de > :: serde :: de :: Visitor < 'de > for DataVisitor { type Value = Data ; fn expecting (& self , formatter : & mut std :: fmt :: Formatter) -> std :: fmt :: Result { formatter . write_str ("struct Data") } fn visit_map < V > (self , mut map : V) -> Result < Data , V :: Error > where V : :: serde :: de :: MapAccess < 'de > , { $ (let mut $ field = None ;) * let mut id = None ; let mut created_at = None ; let mut updated_at = None ; let mut sect_id = None ; let mut mission_id = None ; while let Some (key) = map . next_key () ? { match key { Field :: id => { if id . is_some () { return Err (:: serde :: de :: Error :: duplicate_field ($ crate :: crate :: schema :: cultivator :: include ! (@ field_serde_name ; id))) ; } id = Some (map . next_value () ?) ; } Field :: created_at => { if created_at . is_some () { return Err (:: serde :: de :: Error :: duplicate_field ($ crate :: crate :: schema :: cultivator :: include ! (@ field_serde_name ; created_at))) ; } created_at = Some (map . next_value () ?) ; } Field :: updated_at => { if updated_at . is_some () { return Err (:: serde :: de :: Error :: duplicate_field ($ crate :: crate :: schema :: cultivator :: include ! (@ field_serde_name ; updated_at))) ; } updated_at = Some (map . next_value () ?) ; } Field :: sect_id => { if sect_id . is_some () { return Err (:: serde :: de :: Error :: duplicate_field ($ crate :: crate :: schema :: cultivator :: include ! (@ field_serde_name ; sect_id))) ; } sect_id = Some (map . next_value () ?) ; } Field :: mission_id => { if mission_id . is_some () { return Err (:: serde :: de :: Error :: duplicate_field ($ crate :: crate :: schema :: cultivator :: include ! (@ field_serde_name ; mission_id))) ; } mission_id = Some (map . next_value () ?) ; } $ (Field :: $ field => { if $ field . is_some () { return Err (:: serde :: de :: Error :: duplicate_field ($ crate :: crate :: schema :: cultivator :: include ! (@ field_serde_name ; $ field))) ; } $ field = Some (map . next_value () ?) ; }) * } } $ (let $ field = $ field . ok_or_else (|| serde :: de :: Error :: missing_field ($ crate :: crate :: schema :: cultivator :: include ! (@ field_serde_name ; $ field))) ? ;) * let id = id . ok_or_else (|| serde :: de :: Error :: missing_field ($ crate :: crate :: schema :: cultivator :: include ! (@ field_serde_name ; id))) ? ; let created_at = created_at . ok_or_else (|| serde :: de :: Error :: missing_field ($ crate :: crate :: schema :: cultivator :: include ! (@ field_serde_name ; created_at))) ? ; let updated_at = updated_at . ok_or_else (|| serde :: de :: Error :: missing_field ($ crate :: crate :: schema :: cultivator :: include ! (@ field_serde_name ; updated_at))) ? ; let sect_id = sect_id . ok_or_else (|| serde :: de :: Error :: missing_field ($ crate :: crate :: schema :: cultivator :: include ! (@ field_serde_name ; sect_id))) ? ; let mission_id = mission_id . ok_or_else (|| serde :: de :: Error :: missing_field ($ crate :: crate :: schema :: cultivator :: include ! (@ field_serde_name ; mission_id))) ? ; Ok (Data { id , created_at , updated_at , sect_id , mission_id , $ ($ field) , * }) } } const FIELDS : & 'static [& 'static str] = & ["id" , "createdAt" , "updatedAt" , "sect" , "sectId" , "mission" , "missionId"] ; deserializer . deserialize_struct ("Data" , FIELDS , DataVisitor) } } $ ($ (pub mod $ field { $ crate :: crate :: schema :: cultivator :: $ selection_mode ! (@ field_module ; $ field : $ selection_mode { $ ($ selections) + }) ; }) ?) + } ; (@ field_type ; sect : $ selection_mode : ident { $ ($ selections : tt) + }) => { sect :: Data } ; (@ field_type ; sect) => { crate :: crate :: schema :: sect :: Data } ; (@ field_type ; mission : $ selection_mode : ident { $ ($ selections : tt) + }) => { Option < mission :: Data > } ; (@ field_type ; mission) => { Option < crate :: crate :: schema :: mission :: Data > } ; (@ field_type ; $ field : ident $ ($ tokens : tt) *) => { compile_error ! (stringify ! (Cannot include nonexistent relation $ field on model "Cultivator" , available relations are "sect, mission")) } ; (@ field_module ; sect : $ selection_mode : ident { $ ($ selections : tt) + }) => { $ crate :: crate :: schema :: sect :: include ! (@ definitions ; ; $ ($ selections) +) ; } ; (@ field_module ; mission : $ selection_mode : ident { $ ($ selections : tt) + }) => { $ crate :: crate :: schema :: mission :: include ! (@ definitions ; ; $ ($ selections) +) ; } ; (@ field_module ; $ ($ tokens : tt) *) => { } ; (@ selection_field_to_selection_param ; sect $ (($ ($ filters : tt) +) $ (. $ arg : ident ($ ($ arg_params : tt) *)) *) ? : $ selection_mode : ident { $ ($ selections : tt) + }) => { { Into :: < $ crate :: crate :: schema :: cultivator :: IncludeParam > :: into ($ crate :: crate :: schema :: cultivator :: sect :: Include :: $ selection_mode ($ crate :: crate :: schema :: sect :: select ! (@ selections_to_params ; : $ selection_mode { $ ($ selections) + }) . into_iter () . collect ())) } } ; (@ selection_field_to_selection_param ; sect $ (($ ($ filters : tt) +) $ (. $ arg : ident ($ ($ arg_params : tt) *)) *) ?) => { { Into :: < $ crate :: crate :: schema :: cultivator :: IncludeParam > :: into ($ crate :: crate :: schema :: cultivator :: sect :: Include :: Fetch) } } ; (@ selection_field_to_selection_param ; mission $ (($ ($ filters : tt) +) $ (. $ arg : ident ($ ($ arg_params : tt) *)) *) ? : $ selection_mode : ident { $ ($ selections : tt) + }) => { { Into :: < $ crate :: crate :: schema :: cultivator :: IncludeParam > :: into ($ crate :: crate :: schema :: cultivator :: mission :: Include :: $ selection_mode ($ crate :: crate :: schema :: mission :: select ! (@ selections_to_params ; : $ selection_mode { $ ($ selections) + }) . into_iter () . collect ())) } } ; (@ selection_field_to_selection_param ; mission $ (($ ($ filters : tt) +) $ (. $ arg : ident ($ ($ arg_params : tt) *)) *) ?) => { { Into :: < $ crate :: crate :: schema :: cultivator :: IncludeParam > :: into ($ crate :: crate :: schema :: cultivator :: mission :: Include :: Fetch) } } ; (@ selection_field_to_selection_param ; $ ($ tokens : tt) *) => { compile_error ! (stringify ! ($ ($ tokens) *)) } ; (@ selections_to_params ; : $ macro_name : ident { $ ($ field : ident $ (($ ($ filters : tt) +) $ (. $ arg : ident ($ ($ arg_params : tt) *)) *) ? $ (: $ selection_mode : ident { $ ($ selections : tt) + }) ?) + }) => { [$ ($ crate :: crate :: schema :: cultivator :: $ macro_name ! (@ selection_field_to_selection_param ; $ field $ (($ ($ filters) +) $ (. $ arg ($ ($ arg_params) *)) *) ? $ (: $ selection_mode { $ ($ selections) + }) ?) ,) +] } ; (@ filters_to_args ;) => { vec ! [] } ; (@ filters_to_args ; $ ($ t : tt) *) => { $ ($ t) * } ; (@ field_serde_name ; id) => { "id" } ; (@ field_serde_name ; created_at) => { "createdAt" } ; (@ field_serde_name ; updated_at) => { "updatedAt" } ; (@ field_serde_name ; sect) => { "sect" } ; (@ field_serde_name ; sect_id) => { "sectId" } ; (@ field_serde_name ; mission) => { "mission" } ; (@ field_serde_name ; mission_id) => { "missionId" } ; } | ~~~~~~~~~~ help: consider importing one of these items | 1 | use async_graphql::Data; | 1 | use crate::tick::cultivator::Data; | 1 | use crate::tick::mission::Data; | 1 | use crate::tick::user::Data; | and 6 other candidates warning: unused import: `mission` --> src/tick.rs:4:26 | 4 | use schema::{cultivator, mission, user, PrismaClient}; | ^^^^^^^ | = note: `#[warn(unused_imports)]` on by default Some errors have detailed explanations: E0412, E0433. For more information about an error, try `rustc --explain E0412`. warning: `prisma-rust-fun` (bin "prisma-rust-fun") generated 1 warning error: could not compile `prisma-rust-fun` due to 5 previous errors; 1 warning emitted ```

Unfortunately I can't use rustfmt to format it because it gives up on lines over a certain length.

Brendonovich commented 1 year ago

With the current implementation of include and select, this is a tricky issue to address. Splitting over multiple lines isn't possible since quote! doesn't include whitespace, and I'm not sure rustfmt would even work since it's not always great at formatting macro code.

In your example, I don't think there's a good way to alleviate the terminal spam since it's a result of an invalid configuration ( which I addressed in Discord). But for errors that are syntactic, there's definitely some extra error handling I could add. Take this for example, where non_existent isn't actually a field on FilePath.

file_path::select!({ non_existent })

The correct error is produced.

error: Cannot include nonexistent relation non_existent on model "FilePath",
       available relations are
       "id, is_dir, cas_id, ..."

But the same terminal spam is produced from some other branch of the macro.

error: no rules expected the token `non_existent`
     --> core/src/prisma.rs:14116:3432
      |
14116 |     macro_rules ! _select_file_path ...

This could definitely be worked on.

Brendonovich commented 1 year ago

ok maybe proc macros are a really good idea

image

damn even rustfmt can work

image
tbillington commented 1 year ago

😍 nice!