Skip to content
Open
Show file tree
Hide file tree
Changes from 15 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: 76 additions & 9 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 All @@ -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]),
}
Expand Down Expand Up @@ -297,7 +299,7 @@ impl<'a> Struct<'a> {
.collect::<Vec<String>>()
.join(" ");

let fields = self.fields();
let mut fields = self.fields();

if fields.is_empty() {
self.has_fields = Some(false);
Expand Down Expand Up @@ -330,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<u8>" {
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(),
}
Expand Down Expand Up @@ -380,7 +382,7 @@ impl<'a> Struct<'a> {
),
};

let struct_code = formatdoc!(
let mut struct_code = formatdoc!(
r#"
{doccomment}
{tsync_attr}{derive_attr}
Expand All @@ -407,6 +409,15 @@ 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(
self.ty,
&ty.format(&table.struct_name),
&fields,
));
}

self.has_fields = Some(true);
self.rendered_code = Some(struct_code);
}
Expand Down Expand Up @@ -761,6 +772,62 @@ 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_type: StructType,
struct_name: &str,
fields: &[StructField],
) -> String {
let fields: Vec<String> = fields
.iter()
.map(|name_typ_nullable| {
format!(
"{name}: {typ_default},",
name = name_typ_nullable.name,
typ_default = if name_typ_nullable.is_optional || struct_type == StructType::Update
{
"None"
} else {
default_for_type(&name_typ_nullable.base_type)
}
)
})
.collect();
formatdoc!(
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
Expand Down Expand Up @@ -789,7 +856,7 @@ 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());
Expand Down
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
18 changes: 18 additions & 0 deletions test/default_impl/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions test/default_impl/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pub mod models;
pub mod schema;

pub mod diesel {
pub use diesel::*;
}
1 change: 1 addition & 0 deletions test/default_impl/models/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod todos;
151 changes: 151 additions & 0 deletions test/default_impl/models/todos/generated.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/* @generated and managed by dsync */

#[allow(unused)]
use crate::diesel::*;
use crate::schema::*;

pub type ConnectionType = diesel::r2d2::PooledConnection<diesel::r2d2::ConnectionManager<diesel::sqlite::SqliteConnection>>;

/// 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<String>,
/// Field representing column `completed`
pub completed: Option<bool>,
/// Field representing column `type`
pub type_: Option<String>,
/// Field representing column `smallint`
pub smallint: Option<i16>,
/// Field representing column `bigint`
pub bigint: Option<i64>,
/// Field representing column `created_at`
pub created_at: Option<chrono::NaiveDateTime>,
/// Field representing column `updated_at`
pub updated_at: Option<chrono::NaiveDateTime>,
}

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(),
}
}
}
Comment on lines +94 to +106
Copy link
Collaborator

Choose a reason for hiding this comment

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

The tests need to be updated.


/// Result of a `.paginate` function
#[derive(Debug, serde::Serialize)]
pub struct PaginationResult<T> {
/// Resulting items that are from the current page
pub items: Vec<T>,
/// 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<Self> {
use crate::schema::todos::dsl::*;

diesel::insert_into(todos).values(item).get_result::<Self>(db)
}

/// Get a row from `todos`, identified by the primary key
pub fn read(db: &mut ConnectionType, param_id: i32) -> diesel::QueryResult<Self> {
use crate::schema::todos::dsl::*;

todos.filter(id.eq(param_id)).first::<Self>(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<Self> {
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<usize> {
use crate::schema::todos::dsl::*;

diesel::delete(todos.filter(id.eq(param_id))).execute(db)
}
}
2 changes: 2 additions & 0 deletions test/default_impl/models/todos/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub use generated::*;
pub mod generated;
16 changes: 16 additions & 0 deletions test/default_impl/schema.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
diesel::table! {
todos (id) {
id -> Int4,
// unsigned -> Unsigned<Integer>,
// unsigned_nullable -> Nullable<Unsigned<Integer>>,
text -> Text,
completed -> Bool,
#[sql_name = "type"]
#[max_length = 255]
type_ -> Varchar,
smallint -> Int2,
bigint -> Int8,
created_at -> Timestamp,
updated_at -> Timestamp,
}
}
Loading
Loading