Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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,
},
},
)?;
Expand Down
85 changes: 82 additions & 3 deletions src/code.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
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};
Expand Down Expand Up @@ -86,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<String>`
pub fn to_rust_type(&self) -> Cow<str> {
pub fn to_rust_type(&self) -> std::borrow::Cow<'_, str> {
let mut rust_type = self.base_type.clone();

// order matters!
Expand Down Expand Up @@ -209,6 +208,7 @@ impl<'a> Struct<'a> {
derives::SELECTABLE,
#[cfg(feature = "derive-queryablebyname")]
derives::QUERYABLEBYNAME,
derives::PARTIALEQ,
]);

if !self.table.foreign_keys.is_empty() {
Expand Down Expand Up @@ -761,10 +761,65 @@ fn build_imports(table: &ParsedTableMacro, config: &GenerationConfig) -> String
imports_vec.join("\n")
}

/// Get default for type
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",
// https://doc.rust-lang.org/std/primitive.bool.html#method.default
"bool" => "false",
"String" => "String::new()",
"&str" | "&'static str" => "\"\"",
"Cow<str>" => "Cow::Owned(String::new())",
_ => {
if typ.starts_with("Option<") {
"None"
} else {
"Default::default()"
}
}
}
}
Comment on lines +776 to +794
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldnt this basically just return Default::default() instead of manually mapping the defaults?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could, but throughout your codebase—for example—you are using more specific calls over Default::default. It makes the code more readable?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont recall that we do this, so i had a quick skim again and couldnt find anything related that we are doing this for Defaults. Do you have a example of where we do this?

Note that i am talking about the generated code not our code style.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here for example https://github.com/Wulf/dsync/blob/2797966/src/global.rs#L356-L368

impl Default for GenerationConfigOpts<'_> {
    fn default() -> Self {
        Self {
            table_options: HashMap::default(),
            default_table_options: Default::default(),
            schema_path: String::from(DEFAULT_SCHEMA_PATH),
            model_path: String::from(DEFAULT_MODEL_PATH),
            once_common_structs: false,
            once_connection_type: false,
            readonly_prefixes: Vec::default(),
            readonly_suffixes: Vec::default(),
        }
    }
}

That could easily be rewritten:

impl Default for GenerationConfigOpts<'_> {
    fn default() -> Self {
        Self {
            table_options: Default::default(),
            default_table_options: Default::default(),
            schema_path: String::from(DEFAULT_SCHEMA_PATH),
            model_path: String::from(DEFAULT_MODEL_PATH),
            once_common_structs: Default::default(),
            once_connection_type: Default::default(),
            readonly_prefixes: Default::default(),
            readonly_suffixes: Default::default(),
        }
    }
}

…but is it more readable that way? - My code makes the generated code roughly as readable as what we write by hand.

Copy link
Collaborator

@hasezoey hasezoey Sep 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That could easily be rewritten:

Yes that could be written as such and i dont think we enforce either style there and either is fine (unless specifically needed).
But like i said, i am talking about generated code not about our code style of dsync itself. This example is not generated code.

Also now that i think about it, why is it implemented manually if a derive could just be added?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes a derive could be written from this and it would mostly work. Not sure if it'd handle the edge cases, like timezone specific starts and whatnot.

If you just want to take the derive approach, then merge my other PR and close this one.

Personally, I like how explicit this one is, and it futureproofs the tz and other more interesting use-cases that can be inferred from db schema but not necessarily other parts of the codebase.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you just want to take the derive approach, then merge my other PR and close this one.

Yes that could be done, but i also see the point of dsync generating the schema once and users manually modifying it afterward, so i this PR still has its use. Just was curious what your use-case for this PR not doing a simple derive was.
Still though, i think from a maintenance point of view Default::default() for all the fields would be the best idea, unless there is a type that does not have that or, as you already said, to customize some types (like timezones).
This means that if there should ever be a change for the default impl, we dont have to also change it, and i think a user would expect to actually see ::default instead of the raw type. (at least i would expect it there)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohh interesting, so you expect people to modify the code after it's generated? - I mean, I do, but that's just because you lack the features I need (and I'm sending you PRs to fix).

How about a compromise: String::default(), Vec::default(), etc.? - And for the scalars / literals, actual literals:

  --> src/main.rs:18:29
   |
18 |     const S: &'static str = Default::default();
   |                             ^^^^^^^^^^^^^^^^^^
   |
   = note: calls in constants are limited to constant functions, tuple structs and tuple variants
error[E0015]: cannot call non-const associated function `<u8 as Default>::default` in constants
  --> src/main.rs:19:20
   |
19 |     const uu: u8 = Default::default();
   |                    ^^^^^^^^^^^^^^^^^^

That way developers will still see default function calls everywhere but the odd false literal; with a fallback to Default::default() if it's not a builtin case I've manually pattern matched.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohh interesting, so you expect people to modify the code after it's generated?

Yes for similar cases as yours where dsync can generate the boilerplate, but can be customized afterward - if necessary.

How about a compromise: String::default(), Vec::default(), etc.?

Sure, but personally, i would expect the same behavior as if using rust-analyzer's "expand derive macro", which practically takes the generated code from the derive and dumps it after the struct, and the Default derive macro uses field: Default::default().

In any case, it is fine as it is now and can be changed in a follow-up PR.

And for the scalars / literals, actual literals:

I dont see how the error messages are relevant for this case. Those Error messages are explicitly for const values.
Playground example


/// Generate default (insides of the `impl Default for StructName { fn default() -> Self {} }`)
fn build_default_impl_fn<'a>(
struct_name: &str,
columns: impl Iterator<Item = &'a ParsedColumnMacro>,
) -> 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!(
" {name}: {typ_default}",
name = name,
typ_default = if nullable {
"None"
} else {
default_for_type(typ)
}
)
})
.collect::<Vec<String>>()
.join(",\n");
format!(
r#"impl Default for {struct_name} {{
fn default() -> Self {{
Self {{
{fields_to_defaults}
}}
}}
}}"#
)
}

/// 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 generated_columns = table_options.get_autogenerated_columns();

let mut ret_buffer = format!("{FILE_SIGNATURE}\n\n");

Expand All @@ -777,9 +832,24 @@ 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());
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),
)
.as_str(),
);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should arguably be in Struct::render.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious to see what that would look like

ret_buffer.push('\n');
}

let update_struct = Struct::new(StructType::Update, table, config);
Expand All @@ -789,11 +859,20 @@ 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(&struct_name, table.columns.iter().filter(not_generated))
.as_str(),
);
ret_buffer.push('\n');
}

ret_buffer
}
3 changes: 3 additions & 0 deletions src/global.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,8 @@ pub struct GenerationConfigOpts<'a> {
pub readonly_prefixes: Vec<String>,
/// Suffixes to treat tables as readonly
pub readonly_suffixes: Vec<String>,
/// Generate the "default" function in an `impl Default`
pub default_impl: bool,
}

impl GenerationConfigOpts<'_> {
Expand Down Expand Up @@ -363,6 +365,7 @@ impl Default for GenerationConfigOpts<'_> {
once_connection_type: false,
readonly_prefixes: Vec::default(),
readonly_suffixes: Vec::default(),
default_impl: false,
}
}
}
Expand Down
Loading