From 84c41dda4d682b58f3c64622fe67cf1fdd41749a Mon Sep 17 00:00:00 2001 From: Robert Perce Date: Mon, 26 Jan 2026 22:14:58 -0600 Subject: [PATCH] refactor: fewer non-macro queries --- src/models/contact.rs | 155 +++++++++++++++++++++++++++++------------- src/web/contact.rs | 36 +--------- src/web/home.rs | 18 +---- src/web/ics.rs | 12 +--- src/web/mod.rs | 9 +-- 5 files changed, 119 insertions(+), 111 deletions(-) diff --git a/src/models/contact.rs b/src/models/contact.rs index 46353ea..580d910 100644 --- a/src/models/contact.rs +++ b/src/models/contact.rs @@ -1,10 +1,18 @@ use chrono::{DateTime, NaiveDate, Utc}; -use sqlx::sqlite::SqliteRow; -use sqlx::{FromRow, Row}; +use sqlx::sqlite::SqlitePool; use std::str::FromStr; use super::Birthday; +use crate::AppError; use crate::db::DbId; +use crate::switchboard::MentionHostType; + +struct RawContact { + id: DbId, + birthday: Option, + manually_freshened_at: Option, + lives_with: String, +} #[derive(Clone, Debug)] pub struct Contact { @@ -14,6 +22,31 @@ pub struct Contact { pub lives_with: String, } +impl Into for RawContact { + fn into(self) -> Contact { + Contact { + id: self.id, + birthday: self + .birthday + .and_then(|s| Birthday::from_str(s.as_ref()).ok()), + manually_freshened_at: self + .manually_freshened_at + .and_then(|str| DateTime::parse_from_str(str.as_ref(), "%+").ok()) + .map(|d| d.to_utc()), + lives_with: self.lives_with, + } + } +} + +struct RawHydratedContact { + id: DbId, + birthday: Option, + manually_freshened_at: Option, + lives_with: String, + last_mention_date: Option, + names: Option, +} + #[derive(Clone, Debug)] pub struct HydratedContact { pub contact: Contact, @@ -21,6 +54,28 @@ pub struct HydratedContact { pub names: Vec, } +impl Into for RawHydratedContact { + fn into(self) -> HydratedContact { + HydratedContact { + contact: Into::::into(RawContact { + id: self.id, + birthday: self.birthday, + manually_freshened_at: self.manually_freshened_at, + lives_with: self.lives_with, + }), + names: self + .names + .unwrap_or(String::new()) + .split('\x1c') + .map(|s| s.to_string()) + .collect::>(), + last_mention_date: self + .last_mention_date + .and_then(|str| NaiveDate::from_str(str.as_ref()).ok()), + } + } +} + impl std::ops::Deref for HydratedContact { type Target = Contact; fn deref(&self) -> &Self::Target { @@ -36,54 +91,60 @@ impl HydratedContact { "(unnamed)".to_string() } } -} -impl FromRow<'_, SqliteRow> for Contact { - fn from_row(row: &SqliteRow) -> sqlx::Result { - let id: DbId = row.try_get("id")?; + pub async fn load(id: DbId, pool: &SqlitePool) -> Result { + // copy-paste the query from 'all', then add "where c.id = $2" to the last line + let raw = sqlx::query_as!( + RawHydratedContact, + r#"select id, birthday, lives_with, manually_freshened_at as "manually_freshened_at: String", ( + select string_agg(name,'\x1c' order by sort) + from names where contact_id = c.id + ) as names, ( + select jes.date from journal_entries jes + join mentions ms on ms.entity_id = jes.id + where ms.entity_type = $1 + and ms.url = '/contact/'||c.id + or ms.url in ( + select '/group/'||slug from groups where + contact_id = c.id + ) + order by jes.date desc limit 1 + ) as last_mention_date from contacts c + where c.id = $2"#, + MentionHostType::JournalEntry as DbId, + id + ) + .fetch_one(pool) + .await?; - let birthday = Birthday::from_row(row).ok(); + Ok(Into::::into(raw)) + } - let manually_freshened_at = row - .try_get::("manually_freshened_at") - .ok() - .and_then(|str| { - DateTime::parse_from_str(&str, "%+") - .ok() - .map(|d| d.to_utc()) - }); + pub async fn all(pool: &SqlitePool) -> Result, AppError> { + let contacts = sqlx::query_as!( + RawHydratedContact, + r#"select id, birthday, lives_with, manually_freshened_at as "manually_freshened_at: String", ( + select string_agg(name,'\x1c' order by sort) + from names where contact_id = c.id + ) as names, ( + select jes.date from journal_entries jes + join mentions ms on ms.entity_id = jes.id + where ms.entity_type = $1 + and ms.url = '/contact/'||c.id + or ms.url in ( + select '/group/'||slug from groups where + contact_id = c.id + ) + order by jes.date desc limit 1 + ) as last_mention_date from contacts c"#, + MentionHostType::JournalEntry as DbId + ) + .fetch_all(pool) + .await?; - let lives_with: String = row.try_get("lives_with")?; - - Ok(Self { - id, - birthday, - manually_freshened_at, - lives_with, - }) - } -} - -impl FromRow<'_, SqliteRow> for HydratedContact { - fn from_row(row: &SqliteRow) -> sqlx::Result { - let contact = Contact::from_row(row)?; - - let names_str: String = row.try_get("names").unwrap_or("".to_string()); - let names = if names_str.is_empty() { - vec![] - } else { - names_str.split('\x1c').map(|s| s.to_string()).collect() - }; - - let last_mention_date = row - .try_get::("last_mention_date") - .ok() - .and_then(|str| NaiveDate::from_str(&str).ok()); - - Ok(Self { - contact, - names, - last_mention_date, - }) + Ok(contacts + .into_iter() + .map(|raw| Into::::into(raw)) + .collect()) } } diff --git a/src/web/contact.rs b/src/web/contact.rs index e4c03f1..9cf7bcc 100644 --- a/src/web/contact.rs +++ b/src/web/contact.rs @@ -76,17 +76,7 @@ mod get { let user = auth_session.user.unwrap(); let pool = &state.db(&user).pool; - let contact: HydratedContact = sqlx::query_as( - "select *, ( - select string_agg(name,'\x1c' order by sort) - from names where contact_id = c.id - ) as names - from contacts c - where c.id = $1", - ) - .bind(contact_id) - .fetch_one(pool) - .await?; + let contact = HydratedContact::load(contact_id, pool).await?; let entries: Vec = sqlx::query_as( "select distinct j.id, j.value, j.date from journal_entries j @@ -231,29 +221,7 @@ mod get { layout: Layout, ) -> Result { let pool = &state.db(&auth_session.user.unwrap()).pool; - let contact: HydratedContact = sqlx::query_as( - "select *, ( - select string_agg(name,'\x1c' order by sort) - from names where contact_id = c.id - ) as names, ( - select jes.date from journal_entries jes - join mentions m on m.entity_id = jes.id - where - m.entity_type = $1 and ( - m.url = '/contact/'||c.id - or m.url in ( - select '/group/'||name - from groups - where contact_id = c.id - )) - order by jes.date desc limit 1 - ) as last_mention_date from contacts c - where c.id = $2", - ) - .bind(MentionHostType::JournalEntry as DbId) - .bind(contact_id) - .fetch_one(pool) - .await?; + let contact = HydratedContact::load(contact_id, pool).await?; let addresses: Vec
= sqlx::query_as!( Address, diff --git a/src/web/home.rs b/src/web/home.rs index a627b46..806ef77 100644 --- a/src/web/home.rs +++ b/src/web/home.rs @@ -9,7 +9,6 @@ use super::Layout; use crate::db::DbId; use crate::models::user::AuthSession; use crate::models::{Birthday, HydratedContact, JournalEntry}; -use crate::switchboard::{MentionHost, MentionHostType}; use crate::{AppError, AppState}; #[derive(Debug, Clone)] @@ -129,22 +128,7 @@ pub mod get { let user = auth_session.user.unwrap(); let pool = &state.db(&user).pool; - let contacts: Vec = sqlx::query_as( - "select *, ( - select string_agg(name,'\x1c' order by sort) - from names where contact_id = c.id - ) as names, ( - select jes.date from journal_entries jes - join mentions ms on ms.entity_id = jes.id - where ms.entity_type = $1 - and ms.url = '/contact/'||c.id - order by jes.date desc limit 1 - ) as last_mention_date from contacts c", - ) - .bind(MentionHostType::JournalEntry as DbId) - .fetch_all(pool) - .await?; - + let contacts = HydratedContact::all(&pool).await?; let mut freshens: Vec = contacts .clone() .into_iter() diff --git a/src/web/ics.rs b/src/web/ics.rs index 107e270..b7780ec 100644 --- a/src/web/ics.rs +++ b/src/web/ics.rs @@ -55,15 +55,9 @@ mod get { let mut calendar = Calendar::new(); calendar.name(&calname); calendar.append_property(("PRODID", "Mascarpone CRM")); - let contacts: Vec = sqlx::query_as( - "select id, birthday, ( - select string_agg(name,'\x1c' order by sort) - from names where contact_id = c.id - ) as names - from contacts c", - ) - .fetch_all(&pool) - .await?; + + // TODO; this does some db work to pull in last_modified_date that we don't use + let contacts = HydratedContact::all(&pool).await?; for contact in &contacts { if let Some(Birthday::Date(yo_date)) = &contact.birthday { if let Some(date) = NaiveDate::from_ymd_opt( diff --git a/src/web/mod.rs b/src/web/mod.rs index 97c0d1d..bde8e13 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -3,10 +3,10 @@ use axum::extract::FromRequestParts; use cache_bust::asset; use http::request::Parts; use maud::{DOCTYPE, Markup, html}; -use sqlx::FromRow; use super::models::user::{AuthSession, User}; use super::{AppError, AppState}; +use crate::db::DbId; pub mod auth; pub mod contact; @@ -16,10 +16,10 @@ pub mod ics; pub mod journal; pub mod settings; -#[derive(Debug, FromRow)] +#[derive(Debug)] struct ContactLink { name: String, - contact_id: u32, + contact_id: DbId, } pub struct Layout { contact_links: Vec, @@ -39,7 +39,8 @@ impl FromRequestParts for Layout { .map_err(|_| anyhow::Error::msg("could not get session"))?; let user = auth_session.user.unwrap(); - let contact_links: Vec = sqlx::query_as( + let contact_links = sqlx::query_as!( + ContactLink, "select c.id as contact_id, coalesce(n.name, '(unnamed)') as name from contacts c