Code Monkey home page Code Monkey logo

dsync's Introduction

dsync

License: MIT OR Apache-2.0

A utility to generate database structs and querying code from diesel schema files. Primarily built for create-rust-app.

Currently, it's more advantageous to generate code over deriving code with macros because intellisense and autocompletion isn't quite there when it comes to macro expansion.

Demo

Given the following schema:

// schema.rs
diesel::table! {
    todos (id) {
        id -> Int4,
        text -> Text,
        completed -> Bool,
    }
}

We run:

cargo dsync -i schema.rs -o models

Now we have everything we need!

use models::todos;

async fn demo(db: Connection) {
  let created_todo = todos::create(&mut db, todos::CreateTodo {
    text: "Create a demo",
    completed: false,
  })?;
    
  let updated_todo = todos::update(&mut db, created_todo.id, UpdateTodo {
    text: created_todo.text,
    completed: true,
  })?;
}

For a complete example, see test/simple_table/schema.rs which generates all the code in test/simple_schema/models.

Usage

  1. Add this crate:

    cargo add dsync
  2. Create a new binary in your project which uses the crate (for example, bin/dsync.rs)

    use std::{collections::HashMap, path::PathBuf};
    use dsync::{GenerationConfig, TableOptions};
    
    pub fn main() {
        let dir = env!("CARGO_MANIFEST_DIR");
    
        dsync::generate_files(
            PathBuf::from_iter([dir, "src/schema.rs"]), 
            PathBuf::from_iter([dir, "src/models"]), 
            GenerationConfig { /* ... your generation options ... */ }
        );
    }
  3. Create a Cargo.toml binary entry:

    [[bin]]
    name = "dsync"
    path = "bin/dsync.rs"
  4. Execute!

    cargo run --bin dsync

Protip: to use cargo dsync, create an alias in .cargo/config:

[alias]
dsync="run --bin dsync"

Pre-built binary

Setting up a custom binary allows you to completely customize the generation; however, if complete customization isn't necessary, you can install the CLI directly (you'll have to make sure you keep it up-to-date by running this periodically):

cargo install dsync 

CLI Usage

  • -i: input argument: path to schema file
  • -o: output argument: path to directory where generated code should be written
  • -c: connection type (for example: diesel::r2d2::PooledConnection<diesel::r2d2::ConnectionManager<diesel::pg::PgConnection>>)
  • -g: (optional, repeatable) list of columns that are automatically generated by create/update triggers (for example, created_at, updated_at)
  • --tsync: (optional) adds #[tsync] attribute to generated structs (see https://github.com/Wulf/tsync)
  • --model-path: (optional) set a custom model import path, default crate::models::
  • --schema-path: (optional) set a custom schema import path, default crate::schema::
  • --no-serde: (optional) if set, does not output any serde related code
  • --no-crud: (optional) Do not generate the CRUD functions for generated models
  • --create-str: (optional) Set which string type to use for Create* structs (possible are string, str, cow)
  • --update-str: (optional) Set which string type to use for Update* structs (possible are string, str, cow)
  • --single-model-file: (optional) Generate only a single model file, instead of a directory with mod.rs and generated.rs
  • --readonly-prefix: (optional, repeatable) A prefix to treat a table matching this as readonly *2
  • --readonly-suffix: (optional, repeatable) A suffix to treat a table matching this as readonly *2
  • --diesel-backend: (when the "advanced-queries" feature is enabled) The diesel backend in use (possible values include diesel::pg::Pg, diesel::sqlite::Sqlite, diesel::mysql::Mysql, or your custom backend type)
  • note: the CLI has fail-safes to prevent accidental file overwriting
dsync -i src/schema.rs -o src/models

Notes:

  • *2: "readonly" tables dont have Update* & Create* structs, only *(no suffix / prefix) structs. For example this is useful for Sqlite views, which are read-only (cannot be written to, but can be read)

Experimental API

We're currently experimenting with advanced query generation. This includes pagination, filtering/searching, and the like. Enable the advanced-queries feature flag to see some of it in action.

Alternatively, you can see what gets generated in the advanced queries test here: test/advanced_queries/models

Feel free to open an issue to discuss these API and provide your feeedback.

