SQL-Gen - Rust CLI Tool for PostgreSQL Database Operations Generation extending SQLX

PR's and Issues welcome! This is still early stages of devopment, though hopefully useful to some!

SQL-Gen is a command-line tool written in Rust that helps you generate Rust structs, queries, and SQL migrations based on an existing PostgreSQL database schema. It utilizes the sqlx and clap libraries to provide a user-friendly and efficient experience for working with your PostgreSQL database.

This project draws inspiration from rsdbgen (GitHub: brianhv/rsdbgen) and we appreciate their contribution to the Rust database tooling ecosystem. Originally this was going to be extending rsdbgen but at a certain point of changes it seemed to have diverged.

Features

Installation

To use SQL-Gen, make sure you have Rust and Cargo installed on your system. You can install them by following the instructions at https://www.rust-lang.org/.

Once you have Rust and Cargo installed, you can build SQL-Gen by running the following command:

shell cargo install sql-gen

Or for the latest github shell cargo install --git https://github.com/jayy-lmao/sql-codegen --branch main

Usage

sql-gen [SUBCOMMAND] [OPTIONS]

Subcommands:

Options:

Example .env file

Create a .env file in the project root directory with the following content:

DATABASE_URL=postgres://username:password@localhost/mydatabase SQLGEN_MODEL_OUTPUT_FOLDER=./src/models/ SQLGEN_MODEL_FOLDER=./src/models/ SQLGEN_MIGRATION_OUTPUT=./migrations

Make sure to replace the values with your actual database connection URL and desired folder paths for generated structs and migrations.

Generate Structs and Queries

To generate Rust structs and queries for a PostgreSQL database, use the generate command:

shell sql-gen generate --output db --database <DATABASE_URL>

Replace <DATABASE_URL> with the URL of your PostgreSQL database. The generated code will be saved in the db folder.

Example:

Assuming we have the following input database schema:

sql CREATE TABLE customer ( id SERIAL PRIMARY KEY, created_at TIMESTAMPTZ DEFAULT NOW() email VARCHAR(255) UNIQUE, );

Running SQLGen with the generate command:

shell sql-gen generate --output db --database postgresql://postgres:password@localhost/mydatabase

This will generate the following Rust structs and queries:

```rust // in db/customer.rs

[derive(sqlx::FromRow, Debug)]

struct Customer { pub id: i32, pub created_at: Option>, pub email: Option, }

// in db/customerdbset.rs use sqlx::{query, query_as, PgExecutor, Result}; use super::Customer;

pub struct CustomerSet;

impl CustomerSet { pub async fn all<'e, E: PgExecutor<'e>>(&self, executor: E) -> Result> { queryas::<_, Customer>(r#"SELECT * FROM "customer""#) .fetchall(executor) .await }

pub async fn by_id<'e, E: PgExecutor<'e>>(&self, executor: E, id: i64) -> Result<Customer> {
    query_as::<_, Customer>(r#"SELECT * FROM "customer" WHERE "id" = $1"#)
        .bind(id)
        .fetch_one(executor)
        .await
}

pub async fn by_id_optional<'e, E: PgExecutor<'e>>(&self, executor: E, id: i64) -> Result<Option<Customer>> {
    query_as::<_, Customer>(r#"SELECT * FROM "customer" WHERE "id" = $1"#)
        .bind(id)
        .fetch_optional(executor)
        .await
}

// Doesn't exist in this example, but foreign keys will functions like this, assuming customer has a fk field called category
// pub async fn all_by_categories_id<'e, E: PgExecutor<'e>>(executor: E, categories_id: i64) -> Result<Vec<Customer>> {
//     query_as::<_, Customer>(r#"SELECT * FROM "customer" WHERE category = $1"#)
//         .bind(categories_id)
//         .fetch_all(executor)
//         .await
// }

pub async fn insert<'e, E: PgExecutor<'e>>(&self, executor: E, products: Customer) -> Result<Customer> {
    query_as::<_, Customer>(r#"INSERT INTO "customer" ("id", "created_at", "email", "category") VALUES ($1, $2, $3, $4) RETURNING *;"#)
        .bind(products.id)
        .bind(products.created_at)
        .bind(products.email)
        .fetch_one(executor)
        .await
}

pub async fn update<'e, E: PgExecutor<'e>>(&self, executor: E, products: Customer) -> Result<Customer> {
    query_as::<_, Customer>(r#"UPDATE "customer" SET "created_at" = $2, "email" = $3 WHERE "id" = 1 RETURNING *;"#)
        .bind(products.id)
        .bind(products.created_at)
        .bind(products.email)
        .fetch_one(executor)
        .await
}

pub async fn delete<'e, E: PgExecutor<'e>>(&self, executor: E) -> Result<()> {
    query(r#"DELETE FROM "customer" WHERE "id" = 1"#)
        .execute(executor)
        .await
        .map(|_| ())
}

}

// in db/mod.rs pub mod customer; pub use customer::Customer; pub mod customerdbset; pub use customerdbset::CustomerSet;

pub struct PostgresContext;

impl PostgresContext { pub fn customer(&self) -> CustomerSet { CustomerSet }

} ``` The name of the context will default to the name of your database, or can be set with the '--context' flag. These queries may need modifying or changing, but they can serve as a good start. You should be able to run commands like:

rust let customers = PostgresContext.customer().all(&pool).await?;

The suggested way to add customer queries etc would be to add them somewhere like db/customer_custom_queries.rs so that they are not overwritten by codgen. If you impl CustomerSet and add functions it should extend it.

Generate Migrations

To generate SQL migrations based on changes in the structs, use the migrate generate command:

shell sql-gen migrate generate --database <DATABASE_URL> --include <FOLDER_PATH> --output migrations

Replace <DATABASE_URL> with the URL of your PostgreSQL database, <FOLDER_PATH> with the folder containing the generated structs (db in the previous example), and migrations with the output folder for the SQL migrations.

Example:

Running SQLGen with the migrate generate command:

shell sql-gen migrate generate --database postgresql://postgres:password@localhost/mydatabase --include db --output migrations

This will perform a dry run of the previous database generation, compare it with the existing structs in the db folder, and generate SQL migrations for any detected changes. The migrations will be saved in the migrations folder.

Example Migration:

Assuming a change is made to the Customer struct, adding a new field:

rust pub struct Customer { pub id: i32, pub created_at: Option<chrono::DateTime<chrono::Utc>>, pub email: Option<String>, pub address: Option<String>, // New field }

Running SQL-Gen with the migrate generate command will generate the following migration:

sql -- Migration generated for struct: Customer ALTER TABLE customer ADD COLUMN address TEXT;

For a complete list of available commands and options, you can use the --help flag:

shell sql-gen --help

Roadmap

SQL-Gen is under active development, and future enhancements are planned. Here are some items on the roadmap:

Your contributions and feedback are highly appreciated! If you encounter any issues or have suggestions