use super::naming::{pascal, snake, sql};
use super::types::base_type;
use crate::database::introspection::{Column, Schema, Table};
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()
}