Docs

See dsync --help for more information.

Feel free to open tickets for support or feature requests.

Development/Testing

Use ./test/test_all.sh to run tests. After running the test, there should be no unexpected changes to files in ./test (use git status and git diff to see if there were any changes).

License

This tool is distributed under the terms of both the MIT license and the Apache License (Version 2.0).

See LICENSE-APACHE, LICENSE-MIT, and COPYRIGHT for details.

dsync's People

Contributors

anthonymichaeltdm avatar dependabot[bot] avatar driver005 avatar hasezoey avatar jean-santos avatar jimmy-ho avatar longsleep avatar thwyr avatar tpotancok avatar wulf avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

dsync's Issues

fix: incomplete generated code for pk

스크린샷 2023-10-16 오후 12 58 41

As we can see, part of using primary key is missing.

INFO

  • dsync: 0.0.16
  • diesel: 2.1.3
  • rustc: 1.70.0 (90c541806 2023-05-31)
  • OS: macOS Ventura 13.5.2(22G91)
  • HW: M1 MAX

Dsync not working with schema generated by diesel 2.1.0

My schema.rs generated by diesel 2.0.4 worked fine.
Now I've upgraded to diesel 2.1.0 which, on top of everything else, added support for column size restrictions (aka max_length) and dsync panic with Unsupported schema format! (Invalid column definition token in diesel table macro).

Add `PartialEq` derive to generated "Update" structs

A simple way to check if an update is "empty" is to compare it to Default::default(). This requires the struct to derive PartialEq which should be safe to add always.

With the changes from #87 this is now also super easy to add.

Dsync generated code does not work with MySQL

The generated code from dsync (for create and update) uses get_result which implicitly adds a RETURNING to the generated SQL statement. MySQL does not support RETURNING hence, the trait check for generated code fails as the MysqlConnection does not implement the SupportsReturningClause marker trait.

Instead the create and update functions should use execute and leave it to the caller if it is needed to actually load the data after insert or update.

Imports in `diesel::table!` panic

The handle_table_macro function in parser.rs assumes the macro only contains the actual table declaration. This makes diesel-derive-enum, full text search and other extensions impossible to use with dsync. Imports inside the macro are officially supported and dsync should not panic because of them.

The "use diesel::*" change causes breakage as this can have conflicts with database table names

With 5062ebe the previous use crate::diesel::*; was changed to use diesel::*. While this is fine in general, this import is too "wide" if by chance a database table matches one of the very many imports which are pulled in from this.

Please revert this change.

In my example, there is a table named associations which is not super uncommon.

Having the import from crate::diesel::* allows manual defines of the actually needed diesel imports. Maybe it is even better to avoid the wildcard import altogether.

pub(crate) mod diesel {
    // NOTE(longsleep): This module wrapps the diesel crate exports so that
    // it does not export "associations" as that conflicts with our database
    // table "associations".
    pub use diesel::{
        backend, deserialize, dsl, expression, insert_into, insertable, internal, prelude, query_builder, row,
        AsChangeset, ExpressionMethods, Insertable, QueryDsl, Queryable, RunQueryDsl, Selectable,
    };
}

Question about upstreaming changes from fork

Hello, because this project was inactive for some time, i had made a fork and added some features to it, i would like to know if i should upstream the changes and when yes which?

list of changes can be found in the forks CHANGELOG

PS: i know this should likely not be a issue, but github discussions is not enabled for this project

fix: allow optional `GenerationConfig` fields

The README guides creating custom bin like this.

use std::{collections::HashMap, path::PathBuf};
use dsync::{GenerationConfig, TableOptions};

pub fn main() {
    let dir = env!("CARGO_MANIFEST_DIR");

    dsync::generate_files(
        PathBuf::from_iter([dir, "src/schema.rs"]), 
        PathBuf::from_iter([dir, "src/models"]), 
        GenerationConfig { /* ... your generation options ... */ }
    );
}

dsync/src/lib.rs

Lines 216 to 237 in 007ace8

