rustyroad 1.7.4

Rusty Road is a framework written in Rust that is based on Ruby on Rails. It is designed to provide the familiar conventions and ease of use of Ruby on Rails, while also taking advantage of the performance and efficiency of Rust.
Documentation
//! SQLx repository rendering.

use super::naming::{pascal, snake, sql};
use super::types::base_type;
use crate::database::introspection::{Column, Schema, Table};

/// Renders `repositories.rs`.
pub(super) fn render(schema: &Schema) -> String {
    let mut output = String::from(
        "// Generated by `rustyroad pull --language rust`; do not edit.\n\n\
         use sqlx::{PgPool, Postgres, QueryBuilder};\n\n\
         use super::models::*;\n\n",
    );
    for table in schema.tables.iter().filter(|table| table.has_simple_key()) {
        output.push_str(&repository(table, schema));
    }
    output
}

fn repository(table: &Table, schema: &Schema) -> String {
    let function = snake(&table.name);
    let model = pascal(&table.name);
    let key = table
        .column(&table.primary_key[0])
        .expect("introspected primary key should reference a column");
    let key_type = base_type(&key.sql_type, schema);
    let table_sql = sql(&table.name);
    let key_sql = sql(&key.name);

    format!(
        "/// Returns every row from `{table_name}`.\n\
         pub async fn list_{function}(pool: &PgPool) -> Result<Vec<{model}>, sqlx::Error> {{\n\
             sqlx::query_as::<_, {model}>({list_query:?}).fetch_all(pool).await\n\
         }}\n\n\
         /// Returns one `{table_name}` row by primary key.\n\
         pub async fn find_{function}(pool: &PgPool, id: {key_type}) -> Result<Option<{model}>, sqlx::Error> {{\n\
             sqlx::query_as::<_, {model}>({find_query:?})\n\
                 .bind(id)\n\
                 .fetch_optional(pool)\n\
                 .await\n\
         }}\n\n\
         {create}\n\
         {update}\n\
         /// Deletes one `{table_name}` row by primary key.\n\
         pub async fn delete_{function}(pool: &PgPool, id: {key_type}) -> Result<bool, sqlx::Error> {{\n\
             let result = sqlx::query({delete_query:?}).bind(id).execute(pool).await?;\n\
             Ok(result.rows_affected() > 0)\n\
         }}\n\n",
        table_name = table.name,
        list_query = format!("SELECT * FROM {table_sql}"),
        find_query = format!("SELECT * FROM {table_sql} WHERE {key_sql} = $1 LIMIT 1"),
        delete_query = format!("DELETE FROM {table_sql} WHERE {key_sql} = $1"),
        create = create(table),
        update = update(table, &key_type),
    )
}

fn create(table: &Table) -> String {
    let function = snake(&table.name);
    let model = pascal(&table.name);
    let columns = table
        .columns
        .iter()
        .filter(|column| !column.auto_increment)
        .collect::<Vec<_>>();
    let has_required = columns.iter().any(|column| !optional(column));
    let optional_checks = columns
        .iter()
        .filter(|column| optional(column))
        .map(|column| format!("input.{}.is_some()", snake(&column.name)))
        .collect::<Vec<_>>();
    let has_values = if has_required {
        "true".to_string()
    } else if optional_checks.is_empty() {
        "false".to_string()
    } else {
        optional_checks.join(" || ")
    };
    let column_pushes = columns
        .iter()
        .map(|column| {
            conditional_push(
                column,
                "columns",
                &format!(".push({:?})", sql(&column.name)),
            )
        })
        .collect::<Vec<_>>()
        .join("\n");
    let value_pushes = columns
        .iter()
        .map(|column| {
            let field = snake(&column.name);
            if optional(column) {
                format!(
                    "        if let Some(value) = input.{field}.as_ref() {{\n            values.push_bind(value);\n        }}"
                )
            } else {
                format!("        values.push_bind(&input.{field});")
            }
        })
        .collect::<Vec<_>>()
        .join("\n");

    format!(
        "/// Creates one `{table}` row and returns it.\n\
         pub async fn create_{function}(pool: &PgPool, input: New{model}) -> Result<{model}, sqlx::Error> {{\n\
             if !({has_values}) {{\n\
                 return sqlx::query_as::<_, {model}>({default_query:?}).fetch_one(pool).await;\n\
             }}\n\n\
             let mut query = QueryBuilder::<Postgres>::new({insert_prefix:?});\n\
             {{\n\
                 let mut columns = query.separated(\", \" );\n\
         {column_pushes}\n\
             }}\n\
             query.push(\") VALUES (\");\n\
             {{\n\
                 let mut values = query.separated(\", \" );\n\
         {value_pushes}\n\
             }}\n\
             query.push(\") RETURNING *\");\n\
             query.build_query_as::<{model}>().fetch_one(pool).await\n\
         }}",
        table = table.name,
        default_query = format!("INSERT INTO {} DEFAULT VALUES RETURNING *", sql(&table.name)),
        insert_prefix = format!("INSERT INTO {} (", sql(&table.name)),
    )
}

fn update(table: &Table, key_type: &str) -> String {
    let function = snake(&table.name);
    let model = pascal(&table.name);
    let key = &table.primary_key[0];
    let mutable = table
        .columns
        .iter()
        .filter(|column| column.name != *key)
        .collect::<Vec<_>>();
    let changed = mutable
        .iter()
        .map(|column| format!("input.{}.is_some()", snake(&column.name)))
        .collect::<Vec<_>>()
        .join(" || ");
    let changed = if changed.is_empty() {
        "false"
    } else {
        &changed
    };
    let assignments = mutable
        .iter()
        .map(|column| {
            let field = snake(&column.name);
            let assignment = format!("{} = ", sql(&column.name));
            format!(
                "        if let Some(value) = input.{field}.as_ref() {{\n\
                     set.push({assignment:?}).push_bind_unseparated(value);\n\
                 }}"
            )
        })
        .collect::<Vec<_>>()
        .join("\n");

    format!(
        "/// Applies a partial update to one `{table}` row.\n\
         pub async fn update_{function}(pool: &PgPool, id: {key_type}, input: Patch{model}) -> Result<Option<{model}>, sqlx::Error> {{\n\
             if !({changed}) {{\n\
                 return find_{function}(pool, id).await;\n\
             }}\n\n\
             let mut query = QueryBuilder::<Postgres>::new({update_prefix:?});\n\
             {{\n\
                 let mut set = query.separated(\", \" );\n\
         {assignments}\n\
             }}\n\
             query.push({where_clause:?}).push_bind(id).push(\" RETURNING *\");\n\
             query.build_query_as::<{model}>().fetch_optional(pool).await\n\
         }}",
        table = table.name,
        update_prefix = format!("UPDATE {} SET ", sql(&table.name)),
        where_clause = format!(" WHERE {} = ", sql(key)),
    )
}

fn conditional_push(column: &Column, target: &str, push: &str) -> String {
    let field = snake(&column.name);
    if optional(column) {
        format!("        if input.{field}.is_some() {{\n            {target}{push};\n        }}")
    } else {
        format!("        {target}{push};")
    }
}

fn optional(column: &Column) -> bool {
    column.nullable || column.default.is_some()
}