@@ -17,8 +17,8 @@ use syn::parse::{Parse, ParseStream};
17
17
use syn:: punctuated:: Punctuated ;
18
18
use syn:: spanned:: Spanned ;
19
19
use syn:: {
20
- parse_macro_input, parse_quote, Data , DeriveInput , Expr , Fields , FieldsNamed , Ident , LitStr , Meta ,
21
- Token , Type ,
20
+ parse_macro_input, parse_quote, Data , DeriveInput , Expr , ExprLit , Fields , FieldsNamed , Ident , Lit ,
21
+ LitStr , Meta , MetaNameValue , Token , Type ,
22
22
} ;
23
23
24
24
#[ cfg( not( feature = "test" ) ) ]
@@ -58,6 +58,7 @@ struct Column {
58
58
name : String ,
59
59
rust_type : String ,
60
60
sql_type : & ' static str ,
61
+ sql_default : Option < String > ,
61
62
}
62
63
63
64
#[ derive( Clone , Serialize , Deserialize , Debug ) ]
@@ -67,8 +68,10 @@ struct MiniColumn {
67
68
sql_type : String ,
68
69
}
69
70
70
- static U8_ARRAY_RE : Lazy < regex:: Regex > =
71
+ static OPTION_U8_ARRAY_RE : Lazy < regex:: Regex > =
71
72
Lazy :: new ( || regex:: Regex :: new ( r"^Option < \[u8 ; \d+\] >$" ) . unwrap ( ) ) ;
73
+ static U8_ARRAY_RE : Lazy < regex:: Regex > =
74
+ Lazy :: new ( || regex:: Regex :: new ( r"^\[u8 ; \d+\]$" ) . unwrap ( ) ) ;
72
75
73
76
#[ derive( Debug ) ]
74
77
struct SelectTokens {
@@ -489,7 +492,10 @@ fn do_parse_tokens(
489
492
Content :: SingleColumn ( col) => col. column == c. name ,
490
493
_ => true ,
491
494
} {
492
- if c. sql_type == "TEXT" && c. rust_type != "Option < String >" {
495
+ if c. sql_type . starts_with ( "TEXT" )
496
+ && c. rust_type != "Option < String >"
497
+ && c. rust_type != "String"
498
+ {
493
499
Some ( format ! ( "{} AS {}__serialized" , c. name, c. name) )
494
500
} else {
495
501
Some ( c. name . clone ( ) )
@@ -600,7 +606,13 @@ fn do_parse_tokens(
600
606
. unwrap_or_else ( |_| abort_call_site ! ( "stmt_info.membersandcasters failed" ) ) ;
601
607
let row_casters = m. row_casters ;
602
608
603
- handle_row = quote ! { #content { #( #row_casters) , * } } ;
609
+ handle_row = quote ! {
610
+ #[ allow( clippy:: needless_update) ]
611
+ #content {
612
+ #( #row_casters) , * ,
613
+ ..Default :: default ( )
614
+ }
615
+ } ;
604
616
content_ty = quote ! { #content } ;
605
617
}
606
618
Content :: SingleColumn ( col) => {
@@ -769,18 +781,37 @@ fn extract_columns(fields: &FieldsNamed) -> Vec<Column> {
769
781
. named
770
782
. iter ( )
771
783
. filter_map ( |f| {
772
- // Skip (skip) fields
784
+ let mut sql_default = None ;
773
785
774
786
for attr in & f. attrs {
775
787
if attr. path ( ) . is_ident ( "turbosql" ) {
776
788
for meta in attr. parse_args_with ( Punctuated :: < Meta , Token ! [ , ] > :: parse_terminated) . unwrap ( ) {
777
- match meta {
789
+ match & meta {
778
790
Meta :: Path ( path) if path. is_ident ( "skip" ) => {
779
- // TODO: For skipped fields, Handle derive(Default) requirement better
780
- // require Option and manifest None values
781
791
return None ;
782
792
}
783
- _ => ( )
793
+ Meta :: NameValue ( MetaNameValue { path, value : Expr :: Lit ( ExprLit { lit, .. } ) , .. } )
794
+ if path. is_ident ( "sql_default" ) =>
795
+ {
796
+ match lit {
797
+ Lit :: Bool ( value) => sql_default = Some ( value. value ( ) . to_string ( ) ) ,
798
+ Lit :: Int ( token) => sql_default = Some ( token. to_string ( ) ) ,
799
+ Lit :: Float ( token) => sql_default = Some ( token. to_string ( ) ) ,
800
+ Lit :: Str ( token) => sql_default = Some ( format ! ( "'{}'" , token. value( ) ) ) ,
801
+ Lit :: ByteStr ( token) => {
802
+ use std:: fmt:: Write ;
803
+ sql_default = Some ( format ! (
804
+ "x'{}'" ,
805
+ token. value( ) . iter( ) . fold( String :: new( ) , |mut o, b| {
806
+ let _ = write!( o, "{b:02x}" ) ;
807
+ o
808
+ } )
809
+ ) )
810
+ }
811
+ _ => ( ) ,
812
+ }
813
+ }
814
+ _ => ( ) ,
784
815
}
785
816
}
786
817
}
@@ -792,50 +823,70 @@ fn extract_columns(fields: &FieldsNamed) -> Vec<Column> {
792
823
let ty = & f. ty ;
793
824
let ty_str = quote ! ( #ty) . to_string ( ) ;
794
825
795
- // TODO: have specific error messages or advice for other numeric types
796
- // specifically, sqlite cannot represent u64 integers, would be coerced to float.
797
- // https://sqlite.org/fileformat.html
798
-
799
- let sql_type = match (
826
+ let ( sql_type, default_example) = match (
800
827
name. as_str ( ) ,
801
- if U8_ARRAY_RE . is_match ( & ty_str) { "Option < [u8; _] >" } else { ty_str. as_str ( ) } ,
828
+ if OPTION_U8_ARRAY_RE . is_match ( & ty_str) {
829
+ "Option < [u8; _] >"
830
+ } else if U8_ARRAY_RE . is_match ( & ty_str) {
831
+ "[u8; _]"
832
+ } else {
833
+ ty_str. as_str ( )
834
+ } ,
802
835
) {
803
- ( "rowid" , "Option < i64 >" ) => "INTEGER PRIMARY KEY" ,
804
- ( _, "Option < i8 >" ) => "INTEGER" ,
805
- ( _, "Option < u8 >" ) => "INTEGER" ,
806
- ( _, "Option < i16 >" ) => "INTEGER" ,
807
- ( _, "Option < u16 >" ) => "INTEGER" ,
808
- ( _, "Option < i32 >" ) => "INTEGER" ,
809
- ( _, "Option < u32 >" ) => "INTEGER" ,
810
- ( _, "Option < i64 >" ) => "INTEGER" ,
836
+ ( "rowid" , "Option < i64 >" ) => ( "INTEGER PRIMARY KEY" , "NULL" ) ,
837
+ ( _, "Option < i8 >" ) => ( "INTEGER" , "0" ) ,
838
+ ( _, "i8" ) => ( "INTEGER NOT NULL" , "0" ) ,
839
+ ( _, "Option < u8 >" ) => ( "INTEGER" , "0" ) ,
840
+ ( _, "u8" ) => ( "INTEGER NOT NULL" , "0" ) ,
841
+ ( _, "Option < i16 >" ) => ( "INTEGER" , "0" ) ,
842
+ ( _, "i16" ) => ( "INTEGER NOT NULL" , "0" ) ,
843
+ ( _, "Option < u16 >" ) => ( "INTEGER" , "0" ) ,
844
+ ( _, "u16" ) => ( "INTEGER NOT NULL" , "0" ) ,
845
+ ( _, "Option < i32 >" ) => ( "INTEGER" , "0" ) ,
846
+ ( _, "i32" ) => ( "INTEGER NOT NULL" , "0" ) ,
847
+ ( _, "Option < u32 >" ) => ( "INTEGER" , "0" ) ,
848
+ ( _, "u32" ) => ( "INTEGER NOT NULL" , "0" ) ,
849
+ ( _, "Option < i64 >" ) => ( "INTEGER" , "0" ) ,
850
+ ( _, "i64" ) => ( "INTEGER NOT NULL" , "0" ) ,
811
851
( _, "Option < u64 >" ) => abort ! ( ty, SQLITE_U64_ERROR ) ,
812
- ( _, "Option < f64 >" ) => "REAL" ,
813
- ( _, "Option < f32 >" ) => "REAL" ,
814
- ( _, "Option < bool >" ) => "INTEGER" ,
815
- ( _, "Option < String >" ) => "TEXT" ,
852
+ ( _, "u64" ) => abort ! ( ty, SQLITE_U64_ERROR ) ,
853
+ ( _, "Option < f64 >" ) => ( "REAL" , "0.0" ) ,
854
+ ( _, "f64" ) => ( "REAL NOT NULL" , "0.0" ) ,
855
+ ( _, "Option < f32 >" ) => ( "REAL" , "0.0" ) ,
856
+ ( _, "f32" ) => ( "REAL NOT NULL" , "0.0" ) ,
857
+ ( _, "Option < bool >" ) => ( "INTEGER" , "false" ) ,
858
+ ( _, "bool" ) => ( "INTEGER NOT NULL" , "false" ) ,
859
+ ( _, "Option < String >" ) => ( "TEXT" , "\" \" " ) ,
860
+ ( _, "String" ) => ( "TEXT NOT NULL" , "''" ) ,
816
861
// SELECT LENGTH(blob_column) ... will be null if blob is null
817
- ( _, "Option < Blob >" ) => "BLOB" ,
818
- ( _, "Option < Vec < u8 > >" ) => "BLOB" ,
819
- ( _, "Option < [u8; _] >" ) => "BLOB" ,
862
+ ( _, "Option < Blob >" ) => ( "BLOB" , "b\" \" " ) ,
863
+ ( _, "Blob" ) => ( "BLOB NOT NULL" , "''" ) ,
864
+ ( _, "Option < Vec < u8 > >" ) => ( "BLOB" , "b\" \" " ) ,
865
+ ( _, "Vec < u8 >" ) => ( "BLOB NOT NULL" , "''" ) ,
866
+ ( _, "Option < [u8; _] >" ) => ( "BLOB" , "b\" \\ x00\\ x01\\ xff\" " ) ,
867
+ ( _, "[u8; _]" ) => ( "BLOB NOT NULL" , "''" ) ,
820
868
_ => {
869
+ // JSON-serialized
821
870
if ty_str. starts_with ( "Option < " ) {
822
- "TEXT" // JSON-serialized
871
+ ( "TEXT" , " \" \" " )
823
872
} else {
824
- abort ! (
825
- ty,
826
- "Turbosql types must be wrapped in Option for forward/backward schema compatibility. Try: Option<{}>" ,
827
- ty_str
828
- )
873
+ ( "TEXT NOT NULL" , "''" )
829
874
}
830
875
}
831
876
} ;
832
877
878
+ if sql_default. is_none ( ) && sql_type. ends_with ( "NOT NULL" ) {
879
+ sql_default = Some ( default_example. into ( ) ) ;
880
+ // abort!(f, "Field `{}` has no default value and is not nullable. Either add a default value with e.g. #[turbosql(sql_default = {default_example})] or make it Option<{ty_str}>.", name);
881
+ }
882
+
833
883
Some ( Column {
834
884
ident : ident. clone ( ) . unwrap ( ) ,
835
885
span : ty. span ( ) ,
836
886
rust_type : ty_str,
837
887
name,
838
888
sql_type,
889
+ sql_default,
839
890
} )
840
891
} )
841
892
. collect :: < Vec < _ > > ( ) ;
@@ -961,9 +1012,13 @@ fn make_migrations(table: &Table) -> Vec<String> {
961
1012
let mut alters = table
962
1013
. columns
963
1014
. iter ( )
964
- . filter_map ( |c| match ( c. name . as_str ( ) , c. sql_type ) {
965
- ( "rowid" , "INTEGER PRIMARY KEY" ) => None ,
966
- _ => Some ( format ! ( "ALTER TABLE {} ADD COLUMN {} {}" , table. name, c. name, c. sql_type) ) ,
1015
+ . filter_map ( |c| match ( c. name . as_str ( ) , c. sql_type , & c. sql_default ) {
1016
+ ( "rowid" , "INTEGER PRIMARY KEY" , _) => None ,
1017
+ ( _, _, None ) => Some ( format ! ( "ALTER TABLE {} ADD COLUMN {} {}" , table. name, c. name, c. sql_type) ) ,
1018
+ ( _, _, Some ( sql_default) ) => Some ( format ! (
1019
+ "ALTER TABLE {} ADD COLUMN {} {} DEFAULT {}" ,
1020
+ table. name, c. name, c. sql_type, sql_default
1021
+ ) ) ,
967
1022
} )
968
1023
. collect :: < Vec < _ > > ( ) ;
969
1024
0 commit comments