use crate::schema::{Column, Table, mentions_generated_type};
use super::{
HEADER, doc_name, insert_field_type, keys, push_and_chain, push_doc, push_serde_derive,
update_field_type, use_paths,
};
fn column_doc(column: &Column) -> String {
format!("The {} column.", doc_name(&column.sql_name))
}
pub fn render(table: &Table, serde: bool) -> String {
let mut output = format!("{HEADER}\n");
let mut imports = keys::id_imports(table, &table.columns, true);
if table
.columns
.iter()
.any(|column| mentions_generated_type(&column.rust_type))
{
imports.push("super::super::types".to_owned());
}
if !imports.is_empty() {
output.push_str(&use_paths(&imports));
output.push('\n');
}
keys::render_key_type(&mut output, table, serde);
render_row(&mut output, table, serde);
keys::render_row_key(&mut output, table);
let insert_columns = table.insert_columns();
if !insert_columns.is_empty() {
render_insert_input(&mut output, table, &insert_columns, serde);
}
let update_columns = table.update_columns();
if !table.primary_key.is_empty() && !update_columns.is_empty() {
render_update_patch(&mut output, table, &update_columns, serde);
}
output
}
fn render_row(output: &mut String, table: &Table, serde: bool) {
let doc = format!("A row of the `{}` table.", table.display_name());
push_doc(output, 0, &[&doc]);
output.push_str("#[derive(Debug, Clone, PartialEq, sqlx::FromRow)]\n");
push_serde_derive(output, serde);
output.push_str(&format!("pub struct {} {{\n", table.struct_name));
for column in &table.columns {
push_doc(output, 4, &[&column_doc(column)]);
if column.rust_name != column.sql_name {
output.push_str(&format!(
" #[sqlx(rename = {})]\n",
super::sql::str_literal(&column.sql_name)
));
}
output.push_str(&format!(
" pub {}: {},\n",
column.rust_name,
column.field_type()
));
}
output.push_str("}\n");
}
fn render_insert_input(output: &mut String, table: &Table, columns: &[&Column], serde: bool) {
output.push('\n');
let has_defaults = columns.iter().any(|column| column.is_omittable_on_insert());
let mut doc = vec!["Input for `create`: every column the caller may supply."];
if has_defaults {
doc.extend([
"",
"`Option` fields are columns with a database default: `None` leaves the",
"column out of the `INSERT` so the default applies.",
]);
}
push_doc(output, 0, &doc);
let all_optional = columns
.iter()
.all(|column| column.is_omittable_on_insert() || column.nullable);
if all_optional {
output.push_str("#[derive(Debug, Clone, Default, PartialEq)]\n");
} else {
output.push_str("#[derive(Debug, Clone, PartialEq)]\n");
}
push_serde_derive(output, serde);
output.push_str(&format!("pub struct New{} {{\n", table.struct_name));
for column in columns {
let doc = column_doc(column);
if column.is_omittable_on_insert() && column.nullable {
push_doc(
output,
4,
&[
&doc,
"`None` applies the column default; `Some(None)` inserts `NULL`.",
],
);
} else if column.is_omittable_on_insert() {
push_doc(output, 4, &[&doc, "`None` applies the column default."]);
} else {
push_doc(output, 4, &[&doc]);
}
output.push_str(&format!(
" pub {}: {},\n",
column.rust_name,
insert_field_type(column)
));
}
output.push_str("}\n");
}
fn render_update_patch(output: &mut String, table: &Table, columns: &[&Column], serde: bool) {
output.push('\n');
push_doc(
output,
0,
&["Partial update for `update`: `None` leaves a column unchanged."],
);
output.push_str("#[derive(Debug, Clone, Default, PartialEq)]\n");
push_serde_derive(output, serde);
output.push_str(&format!("pub struct Update{} {{\n", table.struct_name));
for column in columns {
let doc = format!(
"The new value of the {} column.",
doc_name(&column.sql_name)
);
if column.nullable {
push_doc(
output,
4,
&[&doc, "`Some(None)` sets the column to `NULL`."],
);
} else {
push_doc(output, 4, &[&doc]);
}
output.push_str(&format!(
" pub {}: {},\n",
column.rust_name,
update_field_type(column)
));
}
output.push_str("}\n\n");
output.push_str(&format!("impl Update{} {{\n", table.struct_name));
push_doc(
output,
4,
&["Returns `true` when the patch changes no column."],
);
output.push_str(" #[must_use]\n pub fn is_empty(&self) -> bool {\n");
let terms: Vec<String> = columns
.iter()
.map(|column| format!("self.{}.is_none()", column.rust_name))
.collect();
push_and_chain(output, 8, "", &terms, "");
output.push_str(" }\n}\n");
}
#[cfg(test)]
mod tests {
use super::super::test_support::{column, defaulted, generated, nullable, table};
use super::*;
fn render(table: &Table) -> String {
super::render(table, false)
}
#[test]
fn serde_derives_rows_inputs_and_patches_on_request() {
let users = table(
vec![column("id", "i64"), column("email", "String")],
&["id"],
);
let serde = "serde::Serialize, serde::Deserialize)]\n";
assert!(!render(&users).contains("serde"));
let rendered = super::render(&users, true);
for item in ["User", "NewUser", "UpdateUser"] {
assert!(
rendered.contains(&format!("{serde}pub struct {item} {{")),
"{item}:\n{rendered}"
);
}
}
#[test]
fn renders_row_struct_with_from_row() {
let rendered = render(&table(vec![column("email", "String")], &[]));
assert!(rendered.starts_with("// @generated by rok-sqlc. Do not edit by hand.\n"));
assert!(rendered.contains(
"/// A row of the `users` table.\n#[derive(Debug, Clone, PartialEq, sqlx::FromRow)]\npub struct User {\n /// The `email` column.\n pub email: String,\n}"
));
}
#[test]
fn row_doc_names_schema_qualified_table() {
let mut users = table(vec![column("email", "String")], &[]);
users.schema = Some("core".to_owned());
assert!(render(&users).contains("/// A row of the `core.users` table."));
}
#[test]
fn wraps_nullable_columns_in_option() {
let rendered = render(&table(vec![nullable("email", "String")], &[]));
assert!(rendered.contains("pub email: Option<String>,"));
}
#[test]
fn emits_rename_for_renamed_fields() {
let mut renamed = column("type", "String");
renamed.rust_name = "type_".to_owned();
let mut quoted = column("Mixed \"Case\"", "String");
quoted.rust_name = "mixed_case".to_owned();
let rendered = render(&table(vec![renamed, quoted], &[]));
assert!(rendered.contains(" #[sqlx(rename = \"type\")]\n pub type_: String,"));
assert!(
rendered.contains(
" #[sqlx(rename = r#\"Mixed \"Case\"\"#)]\n pub mixed_case: String,"
)
);
}
#[test]
fn insert_input_omits_generated_and_wraps_defaulted_columns() {
let rendered = render(&table(
vec![
generated(column("id", "i64")),
column("email", "String"),
nullable("nickname", "String"),
defaulted(column("created_at", "chrono::DateTime<chrono::Utc>")),
defaulted(nullable("tag", "String")),
],
&["id"],
));
let input = rendered.split("pub struct NewUser {").nth(1).unwrap();
let input = input.split('}').next().unwrap();
assert!(!input.contains("pub id:"));
assert!(input.contains(" pub email: String,\n"));
assert!(input.contains(" pub nickname: Option<String>,\n"));
assert!(input.contains(" pub created_at: Option<chrono::DateTime<chrono::Utc>>,\n"));
assert!(input.contains(
" /// `None` applies the column default; `Some(None)` inserts `NULL`.\n pub tag: Option<Option<String>>,\n"
));
assert!(rendered.contains("#[derive(Debug, Clone, PartialEq)]\npub struct NewUser {"));
}
#[test]
fn insert_input_derives_default_when_every_field_is_optional() {
let rendered = render(&table(
vec![
generated(column("id", "i64")),
defaulted(column("hits", "i32")),
nullable("note", "String"),
],
&["id"],
));
assert!(
rendered.contains("#[derive(Debug, Clone, Default, PartialEq)]\npub struct NewUser {")
);
}
#[test]
fn update_patch_documents_null_and_offers_is_empty() {
let rendered = render(&table(
vec![
column("id", "i64"),
column("email", "String"),
nullable("note", "String"),
],
&["id"],
));
assert!(
rendered
.contains("#[derive(Debug, Clone, Default, PartialEq)]\npub struct UpdateUser {")
);
assert!(rendered.contains(" pub email: Option<String>,\n"));
assert!(rendered.contains(
" /// `Some(None)` sets the column to `NULL`.\n pub note: Option<Option<String>>,\n"
));
assert!(!rendered.contains(" pub id: Option"));
assert!(rendered.contains(
" pub fn is_empty(&self) -> bool {\n self.email.is_none() && self.note.is_none()\n }"
));
}
#[test]
fn omits_update_patch_without_primary_key() {
let rendered = render(&table(vec![column("email", "String")], &[]));
assert!(!rendered.contains("pub struct UpdateUser"));
}
#[test]
fn output_is_deterministic() {
let build = || {
render(&table(
vec![column("id", "i64"), column("email", "String")],
&["id"],
))
};
assert_eq!(build(), build());
}
}