From 5943f4419e16788e7cfca8a41615e9f401db6af3 Mon Sep 17 00:00:00 2001 From: Samuel Marks <807580+SamuelMarks@users.noreply.github.com> Date: Wed, 27 Nov 2024 16:48:56 -0600 Subject: [PATCH 01/14] [src/code.rs] Implement generator for `impl Default` ; [src/{global.rs,bin/main.rs}] Expose new generator all the way up to binary --- src/bin/main.rs | 5 +++++ src/code.rs | 60 +++++++++++++++++++++++++++++++++++++++++++++---- src/global.rs | 3 +++ 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/src/bin/main.rs b/src/bin/main.rs index 731cad8e..ef2c54b3 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -136,6 +136,10 @@ pub struct MainOptions { /// See `crate::GenerationConfig::diesel_backend` for more details. #[arg(short = 'b', long = "diesel-backend")] pub diesel_backend: String, + + /// Generate the "default" function in an `impl Default` + #[arg(long)] + pub default_impl: bool, } #[derive(Debug, ValueEnum, Clone, PartialEq, Default)] @@ -265,6 +269,7 @@ fn actual_main() -> dsync::Result<()> { once_connection_type: args.once_connection_type, readonly_prefixes: args.readonly_prefixes, readonly_suffixes: args.readonly_suffixes, + default_impl: args.default_impl, }, }, )?; diff --git a/src/code.rs b/src/code.rs index b4388310..031cf9d3 100644 --- a/src/code.rs +++ b/src/code.rs @@ -1,9 +1,10 @@ +use crate::parser::{ParsedColumnMacro, ParsedTableMacro, FILE_SIGNATURE}; +use crate::{get_table_module_name, GenerationConfig, TableOptions}; use heck::ToPascalCase; use indoc::formatdoc; use std::borrow::Cow; - -use crate::parser::{ParsedColumnMacro, ParsedTableMacro, FILE_SIGNATURE}; -use crate::{get_table_module_name, GenerationConfig, TableOptions}; +use std::iter::Map; +use std::slice::Iter; #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum StructType { @@ -761,6 +762,51 @@ fn build_imports(table: &ParsedTableMacro, config: &GenerationConfig) -> String imports_vec.join("\n") } +/// Get default for type +fn default_for_type(typ: String) -> &'static str { + match typ.as_str() { + "i8" | "u8" | "i16" | "u16" | "i32" | "u32" | "i64" | "u64" | "i128" | "u128" | "isize" + | "usize" => "0", + "f32" | "f64" => "0.0", + // https://doc.rust-lang.org/std/primitive.bool.html#method.default + "bool" => "false", + "String" => "String::new()", + "&str" | "&'static str" => "\"\"", + _ => "Default::default()", + } +} + +/// Generate default (insides of the `impl Default for StructName { fn default() -> Self {} }`) +fn build_default_impl_fn(table: &ParsedTableMacro) -> String { + let mut buffer = String::with_capacity(table.struct_name.len() + table.columns.len() * 4); + buffer.push_str(&format!( + "impl Default for {struct_name} {{\n fn default() -> Self {{\n", + struct_name = table.struct_name.as_str() + )); + let column_name_and_type: Map< + Iter, + fn(&ParsedColumnMacro) -> (String, String), + > = table + .columns + .iter() + .map(|col| (col.name.to_string(), col.ty.to_string())); + + buffer.push_str(" Self {\n"); + let item_id_params = column_name_and_type + .map(|(name, typ)| { + format!( + " param_{name}: {typ_default}", + name = name, + typ_default = default_for_type(typ) + ) + }) + .collect::>() + .join(",\n"); + buffer.push_str(item_id_params.as_str()); + buffer.push_str("\n }\n }\n}"); + buffer +} + /// Generate a full file for a given diesel table pub fn generate_for_table(table: &ParsedTableMacro, config: &GenerationConfig) -> String { // early to ensure the table options are set for the current table @@ -789,11 +835,17 @@ pub fn generate_for_table(table: &ParsedTableMacro, config: &GenerationConfig) - ret_buffer.push_str(update_struct.code()); } - // third and lastly, push functions - if enabled + // third, push functions - if enabled if table_options.get_fns() { ret_buffer.push('\n'); ret_buffer.push_str(build_table_fns(table, config, create_struct, update_struct).as_str()); } + if config.options.default_impl { + ret_buffer.push('\n'); + ret_buffer.push_str(build_default_impl_fn(table).as_str()); + ret_buffer.push('\n'); + } + ret_buffer } diff --git a/src/global.rs b/src/global.rs index 9c951bac..8f8ed493 100644 --- a/src/global.rs +++ b/src/global.rs @@ -328,6 +328,8 @@ pub struct GenerationConfigOpts<'a> { pub readonly_prefixes: Vec, /// Suffixes to treat tables as readonly pub readonly_suffixes: Vec, + /// Generate the "default" function in an `impl Default` + pub default_impl: bool, } impl GenerationConfigOpts<'_> { @@ -363,6 +365,7 @@ impl Default for GenerationConfigOpts<'_> { once_connection_type: false, readonly_prefixes: Vec::default(), readonly_suffixes: Vec::default(), + default_impl: false, } } } From 25161caad41f1bbe7fd670f825359cd6b82c0a5f Mon Sep 17 00:00:00 2001 From: Samuel Marks <807580+SamuelMarks@users.noreply.github.com> Date: Wed, 27 Nov 2024 20:22:57 -0600 Subject: [PATCH 02/14] [src/code.rs] `None` as default when column is optional ; add `PartialEq` on read --- src/code.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/code.rs b/src/code.rs index 031cf9d3..73907b9f 100644 --- a/src/code.rs +++ b/src/code.rs @@ -210,6 +210,7 @@ impl<'a> Struct<'a> { derives::SELECTABLE, #[cfg(feature = "derive-queryablebyname")] derives::QUERYABLEBYNAME, + derives::PARTIALEQ, ]); if !self.table.foreign_keys.is_empty() { @@ -783,26 +784,30 @@ fn build_default_impl_fn(table: &ParsedTableMacro) -> String { "impl Default for {struct_name} {{\n fn default() -> Self {{\n", struct_name = table.struct_name.as_str() )); - let column_name_and_type: Map< + let column_name_type_nullable: Map< Iter, - fn(&ParsedColumnMacro) -> (String, String), + fn(&ParsedColumnMacro) -> (String, String, bool), > = table .columns .iter() - .map(|col| (col.name.to_string(), col.ty.to_string())); + .map(|col| (col.name.to_string(), col.ty.to_string(), col.is_nullable)); buffer.push_str(" Self {\n"); - let item_id_params = column_name_and_type - .map(|(name, typ)| { + let fields_to_defaults = column_name_type_nullable + .map(|(name, typ, nullable)| { format!( " param_{name}: {typ_default}", name = name, - typ_default = default_for_type(typ) + typ_default = if nullable { + "None" + } else { + default_for_type(typ) + } ) }) .collect::>() .join(",\n"); - buffer.push_str(item_id_params.as_str()); + buffer.push_str(fields_to_defaults.as_str()); buffer.push_str("\n }\n }\n}"); buffer } From 797b323025300eb092dbfc5b9e6cbd28f329a110 Mon Sep 17 00:00:00 2001 From: Samuel Marks <807580+SamuelMarks@users.noreply.github.com> Date: Wed, 27 Nov 2024 21:09:56 -0600 Subject: [PATCH 03/14] [src/code.rs] Generate `impl Default for` CRUD structs. BUG: column names don't match struct --- src/code.rs | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/src/code.rs b/src/code.rs index 73907b9f..b5d1528d 100644 --- a/src/code.rs +++ b/src/code.rs @@ -778,17 +778,16 @@ fn default_for_type(typ: String) -> &'static str { } /// Generate default (insides of the `impl Default for StructName { fn default() -> Self {} }`) -fn build_default_impl_fn(table: &ParsedTableMacro) -> String { - let mut buffer = String::with_capacity(table.struct_name.len() + table.columns.len() * 4); +fn build_default_impl_fn(struct_name: &str, columns: &Vec) -> String { + let mut buffer = String::with_capacity(struct_name.len() + columns.len() * 4); buffer.push_str(&format!( "impl Default for {struct_name} {{\n fn default() -> Self {{\n", - struct_name = table.struct_name.as_str() + struct_name = struct_name )); let column_name_type_nullable: Map< Iter, fn(&ParsedColumnMacro) -> (String, String, bool), - > = table - .columns + > = columns .iter() .map(|col| (col.name.to_string(), col.ty.to_string(), col.is_nullable)); @@ -796,7 +795,7 @@ fn build_default_impl_fn(table: &ParsedTableMacro) -> String { let fields_to_defaults = column_name_type_nullable .map(|(name, typ, nullable)| { format!( - " param_{name}: {typ_default}", + " {name}: {typ_default}", name = name, typ_default = if nullable { "None" @@ -815,6 +814,7 @@ fn build_default_impl_fn(table: &ParsedTableMacro) -> String { /// Generate a full file for a given diesel table pub fn generate_for_table(table: &ParsedTableMacro, config: &GenerationConfig) -> String { // early to ensure the table options are set for the current table + let struct_name = table.struct_name.to_string(); let table_options = config.table(&table.name.to_string()); let mut ret_buffer = format!("{FILE_SIGNATURE}\n\n"); @@ -831,6 +831,17 @@ pub fn generate_for_table(table: &ParsedTableMacro, config: &GenerationConfig) - if create_struct.has_code() { ret_buffer.push('\n'); ret_buffer.push_str(create_struct.code()); + if config.options.default_impl { + ret_buffer.push('\n'); + ret_buffer.push_str( + build_default_impl_fn( + &format!("Create{struct_name}"), + &create_struct.table.columns, + ) + .as_str(), + ); + } + ret_buffer.push('\n'); } let update_struct = Struct::new(StructType::Update, table, config); @@ -838,6 +849,17 @@ pub fn generate_for_table(table: &ParsedTableMacro, config: &GenerationConfig) - if update_struct.has_code() { ret_buffer.push('\n'); ret_buffer.push_str(update_struct.code()); + if config.options.default_impl { + ret_buffer.push('\n'); + ret_buffer.push_str( + build_default_impl_fn( + &format!("Update{struct_name}"), + &update_struct.table.columns, + ) + .as_str(), + ); + } + ret_buffer.push('\n'); } // third, push functions - if enabled @@ -848,7 +870,7 @@ pub fn generate_for_table(table: &ParsedTableMacro, config: &GenerationConfig) - if config.options.default_impl { ret_buffer.push('\n'); - ret_buffer.push_str(build_default_impl_fn(table).as_str()); + ret_buffer.push_str(build_default_impl_fn(&struct_name, &table.columns).as_str()); ret_buffer.push('\n'); } From 867c512f01a5a9194edbca1ee3ba2daca837e075 Mon Sep 17 00:00:00 2001 From: Samuel Marks <807580+SamuelMarks@users.noreply.github.com> Date: Thu, 28 Nov 2024 08:53:38 -0600 Subject: [PATCH 04/14] [src/code.rs] Optimise string allocations ; avoid `impl Default` on update ; use `StructType::format` --- src/code.rs | 35 +++++++++-------------------------- 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/src/code.rs b/src/code.rs index b5d1528d..ffccef9d 100644 --- a/src/code.rs +++ b/src/code.rs @@ -764,8 +764,8 @@ fn build_imports(table: &ParsedTableMacro, config: &GenerationConfig) -> String } /// Get default for type -fn default_for_type(typ: String) -> &'static str { - match typ.as_str() { +fn default_for_type(typ: &str) -> &'static str { + match typ { "i8" | "u8" | "i16" | "u16" | "i32" | "u32" | "i64" | "u64" | "i128" | "u128" | "isize" | "usize" => "0", "f32" | "f64" => "0.0", @@ -779,19 +779,12 @@ fn default_for_type(typ: String) -> &'static str { /// Generate default (insides of the `impl Default for StructName { fn default() -> Self {} }`) fn build_default_impl_fn(struct_name: &str, columns: &Vec) -> String { - let mut buffer = String::with_capacity(struct_name.len() + columns.len() * 4); - buffer.push_str(&format!( - "impl Default for {struct_name} {{\n fn default() -> Self {{\n", - struct_name = struct_name - )); let column_name_type_nullable: Map< Iter, - fn(&ParsedColumnMacro) -> (String, String, bool), + fn(&ParsedColumnMacro) -> (String, &str, bool), > = columns .iter() - .map(|col| (col.name.to_string(), col.ty.to_string(), col.is_nullable)); - - buffer.push_str(" Self {\n"); + .map(|col| (col.name.to_string(), col.ty.as_str(), col.is_nullable)); let fields_to_defaults = column_name_type_nullable .map(|(name, typ, nullable)| { format!( @@ -806,9 +799,10 @@ fn build_default_impl_fn(struct_name: &str, columns: &Vec) -> }) .collect::>() .join(",\n"); - buffer.push_str(fields_to_defaults.as_str()); - buffer.push_str("\n }\n }\n}"); - buffer + format!( + "impl Default for {struct_name} {{\n fn default() -> Self {{\n Self {{\n{f2d}\n }}\n }}\n}}", + struct_name = struct_name, f2d=fields_to_defaults.as_str() + ) } /// Generate a full file for a given diesel table @@ -835,7 +829,7 @@ pub fn generate_for_table(table: &ParsedTableMacro, config: &GenerationConfig) - ret_buffer.push('\n'); ret_buffer.push_str( build_default_impl_fn( - &format!("Create{struct_name}"), + &StructType::format(&StructType::Create, &struct_name), &create_struct.table.columns, ) .as_str(), @@ -849,17 +843,6 @@ pub fn generate_for_table(table: &ParsedTableMacro, config: &GenerationConfig) - if update_struct.has_code() { ret_buffer.push('\n'); ret_buffer.push_str(update_struct.code()); - if config.options.default_impl { - ret_buffer.push('\n'); - ret_buffer.push_str( - build_default_impl_fn( - &format!("Update{struct_name}"), - &update_struct.table.columns, - ) - .as_str(), - ); - } - ret_buffer.push('\n'); } // third, push functions - if enabled From 9874dd5f57ca908eb8690a6ef8da214cf4844e0f Mon Sep 17 00:00:00 2001 From: Samuel Marks <807580+SamuelMarks@users.noreply.github.com> Date: Fri, 29 Nov 2024 22:08:10 -0600 Subject: [PATCH 05/14] [src/code.rs] Ignore generated_columns in `impl Default` blocks --- src/code.rs | 44 ++++++++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/src/code.rs b/src/code.rs index ffccef9d..dacfb153 100644 --- a/src/code.rs +++ b/src/code.rs @@ -1,10 +1,8 @@ -use crate::parser::{ParsedColumnMacro, ParsedTableMacro, FILE_SIGNATURE}; -use crate::{get_table_module_name, GenerationConfig, TableOptions}; use heck::ToPascalCase; use indoc::formatdoc; -use std::borrow::Cow; -use std::iter::Map; -use std::slice::Iter; + +use crate::parser::{ParsedColumnMacro, ParsedTableMacro, FILE_SIGNATURE}; +use crate::{get_table_module_name, GenerationConfig, TableOptions}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum StructType { @@ -87,7 +85,7 @@ pub struct StructField { impl StructField { /// Assemble the current options into a rust type, like `base_type: String, is_optional: true` to `Option` - pub fn to_rust_type(&self) -> Cow { + pub fn to_rust_type(&self) -> std::borrow::Cow { let mut rust_type = self.base_type.clone(); // order matters! @@ -773,18 +771,24 @@ fn default_for_type(typ: &str) -> &'static str { "bool" => "false", "String" => "String::new()", "&str" | "&'static str" => "\"\"", - _ => "Default::default()", + "Cow" => "Cow::Owned(String::new())", + _ => { + if typ.starts_with("Option<") { + "None" + } else { + "Default::default()" + } + } } } /// Generate default (insides of the `impl Default for StructName { fn default() -> Self {} }`) -fn build_default_impl_fn(struct_name: &str, columns: &Vec) -> String { - let column_name_type_nullable: Map< - Iter, - fn(&ParsedColumnMacro) -> (String, &str, bool), - > = columns - .iter() - .map(|col| (col.name.to_string(), col.ty.as_str(), col.is_nullable)); +fn build_default_impl_fn<'a>( + struct_name: &str, + columns: impl Iterator, +) -> String { + let column_name_type_nullable = + columns.map(|col| (col.name.to_string(), col.ty.as_str(), col.is_nullable)); let fields_to_defaults = column_name_type_nullable .map(|(name, typ, nullable)| { format!( @@ -810,6 +814,7 @@ pub fn generate_for_table(table: &ParsedTableMacro, config: &GenerationConfig) - // early to ensure the table options are set for the current table let struct_name = table.struct_name.to_string(); let table_options = config.table(&table.name.to_string()); + let generated_columns = table_options.get_autogenerated_columns(); let mut ret_buffer = format!("{FILE_SIGNATURE}\n\n"); @@ -822,6 +827,10 @@ pub fn generate_for_table(table: &ParsedTableMacro, config: &GenerationConfig) - let create_struct = Struct::new(StructType::Create, table, config); + let not_generated = |col: &&ParsedColumnMacro| -> bool { + !generated_columns.contains(&col.column_name.as_str()) + }; + if create_struct.has_code() { ret_buffer.push('\n'); ret_buffer.push_str(create_struct.code()); @@ -830,7 +839,7 @@ pub fn generate_for_table(table: &ParsedTableMacro, config: &GenerationConfig) - ret_buffer.push_str( build_default_impl_fn( &StructType::format(&StructType::Create, &struct_name), - &create_struct.table.columns, + create_struct.table.columns.iter().filter(not_generated), ) .as_str(), ); @@ -853,7 +862,10 @@ pub fn generate_for_table(table: &ParsedTableMacro, config: &GenerationConfig) - if config.options.default_impl { ret_buffer.push('\n'); - ret_buffer.push_str(build_default_impl_fn(&struct_name, &table.columns).as_str()); + ret_buffer.push_str( + build_default_impl_fn(&struct_name, table.columns.iter().filter(not_generated)) + .as_str(), + ); ret_buffer.push('\n'); } From 5e19ba64331e5a9afa8561e75881d73bb26184b1 Mon Sep 17 00:00:00 2001 From: Samuel Marks <807580+SamuelMarks@users.noreply.github.com> Date: Fri, 5 Sep 2025 22:11:35 -0500 Subject: [PATCH 06/14] [src/code.rs] Use multiline raw string syntax --- src/code.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/code.rs b/src/code.rs index 6bda8ce2..048d649f 100644 --- a/src/code.rs +++ b/src/code.rs @@ -85,7 +85,7 @@ pub struct StructField { impl StructField { /// Assemble the current options into a rust type, like `base_type: String, is_optional: true` to `Option` - pub fn to_rust_type(&self) -> std::borrow::Cow { + pub fn to_rust_type(&self) -> std::borrow::Cow<'_, str> { let mut rust_type = self.base_type.clone(); // order matters! @@ -804,8 +804,13 @@ fn build_default_impl_fn<'a>( .collect::>() .join(",\n"); format!( - "impl Default for {struct_name} {{\n fn default() -> Self {{\n Self {{\n{f2d}\n }}\n }}\n}}", - struct_name = struct_name, f2d=fields_to_defaults.as_str() + r#"impl Default for {struct_name} {{ + fn default() -> Self {{ + Self {{ + {fields_to_defaults} + }} + }} +}}"# ) } From ba8b38dca7df828db1704e09fabf96f346549fab Mon Sep 17 00:00:00 2001 From: Samuel Marks <807580+SamuelMarks@users.noreply.github.com> Date: Sat, 6 Sep 2025 12:47:12 -0500 Subject: [PATCH 07/14] [src/code.rs] Apply changed from PR comments --- src/code.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/code.rs b/src/code.rs index 048d649f..9ced917f 100644 --- a/src/code.rs +++ b/src/code.rs @@ -817,7 +817,7 @@ fn build_default_impl_fn<'a>( /// Generate a full file for a given diesel table pub fn generate_for_table(table: &ParsedTableMacro, config: &GenerationConfig) -> String { // early to ensure the table options are set for the current table - let struct_name = table.struct_name.to_string(); + let struct_name = &table.struct_name; let table_options = config.table(&table.name.to_string()); let generated_columns = table_options.get_autogenerated_columns(); @@ -849,7 +849,6 @@ pub fn generate_for_table(table: &ParsedTableMacro, config: &GenerationConfig) - .as_str(), ); } - ret_buffer.push('\n'); } let update_struct = Struct::new(StructType::Update, table, config); @@ -871,7 +870,6 @@ pub fn generate_for_table(table: &ParsedTableMacro, config: &GenerationConfig) - build_default_impl_fn(&struct_name, table.columns.iter().filter(not_generated)) .as_str(), ); - ret_buffer.push('\n'); } ret_buffer From 176fb65845e142e0d6e78e9bc4786ef5e7065df4 Mon Sep 17 00:00:00 2001 From: Samuel Marks <807580+SamuelMarks@users.noreply.github.com> Date: Sat, 6 Sep 2025 13:42:45 -0500 Subject: [PATCH 08/14] [src/code.rs] In `generate_for_table` use `create_struct.fields()` else `table.columns` as before --- src/code.rs | 46 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/src/code.rs b/src/code.rs index 9ced917f..2931ca36 100644 --- a/src/code.rs +++ b/src/code.rs @@ -782,22 +782,26 @@ fn default_for_type(typ: &str) -> &'static str { } } +struct NameTypNullable<'a> { + name: String, + typ: &'a str, + nullable: bool, +} + /// Generate default (insides of the `impl Default for StructName { fn default() -> Self {} }`) fn build_default_impl_fn<'a>( struct_name: &str, - columns: impl Iterator, + column_name_type_nullable: impl Iterator>, ) -> String { - let column_name_type_nullable = - columns.map(|col| (col.name.to_string(), col.ty.as_str(), col.is_nullable)); let fields_to_defaults = column_name_type_nullable - .map(|(name, typ, nullable)| { + .map(|name_typ_nullable| { format!( " {name}: {typ_default}", - name = name, - typ_default = if nullable { + name = name_typ_nullable.name, + typ_default = if name_typ_nullable.nullable { "None" } else { - default_for_type(typ) + default_for_type(name_typ_nullable.typ) } ) }) @@ -820,6 +824,9 @@ pub fn generate_for_table(table: &ParsedTableMacro, config: &GenerationConfig) - let struct_name = &table.struct_name; let table_options = config.table(&table.name.to_string()); let generated_columns = table_options.get_autogenerated_columns(); + let not_generated = |col: &&ParsedColumnMacro| -> bool { + !generated_columns.contains(&col.column_name.as_str()) + }; let mut ret_buffer = format!("{FILE_SIGNATURE}\n\n"); @@ -832,10 +839,6 @@ pub fn generate_for_table(table: &ParsedTableMacro, config: &GenerationConfig) - let create_struct = Struct::new(StructType::Create, table, config); - let not_generated = |col: &&ParsedColumnMacro| -> bool { - !generated_columns.contains(&col.column_name.as_str()) - }; - if create_struct.has_code() { ret_buffer.push('\n'); ret_buffer.push_str(create_struct.code()); @@ -844,7 +847,11 @@ pub fn generate_for_table(table: &ParsedTableMacro, config: &GenerationConfig) - ret_buffer.push_str( build_default_impl_fn( &StructType::format(&StructType::Create, &struct_name), - create_struct.table.columns.iter().filter(not_generated), + create_struct.fields().iter().map(|col| NameTypNullable { + name: col.name.to_owned(), + typ: col.base_type.as_str(), + nullable: col.is_optional, + }), ) .as_str(), ); @@ -867,8 +874,19 @@ pub fn generate_for_table(table: &ParsedTableMacro, config: &GenerationConfig) - if config.options.default_impl { ret_buffer.push('\n'); ret_buffer.push_str( - build_default_impl_fn(&struct_name, table.columns.iter().filter(not_generated)) - .as_str(), + build_default_impl_fn( + &struct_name, + table + .columns + .iter() + .filter(not_generated) + .map(|col| NameTypNullable { + name: col.name.to_string().to_owned(), + typ: col.ty.as_str(), + nullable: col.is_nullable, + }), + ) + .as_str(), ); } From 4d3166356b14b12a9614a05236aef47d50e378aa Mon Sep 17 00:00:00 2001 From: Samuel Marks <807580+SamuelMarks@users.noreply.github.com> Date: Sat, 6 Sep 2025 16:46:17 -0500 Subject: [PATCH 09/14] [src/code.rs] Use `.filter(not_generated)` on `create_struct` (`.fields` was being weird) --- src/code.rs | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/code.rs b/src/code.rs index 2931ca36..ea85b929 100644 --- a/src/code.rs +++ b/src/code.rs @@ -796,7 +796,7 @@ fn build_default_impl_fn<'a>( let fields_to_defaults = column_name_type_nullable .map(|name_typ_nullable| { format!( - " {name}: {typ_default}", + " {name}: {typ_default}", name = name_typ_nullable.name, typ_default = if name_typ_nullable.nullable { "None" @@ -807,11 +807,11 @@ fn build_default_impl_fn<'a>( }) .collect::>() .join(",\n"); - format!( + formatdoc!( r#"impl Default for {struct_name} {{ fn default() -> Self {{ Self {{ - {fields_to_defaults} +{fields_to_defaults} }} }} }}"# @@ -824,6 +824,8 @@ pub fn generate_for_table(table: &ParsedTableMacro, config: &GenerationConfig) - let struct_name = &table.struct_name; let table_options = config.table(&table.name.to_string()); let generated_columns = table_options.get_autogenerated_columns(); + println!("struct_name: {}", struct_name); + println!("&table.name: {}", &table.name); let not_generated = |col: &&ParsedColumnMacro| -> bool { !generated_columns.contains(&col.column_name.as_str()) }; @@ -847,11 +849,16 @@ pub fn generate_for_table(table: &ParsedTableMacro, config: &GenerationConfig) - ret_buffer.push_str( build_default_impl_fn( &StructType::format(&StructType::Create, &struct_name), - create_struct.fields().iter().map(|col| NameTypNullable { - name: col.name.to_owned(), - typ: col.base_type.as_str(), - nullable: col.is_optional, - }), + create_struct + .table + .columns + .iter() + .filter(not_generated) + .map(|col| NameTypNullable { + name: col.column_name.to_string(), + typ: col.ty.as_str(), + nullable: col.is_nullable, + }), ) .as_str(), ); @@ -879,7 +886,7 @@ pub fn generate_for_table(table: &ParsedTableMacro, config: &GenerationConfig) - table .columns .iter() - .filter(not_generated) + // .filter(not_generated) .map(|col| NameTypNullable { name: col.name.to_string().to_owned(), typ: col.ty.as_str(), From 763c52763ed484703e84584d9d2e187319ec9e79 Mon Sep 17 00:00:00 2001 From: Samuel Marks <807580+SamuelMarks@users.noreply.github.com> Date: Sat, 6 Sep 2025 20:02:28 -0500 Subject: [PATCH 10/14] [src/code.rs] Add `impl Default for Update` --- src/code.rs | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/src/code.rs b/src/code.rs index ea85b929..0abfbb31 100644 --- a/src/code.rs +++ b/src/code.rs @@ -228,7 +228,9 @@ impl<'a> Struct<'a> { derives_vec.push(derives::PARTIALEQ); } - derives_vec.push(derives::DEFAULT); + if !self.config.options.default_impl { + derives_vec.push(derives::DEFAULT); + } } StructType::Create => derives_vec.extend_from_slice(&[derives::INSERTABLE]), } @@ -824,8 +826,6 @@ pub fn generate_for_table(table: &ParsedTableMacro, config: &GenerationConfig) - let struct_name = &table.struct_name; let table_options = config.table(&table.name.to_string()); let generated_columns = table_options.get_autogenerated_columns(); - println!("struct_name: {}", struct_name); - println!("&table.name: {}", &table.name); let not_generated = |col: &&ParsedColumnMacro| -> bool { !generated_columns.contains(&col.column_name.as_str()) }; @@ -870,6 +870,25 @@ pub fn generate_for_table(table: &ParsedTableMacro, config: &GenerationConfig) - if update_struct.has_code() { ret_buffer.push('\n'); ret_buffer.push_str(update_struct.code()); + if config.options.default_impl { + ret_buffer.push('\n'); + ret_buffer.push_str( + build_default_impl_fn( + &StructType::format(&StructType::Update, &struct_name), + update_struct + .table + .columns + .iter() + .filter(not_generated) + .map(|col| NameTypNullable { + name: col.column_name.to_string(), + typ: col.ty.as_str(), + nullable: col.is_nullable, + }), + ) + .as_str(), + ); + } } // third, push functions - if enabled @@ -883,15 +902,11 @@ pub fn generate_for_table(table: &ParsedTableMacro, config: &GenerationConfig) - ret_buffer.push_str( build_default_impl_fn( &struct_name, - table - .columns - .iter() - // .filter(not_generated) - .map(|col| NameTypNullable { - name: col.name.to_string().to_owned(), - typ: col.ty.as_str(), - nullable: col.is_nullable, - }), + table.columns.iter().map(|col| NameTypNullable { + name: col.name.to_string().to_owned(), + typ: col.ty.as_str(), + nullable: col.is_nullable, + }), ) .as_str(), ); From b1be2b8d222f59eec92a0f39d8110b99d9d5df75 Mon Sep 17 00:00:00 2001 From: Samuel Marks <807580+SamuelMarks@users.noreply.github.com> Date: Sat, 6 Sep 2025 20:16:39 -0500 Subject: [PATCH 11/14] [src/code.rs] Comment out `impl Default for Update` --- src/code.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/code.rs b/src/code.rs index 0abfbb31..f469c87a 100644 --- a/src/code.rs +++ b/src/code.rs @@ -228,7 +228,7 @@ impl<'a> Struct<'a> { derives_vec.push(derives::PARTIALEQ); } - if !self.config.options.default_impl { + /*if !self.config.options.default_impl*/ { derives_vec.push(derives::DEFAULT); } } @@ -870,7 +870,7 @@ pub fn generate_for_table(table: &ParsedTableMacro, config: &GenerationConfig) - if update_struct.has_code() { ret_buffer.push('\n'); ret_buffer.push_str(update_struct.code()); - if config.options.default_impl { + /*if config.options.default_impl { ret_buffer.push('\n'); ret_buffer.push_str( build_default_impl_fn( @@ -888,7 +888,7 @@ pub fn generate_for_table(table: &ParsedTableMacro, config: &GenerationConfig) - ) .as_str(), ); - } + }*/ } // third, push functions - if enabled From 668266d0b3582e4e8e68bb006628dbae2087b3cb Mon Sep 17 00:00:00 2001 From: hasezoey Date: Sun, 7 Sep 2025 13:45:50 +0200 Subject: [PATCH 12/14] refactor(code): move "default_impl" code to "Struct::render" And add test for it --- src/code.rs | 118 ++++----------- test/default_impl/Cargo.toml | 18 +++ test/default_impl/lib.rs | 6 + test/default_impl/models/mod.rs | 1 + test/default_impl/models/todos/generated.rs | 151 ++++++++++++++++++++ test/default_impl/models/todos/mod.rs | 2 + test/default_impl/schema.rs | 16 +++ test/default_impl/test.sh | 8 ++ 8 files changed, 233 insertions(+), 87 deletions(-) create mode 100644 test/default_impl/Cargo.toml create mode 100644 test/default_impl/lib.rs create mode 100644 test/default_impl/models/mod.rs create mode 100644 test/default_impl/models/todos/generated.rs create mode 100644 test/default_impl/models/todos/mod.rs create mode 100644 test/default_impl/schema.rs create mode 100755 test/default_impl/test.sh diff --git a/src/code.rs b/src/code.rs index f469c87a..56f41b60 100644 --- a/src/code.rs +++ b/src/code.rs @@ -228,7 +228,7 @@ impl<'a> Struct<'a> { derives_vec.push(derives::PARTIALEQ); } - /*if !self.config.options.default_impl*/ { + if !self.config.options.default_impl { derives_vec.push(derives::DEFAULT); } } @@ -299,7 +299,7 @@ impl<'a> Struct<'a> { .collect::>() .join(" "); - let fields = self.fields(); + let mut fields = self.fields(); if fields.is_empty() { self.has_fields = Some(false); @@ -332,18 +332,18 @@ impl<'a> Struct<'a> { }; let mut lines = Vec::with_capacity(fields.len()); - for mut f in fields.into_iter() { + for f in fields.iter_mut() { let field_name = &f.name; if f.base_type == "String" { f.base_type = match self.ty { - StructType::Read => f.base_type, + StructType::Read => f.base_type.clone(), StructType::Update => self.opts.get_update_str_type().as_str().to_string(), StructType::Create => self.opts.get_create_str_type().as_str().to_string(), } } else if f.base_type == "Vec" { f.base_type = match self.ty { - StructType::Read => f.base_type, + StructType::Read => f.base_type.clone(), StructType::Update => self.opts.get_update_bytes_type().as_str().to_string(), StructType::Create => self.opts.get_create_bytes_type().as_str().to_string(), } @@ -382,7 +382,7 @@ impl<'a> Struct<'a> { ), }; - let struct_code = formatdoc!( + let mut struct_code = formatdoc!( r#" {doccomment} {tsync_attr}{derive_attr} @@ -409,6 +409,14 @@ impl<'a> Struct<'a> { lines = lines.join("\n"), ); + if self.config.options.default_impl { + struct_code.push('\n'); + struct_code.push_str(&build_default_impl_fn( + &ty.format(&table.struct_name), + &fields, + )); + } + self.has_fields = Some(true); self.rendered_code = Some(struct_code); } @@ -784,51 +792,40 @@ fn default_for_type(typ: &str) -> &'static str { } } -struct NameTypNullable<'a> { - name: String, - typ: &'a str, - nullable: bool, -} - /// Generate default (insides of the `impl Default for StructName { fn default() -> Self {} }`) -fn build_default_impl_fn<'a>( - struct_name: &str, - column_name_type_nullable: impl Iterator>, -) -> String { - let fields_to_defaults = column_name_type_nullable +fn build_default_impl_fn<'a>(struct_name: &str, fields: &[StructField]) -> String { + let fields: Vec = fields + .iter() .map(|name_typ_nullable| { format!( - " {name}: {typ_default}", + "{name}: {typ_default},", name = name_typ_nullable.name, - typ_default = if name_typ_nullable.nullable { + typ_default = if name_typ_nullable.is_optional { "None" } else { - default_for_type(name_typ_nullable.typ) + default_for_type(&name_typ_nullable.base_type) } ) }) - .collect::>() - .join(",\n"); + .collect(); formatdoc!( - r#"impl Default for {struct_name} {{ - fn default() -> Self {{ - Self {{ -{fields_to_defaults} - }} - }} -}}"# + r#" + impl Default for {struct_name} {{ + fn default() -> Self {{ + Self {{ + {fields} + }} + }} + }} + "#, + fields = fields.join("\n ") ) } /// Generate a full file for a given diesel table pub fn generate_for_table(table: &ParsedTableMacro, config: &GenerationConfig) -> String { // early to ensure the table options are set for the current table - let struct_name = &table.struct_name; let table_options = config.table(&table.name.to_string()); - let generated_columns = table_options.get_autogenerated_columns(); - let not_generated = |col: &&ParsedColumnMacro| -> bool { - !generated_columns.contains(&col.column_name.as_str()) - }; let mut ret_buffer = format!("{FILE_SIGNATURE}\n\n"); @@ -844,25 +841,6 @@ pub fn generate_for_table(table: &ParsedTableMacro, config: &GenerationConfig) - if create_struct.has_code() { ret_buffer.push('\n'); ret_buffer.push_str(create_struct.code()); - if config.options.default_impl { - ret_buffer.push('\n'); - ret_buffer.push_str( - build_default_impl_fn( - &StructType::format(&StructType::Create, &struct_name), - create_struct - .table - .columns - .iter() - .filter(not_generated) - .map(|col| NameTypNullable { - name: col.column_name.to_string(), - typ: col.ty.as_str(), - nullable: col.is_nullable, - }), - ) - .as_str(), - ); - } } let update_struct = Struct::new(StructType::Update, table, config); @@ -870,25 +848,6 @@ pub fn generate_for_table(table: &ParsedTableMacro, config: &GenerationConfig) - if update_struct.has_code() { ret_buffer.push('\n'); ret_buffer.push_str(update_struct.code()); - /*if config.options.default_impl { - ret_buffer.push('\n'); - ret_buffer.push_str( - build_default_impl_fn( - &StructType::format(&StructType::Update, &struct_name), - update_struct - .table - .columns - .iter() - .filter(not_generated) - .map(|col| NameTypNullable { - name: col.column_name.to_string(), - typ: col.ty.as_str(), - nullable: col.is_nullable, - }), - ) - .as_str(), - ); - }*/ } // third, push functions - if enabled @@ -897,20 +856,5 @@ pub fn generate_for_table(table: &ParsedTableMacro, config: &GenerationConfig) - ret_buffer.push_str(build_table_fns(table, config, create_struct, update_struct).as_str()); } - if config.options.default_impl { - ret_buffer.push('\n'); - ret_buffer.push_str( - build_default_impl_fn( - &struct_name, - table.columns.iter().map(|col| NameTypNullable { - name: col.name.to_string().to_owned(), - typ: col.ty.as_str(), - nullable: col.is_nullable, - }), - ) - .as_str(), - ); - } - ret_buffer } diff --git a/test/default_impl/Cargo.toml b/test/default_impl/Cargo.toml new file mode 100644 index 00000000..c8b0b907 --- /dev/null +++ b/test/default_impl/Cargo.toml @@ -0,0 +1,18 @@ +[lib] +path = "lib.rs" + +[package] +name = "default_impl" +version = "0.1.0" +edition = "2021" + +[dependencies] +diesel = { version = "*", default-features = false, features = [ + "sqlite", + "r2d2", + "chrono", + "returning_clauses_for_sqlite_3_35", +] } +r2d2.workspace = true +chrono.workspace = true +serde.workspace = true diff --git a/test/default_impl/lib.rs b/test/default_impl/lib.rs new file mode 100644 index 00000000..fdea3f51 --- /dev/null +++ b/test/default_impl/lib.rs @@ -0,0 +1,6 @@ +pub mod models; +pub mod schema; + +pub mod diesel { + pub use diesel::*; +} diff --git a/test/default_impl/models/mod.rs b/test/default_impl/models/mod.rs new file mode 100644 index 00000000..015a6a2b --- /dev/null +++ b/test/default_impl/models/mod.rs @@ -0,0 +1 @@ +pub mod todos; diff --git a/test/default_impl/models/todos/generated.rs b/test/default_impl/models/todos/generated.rs new file mode 100644 index 00000000..aac39819 --- /dev/null +++ b/test/default_impl/models/todos/generated.rs @@ -0,0 +1,151 @@ +/* @generated and managed by dsync */ + +#[allow(unused)] +use crate::diesel::*; +use crate::schema::*; + +pub type ConnectionType = diesel::r2d2::PooledConnection>; + +/// Struct representing a row in table `todos` +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, diesel::Queryable, diesel::Selectable, diesel::QueryableByName, PartialEq, diesel::Identifiable)] +#[diesel(table_name=todos, primary_key(id))] +pub struct Todos { + /// Field representing column `id` + pub id: i32, + /// Field representing column `text` + pub text: String, + /// Field representing column `completed` + pub completed: bool, + /// Field representing column `type` + pub type_: String, + /// Field representing column `smallint` + pub smallint: i16, + /// Field representing column `bigint` + pub bigint: i64, + /// Field representing column `created_at` + pub created_at: chrono::NaiveDateTime, + /// Field representing column `updated_at` + pub updated_at: chrono::NaiveDateTime, +} + +impl Default for Todos { + fn default() -> Self { + Self { + id: 0, + text: String::new(), + completed: false, + type_: String::new(), + smallint: 0, + bigint: 0, + created_at: Default::default(), + updated_at: Default::default(), + } + } +} + +/// Create Struct for a row in table `todos` for [`Todos`] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, diesel::Insertable)] +#[diesel(table_name=todos)] +pub struct CreateTodos { + /// Field representing column `text` + pub text: String, + /// Field representing column `completed` + pub completed: bool, + /// Field representing column `type` + pub type_: String, + /// Field representing column `smallint` + pub smallint: i16, + /// Field representing column `bigint` + pub bigint: i64, +} + +impl Default for CreateTodos { + fn default() -> Self { + Self { + text: String::new(), + completed: false, + type_: String::new(), + smallint: 0, + bigint: 0, + } + } +} + +/// Update Struct for a row in table `todos` for [`Todos`] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, diesel::AsChangeset, PartialEq)] +#[diesel(table_name=todos)] +pub struct UpdateTodos { + /// Field representing column `text` + pub text: Option, + /// Field representing column `completed` + pub completed: Option, + /// Field representing column `type` + pub type_: Option, + /// Field representing column `smallint` + pub smallint: Option, + /// Field representing column `bigint` + pub bigint: Option, + /// Field representing column `created_at` + pub created_at: Option, + /// Field representing column `updated_at` + pub updated_at: Option, +} + +impl Default for UpdateTodos { + fn default() -> Self { + Self { + text: String::new(), + completed: false, + type_: String::new(), + smallint: 0, + bigint: 0, + created_at: Default::default(), + updated_at: Default::default(), + } + } +} + +/// Result of a `.paginate` function +#[derive(Debug, serde::Serialize)] +pub struct PaginationResult { + /// Resulting items that are from the current page + pub items: Vec, + /// The count of total items there are + pub total_items: i64, + /// Current page, 0-based index + pub page: i64, + /// Size of a page + pub page_size: i64, + /// Number of total possible pages, given the `page_size` and `total_items` + pub num_pages: i64, +} + +impl Todos { + /// Insert a new row into `todos` with a given [`CreateTodos`] + pub fn create(db: &mut ConnectionType, item: &CreateTodos) -> diesel::QueryResult { + use crate::schema::todos::dsl::*; + + diesel::insert_into(todos).values(item).get_result::(db) + } + + /// Get a row from `todos`, identified by the primary key + pub fn read(db: &mut ConnectionType, param_id: i32) -> diesel::QueryResult { + use crate::schema::todos::dsl::*; + + todos.filter(id.eq(param_id)).first::(db) + } + + /// Update a row in `todos`, identified by the primary key with [`UpdateTodos`] + pub fn update(db: &mut ConnectionType, param_id: i32, item: &UpdateTodos) -> diesel::QueryResult { + use crate::schema::todos::dsl::*; + + diesel::update(todos.filter(id.eq(param_id))).set(item).get_result(db) + } + + /// Delete a row in `todos`, identified by the primary key + pub fn delete(db: &mut ConnectionType, param_id: i32) -> diesel::QueryResult { + use crate::schema::todos::dsl::*; + + diesel::delete(todos.filter(id.eq(param_id))).execute(db) + } +} diff --git a/test/default_impl/models/todos/mod.rs b/test/default_impl/models/todos/mod.rs new file mode 100644 index 00000000..a5bb9b90 --- /dev/null +++ b/test/default_impl/models/todos/mod.rs @@ -0,0 +1,2 @@ +pub use generated::*; +pub mod generated; diff --git a/test/default_impl/schema.rs b/test/default_impl/schema.rs new file mode 100644 index 00000000..c91a5c4f --- /dev/null +++ b/test/default_impl/schema.rs @@ -0,0 +1,16 @@ +diesel::table! { + todos (id) { + id -> Int4, + // unsigned -> Unsigned, + // unsigned_nullable -> Nullable>, + text -> Text, + completed -> Bool, + #[sql_name = "type"] + #[max_length = 255] + type_ -> Varchar, + smallint -> Int2, + bigint -> Int8, + created_at -> Timestamp, + updated_at -> Timestamp, + } +} diff --git a/test/default_impl/test.sh b/test/default_impl/test.sh new file mode 100755 index 00000000..7c33aaf3 --- /dev/null +++ b/test/default_impl/test.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" + +cd $SCRIPT_DIR + +cargo run --manifest-path ../../Cargo.toml -- \ +-i schema.rs -o models -g id -g created_at -g updated_at --default-impl -c "diesel::r2d2::PooledConnection>" From 532fa2959977316d201b5202541e8ab9c71bd173 Mon Sep 17 00:00:00 2001 From: Samuel Marks <807580+SamuelMarks@users.noreply.github.com> Date: Mon, 8 Sep 2025 14:41:27 -0500 Subject: [PATCH 13/14] [src/code.rs] `build_default_impl_fn` now makes `None` the default for all Update fields --- src/code.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/code.rs b/src/code.rs index 56f41b60..b8cd8b84 100644 --- a/src/code.rs +++ b/src/code.rs @@ -412,6 +412,7 @@ impl<'a> Struct<'a> { if self.config.options.default_impl { struct_code.push('\n'); struct_code.push_str(&build_default_impl_fn( + self.ty, &ty.format(&table.struct_name), &fields, )); @@ -793,14 +794,19 @@ fn default_for_type(typ: &str) -> &'static str { } /// Generate default (insides of the `impl Default for StructName { fn default() -> Self {} }`) -fn build_default_impl_fn<'a>(struct_name: &str, fields: &[StructField]) -> String { +fn build_default_impl_fn<'a>( + struct_type: StructType, + struct_name: &str, + fields: &[StructField], +) -> String { let fields: Vec = fields .iter() .map(|name_typ_nullable| { format!( "{name}: {typ_default},", name = name_typ_nullable.name, - typ_default = if name_typ_nullable.is_optional { + typ_default = if name_typ_nullable.is_optional || struct_type == StructType::Update + { "None" } else { default_for_type(&name_typ_nullable.base_type) From e617290a43de44fd939481134a81dbc60a7198de Mon Sep 17 00:00:00 2001 From: Samuel Marks <807580+SamuelMarks@users.noreply.github.com> Date: Tue, 9 Sep 2025 15:49:21 -0500 Subject: [PATCH 14/14] [src/code.rs] Add test for `build_default_impl_fn` --- src/code.rs | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/code.rs b/src/code.rs index b8cd8b84..568848c3 100644 --- a/src/code.rs +++ b/src/code.rs @@ -828,10 +828,52 @@ fn build_default_impl_fn<'a>( ) } +#[test] +fn test_build_default_impl_fn() { + let fields = vec![ + StructField { + name: String::from("id"), + column_name: String::from("id"), + base_type: String::from("i32"), + is_optional: false, + is_vec: false, + }, + StructField { + name: String::from("title"), + column_name: String::from("title"), + base_type: String::from("String"), + is_optional: false, + is_vec: false, + }, + StructField { + name: String::from("maybe_value"), + column_name: String::from("maybe_value"), + base_type: String::from("i64"), + is_optional: true, + is_vec: false, + }, + ]; + + let generated_code = build_default_impl_fn(StructType::Create, "CreateFake", &fields); + + let expected = r#"impl Default for CreateFake { + fn default() -> Self { + Self { + id: 0, + title: String::new(), + maybe_value: None, + } + } +} +"#; + + assert_eq!(&generated_code, &expected); +} + /// Generate a full file for a given diesel table pub fn generate_for_table(table: &ParsedTableMacro, config: &GenerationConfig) -> String { // early to ensure the table options are set for the current table - let table_options = config.table(&table.name.to_string()); + let table_options = config.table(table.name.to_string().as_str()); let mut ret_buffer = format!("{FILE_SIGNATURE}\n\n");