pub struct GenerationConfig<'a> {
/// Specific Table options for a given table
pub table_options: HashMap<&'a str, TableOptions<'a>>,
/// Default table options, used when not in `table_options`
pub default_table_options: TableOptions<'a>,
/// Connection type to insert
///
/// Example: `diesel::SqliteConnection`
pub connection_type: String,
/// Diesel schema import path
///
/// by default `crate::schema::`
pub schema_path: String,
/// Dsync model import path
///
/// by default `crate::models::`
pub model_path: String,
/// Generate common structs only once in a "common.rs" file
pub once_common_structs: bool,
/// Generate the "ConnectionType" type only once in a "common.rs" file
pub once_connection_type: bool,
}

I wanted to only specify GenerationConfig.connection_type, but I am forced to input every options.
I think they should be able to be omitted except connection_type.

Thank you very much for this precious project.

Roadmap (Suggestions)

Currently the project has seen many changes and will likely see some more re-structures in the (hopefully near) future, so here is a roadmap on how we could handle this project and agree and some rough events

  • release 0.1.0, after #56 (and likely some fixup PRs) are resolved
    • this would differentiate it from the 0.0.x line, because the internals have changed quite a bit
    • it would also mark this project as being a little more stable (though with changes expected)
    • ensure the publishing CI works (see #71)
  • release 0.2.0 with some other big changes (like #103, #105 and #99)
  • release 1.0.0 once we are confident the code is in a good state (and the CI is working)

what do you think @Wulf, anything to add / change?

Use a Trait to organize functions

Hey, one of the creators of lemmy here, this looks like a really interesting project that could help us. We have a lot of tables, and generating a lot of the simpler Crud-type functions in code would make this really useful.

Instead of pub struct UpdateTodo and exporting that, I highly recommend creating a trait. Here's what we do in our code:

pub trait Crud {
  type Form;
  type IdType; // We use diesel-newtypes to make sure all the Ids are unit structs, to make sure people aren't using a `PostId` when they should be using a `CommentId` for example... using `i32` can be dangerous.
  fn create(conn: &mut Connection, form: &Self::Form) -> Result<Self, Error>
    where Self: Sized;
  fn read(conn: &mut Connection, id: Self::IdType) -> Result<Self, Error> 
  ...

Then your generated code would be:

impl Crud for XXX {
  type Form = XXXForm;
  fn read(...

And people could easily import Crud to get all the functions.

Option to set sql schema and model path

Currently dsync automatically adds a static use crate::schema::* and use crate::models::, which may not be the path where the sql schemas and models are at.

for example i run dsync -i src/data/sql_schema.rs -o src/data/models --connection-type "diesel::SqliteConnection" and so would need something like use crate::data::sql_schema; so a option to set that like connection-type would be great

example:

dsync -i src/data/sql_schema.rs  -o src/data/models --connection-type "diesel::SqliteConnection" --sql-schema-path "crate::data::sql_schema" --sql-model-path "crate::data::models::"

Dsync does not support unsigned integer "id" fields

The code generated for the individual model's read, update and delete functions does not work when the model's id field is unsigned integer. The model is generated fine as u32, but all the functions take and i32 regardless and thus that value cannot be used in the dsl functions as parameters.

CI refactor

Can the cargo test step run after cargo build? I'm assume it can reuse the cached/compiled artifacts so it can potentially run faster

image

feat: custom derive as config

Maybe sometimes custom derives would be needed, IMHO.

#[derive(Debug, Clone)]
pub struct TableOptions<'a> {
    // ... other fields are omitted for brevity. 

    /// any arbitrary derives to append
    extra_derives: Vec<&'a str>
}

How about this?
Thanks.

Change default for tsync

After thinking about this for some time, I've come to the conclusion that #[tsync] additions shouldn't be the default like it currently is. Users should manually copy and edit the generated read struct and filter out any properties that may contain sensitive information not bound for the frontend

The `diesel::Identifiable` trait is not derived for the table a foreign key refers to

In

if !self.table.foreign_keys.is_empty() {
the Identifiable traits gets derived for any table which has a foreign key. That is fine and all but forgets the table the foreign key comes from.

A super simple fix goes like this

diff --git a/src/code.rs b/src/code.rs
index e982a36..277a248 100644
--- a/src/code.rs
+++ b/src/code.rs
@@ -198,6 +198,8 @@ impl<'a> Struct<'a> {

                 if !self.table.foreign_keys.is_empty() {
                     derives_vec.extend_from_slice(&[derives::ASSOCIATIONS, derives::IDENTIFIABLE]);
+                } else if !self.table.primary_key_columns.is_empty() {
+                    derives_vec.push(derives::IDENTIFIABLE);
                 }
             }

When a table has a primary key that usually should mean that Identifiable should work. Dsync already generates the correct other fields related to this trait at other places. See https://docs.diesel.rs/2.0.x/diesel_derives/derive.Identifiable.html.

The above patch works for me. This issue can be used for discussion in case I missed something, before I make a PR for this.

Option to not generate with `serde`

My project does not use serde (at least yet), so it would be great to have a option to disable generating with serde:: types

example:

use serde::{Deserialize, Serialize};

//                 v            v
#[derive(Debug, Serialize, Deserialize, Clone, Queryable, Insertable, AsChangeset, Selectable)]

Add support for "unsigned" numeric fields

Hi, I just gave this project a try to generate DB models for an existing database. The database uses unsigned integers on may columns. Those get typed (for example) as i32 where it should be u32.

I came accross https://github.com/Wulf/dsync/blob/12979e00998635291ecaf721cf6bc4282d9808ee/src/parser.rs (but there is no panic, or message visible when running dsync.

The resulting problem is, that all models fail to derive Insertable and AsChangeset.

trait bound `i32: diesel::Expression` is not satisfied

For example this table

diesel::table! {
    aliases (id) {
        id -> Unsigned<Integer>,
        aliasname -> Varchar,
        mainname -> Varchar,
    }
}

yields with dsync:

#[derive(Debug, Serialize, Deserialize, Clone, Queryable, Insertable, AsChangeset, Identifiable)]
#[diesel(table_name=aliases, primary_key(id))]
pub struct Aliase {
    pub id: i32,
    pub aliasname: String,
    pub mainname: String,
}

And with https://github.com/abbychau/diesel_cli_ext (where it works):

#[derive(Debug, Serialize, Deserialize, Clone, Queryable, Insertable, AsChangeset, Identifiable)]
#[diesel(table_name = aliases)]
pub struct Aliase {
    pub id: u32,
    pub aliasname: String,
    pub mainname: String,
}

Simplify table config fetching logic

Currently, we have a GenerationConfig#table(name) method that retreives the config for a particular table. This should be a simple retrieval, but instead, we make some changes to the table config before returning it. Let's move this to a pre-generation step in the beginning of lib.rs#generate_files.

Related comment here.

Filtering: better "read" fns

I just wanted to share my thoughts on the 'read/paginate' functions:

They are very arbitrary unless we introduce a way to filter the results.

In #103, I attempted to add filter() helper which allows us to generate arbitrary WHERE clauses; however, it currently only supports "equals"-based clauses, and not things like:

  • string search
  • date/time start/end/range
  • between/greater-than/less-than number,
  • etc; essentially the full list of SQL operations allowed in WHERE clauses.

This is a major turning point for dsync which has primarily been a code-generation tool as opposed to a query-builder. Deciding to add filter() is the only way functions like read() and paginate() make sense. However, we can only go so far with the filtering before we end up with a query builder 😅.

So here's the thing: do we continue with this effort?

Some more thoughts: we should consider generating cursor-based pagination functions and may also want to split read into: read_first and read_all (or similar-named functions).

Anyway, no big deal, hope all is well 🙌

Publishing via CI

hey @hasezoey

Do you have any experience setting up crates.io publishing CI? I think it’s time for us to pursue this. I can add a CARGO_TOKEN secret to this repo which we can use for authorization.

Anyway, hope all is well on your end 🙌

Improve Code generation

Currently code generation is basically done by string concatenation, which is "good enough" but could be improved, current suggestions are:

What to do about `StructType::Form`

Currently there is a variant called Form in StructType, but it is unused and cannot be used outside of the library, so i would like to know if we can remove this, or the wanted behavior this should have so it can be implemented

Form, // this one contains primary key columns (not optional) and normal columns (optional) excluding those marked as autogenerated

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.