rok-sqlc 0.6.0

Generate SQLx Rust database modules from SQL schema files
Documentation
use super::super::test_support::{column, defaulted, generated, nullable, table};
use super::*;

pub(super) fn render(table: &Table) -> String {
    super::render(
        table,
        DeriveOptions {
            serde: false,
            validate: false,
            openapi: 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,
        DeriveOptions {
            serde: true,
            validate: false,
            openapi: false,
        },
    );
    for item in ["", "pub struct User {", "pub struct NewUser {"] {
        assert!(
            rendered.contains(&format!("{serde}{item}")),
            "{item}:\n{rendered}"
        );
    }
    assert!(
        rendered.contains(&format!(
            "{serde}#[serde(default)]\npub struct UpdateUser {{"
        )),
        "{rendered}"
    );
}

/// Plain serde reads both an absent field and `null` as `None`; the
/// generated `db::nullable` helper reads `null` as `Some(None)`.
#[test]
fn serde_patches_and_inputs_tell_absent_from_null() {
    let users = table(
        vec![
            column("id", "i64"),
            column("email", "String"),
            nullable("note", "String"),
            defaulted(nullable("tag", "String")),
        ],
        &["id"],
    );
    let rendered = super::render(
        &users,
        DeriveOptions {
            serde: true,
            validate: false,
            openapi: false,
        },
    );
    let patch = rendered.split("pub struct UpdateUser {").nth(1).unwrap();
    assert!(patch.contains(
            "    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub email: Option<String>,\n"
        ), "{patch}");
    assert!(patch.contains(
            "    #[serde(\n        deserialize_with = \"super::super::nullable::deserialize\",\n        skip_serializing_if = \"Option::is_none\"\n    )]\n    pub note: Option<Option<String>>,\n"
        ), "{patch}");

    let input = rendered.split("pub struct NewUser {").nth(1).unwrap();
    let input = input.split("\n}\n").next().unwrap();
    assert!(input.contains(
            "    #[serde(\n        default,\n        deserialize_with = \"super::super::nullable::deserialize\",\n        skip_serializing_if = \"Option::is_none\"\n    )]\n    pub tag: Option<Option<String>>,"
        ), "{input}");
    // Only the `Option<Option<T>>` input field needs the helper.
    assert_eq!(input.matches("#[serde(").count(), 1, "{input}");
}

#[test]
fn nullable_helper_reads_null_as_some_none() {
    let rendered = render_nullable();
    assert!(rendered.starts_with(HEADER));
    assert!(rendered.contains(
        "pub fn deserialize<'de, T, D>(deserializer: D) -> Result<Option<Option<T>>, D::Error>"
    ));
    assert!(rendered.contains("Option::<T>::deserialize(deserializer).map(Some)"));
}

#[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());
}