refactor: fewer non-macro queries
Some checks failed
/ integration-test--firefox (push) Failing after 3m5s

This commit is contained in:
Robert Perce 2026-01-26 22:14:58 -06:00
parent 69e23fd9bb
commit 84c41dda4d
5 changed files with 119 additions and 111 deletions

View file

@ -1,10 +1,18 @@
use chrono::{DateTime, NaiveDate, Utc}; use chrono::{DateTime, NaiveDate, Utc};
use sqlx::sqlite::SqliteRow; use sqlx::sqlite::SqlitePool;
use sqlx::{FromRow, Row};
use std::str::FromStr; use std::str::FromStr;
use super::Birthday; use super::Birthday;
use crate::AppError;
use crate::db::DbId; use crate::db::DbId;
use crate::switchboard::MentionHostType;
struct RawContact {
id: DbId,
birthday: Option<String>,
manually_freshened_at: Option<String>,
lives_with: String,
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Contact { pub struct Contact {
@ -14,6 +22,31 @@ pub struct Contact {
pub lives_with: String, pub lives_with: String,
} }
impl Into<Contact> 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<String>,
manually_freshened_at: Option<String>,
lives_with: String,
last_mention_date: Option<String>,
names: Option<String>,
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct HydratedContact { pub struct HydratedContact {
pub contact: Contact, pub contact: Contact,
@ -21,6 +54,28 @@ pub struct HydratedContact {
pub names: Vec<String>, pub names: Vec<String>,
} }
impl Into<HydratedContact> for RawHydratedContact {
fn into(self) -> HydratedContact {
HydratedContact {
contact: Into::<Contact>::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::<Vec<String>>(),
last_mention_date: self
.last_mention_date
.and_then(|str| NaiveDate::from_str(str.as_ref()).ok()),
}
}
}
impl std::ops::Deref for HydratedContact { impl std::ops::Deref for HydratedContact {
type Target = Contact; type Target = Contact;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
@ -36,54 +91,60 @@ impl HydratedContact {
"(unnamed)".to_string() "(unnamed)".to_string()
} }
} }
}
impl FromRow<'_, SqliteRow> for Contact { pub async fn load(id: DbId, pool: &SqlitePool) -> Result<Self, AppError> {
fn from_row(row: &SqliteRow) -> sqlx::Result<Self> { // copy-paste the query from 'all', then add "where c.id = $2" to the last line
let id: DbId = row.try_get("id")?; 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::<HydratedContact>::into(raw))
}
let manually_freshened_at = row pub async fn all(pool: &SqlitePool) -> Result<Vec<Self>, AppError> {
.try_get::<String, &str>("manually_freshened_at") let contacts = sqlx::query_as!(
.ok() RawHydratedContact,
.and_then(|str| { r#"select id, birthday, lives_with, manually_freshened_at as "manually_freshened_at: String", (
DateTime::parse_from_str(&str, "%+") select string_agg(name,'\x1c' order by sort)
.ok() from names where contact_id = c.id
.map(|d| d.to_utc()) ) 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(contacts
.into_iter()
Ok(Self { .map(|raw| Into::<HydratedContact>::into(raw))
id, .collect())
birthday,
manually_freshened_at,
lives_with,
})
}
}
impl FromRow<'_, SqliteRow> for HydratedContact {
fn from_row(row: &SqliteRow) -> sqlx::Result<Self> {
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::<String, &str>("last_mention_date")
.ok()
.and_then(|str| NaiveDate::from_str(&str).ok());
Ok(Self {
contact,
names,
last_mention_date,
})
} }
} }

View file

@ -76,17 +76,7 @@ mod get {
let user = auth_session.user.unwrap(); let user = auth_session.user.unwrap();
let pool = &state.db(&user).pool; let pool = &state.db(&user).pool;
let contact: HydratedContact = sqlx::query_as( let contact = HydratedContact::load(contact_id, pool).await?;
"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 entries: Vec<JournalEntry> = sqlx::query_as( let entries: Vec<JournalEntry> = sqlx::query_as(
"select distinct j.id, j.value, j.date from journal_entries j "select distinct j.id, j.value, j.date from journal_entries j
@ -231,29 +221,7 @@ mod get {
layout: Layout, layout: Layout,
) -> Result<Markup, AppError> { ) -> Result<Markup, AppError> {
let pool = &state.db(&auth_session.user.unwrap()).pool; let pool = &state.db(&auth_session.user.unwrap()).pool;
let contact: HydratedContact = sqlx::query_as( let contact = HydratedContact::load(contact_id, pool).await?;
"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 addresses: Vec<Address> = sqlx::query_as!( let addresses: Vec<Address> = sqlx::query_as!(
Address, Address,

View file

@ -9,7 +9,6 @@ use super::Layout;
use crate::db::DbId; use crate::db::DbId;
use crate::models::user::AuthSession; use crate::models::user::AuthSession;
use crate::models::{Birthday, HydratedContact, JournalEntry}; use crate::models::{Birthday, HydratedContact, JournalEntry};
use crate::switchboard::{MentionHost, MentionHostType};
use crate::{AppError, AppState}; use crate::{AppError, AppState};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -129,22 +128,7 @@ pub mod get {
let user = auth_session.user.unwrap(); let user = auth_session.user.unwrap();
let pool = &state.db(&user).pool; let pool = &state.db(&user).pool;
let contacts: Vec<HydratedContact> = sqlx::query_as( let contacts = HydratedContact::all(&pool).await?;
"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 mut freshens: Vec<ContactFreshness> = contacts let mut freshens: Vec<ContactFreshness> = contacts
.clone() .clone()
.into_iter() .into_iter()

View file

@ -55,15 +55,9 @@ mod get {
let mut calendar = Calendar::new(); let mut calendar = Calendar::new();
calendar.name(&calname); calendar.name(&calname);
calendar.append_property(("PRODID", "Mascarpone CRM")); calendar.append_property(("PRODID", "Mascarpone CRM"));
let contacts: Vec<HydratedContact> = sqlx::query_as(
"select id, birthday, ( // TODO; this does some db work to pull in last_modified_date that we don't use
select string_agg(name,'\x1c' order by sort) let contacts = HydratedContact::all(&pool).await?;
from names where contact_id = c.id
) as names
from contacts c",
)
.fetch_all(&pool)
.await?;
for contact in &contacts { for contact in &contacts {
if let Some(Birthday::Date(yo_date)) = &contact.birthday { if let Some(Birthday::Date(yo_date)) = &contact.birthday {
if let Some(date) = NaiveDate::from_ymd_opt( if let Some(date) = NaiveDate::from_ymd_opt(

View file

@ -3,10 +3,10 @@ use axum::extract::FromRequestParts;
use cache_bust::asset; use cache_bust::asset;
use http::request::Parts; use http::request::Parts;
use maud::{DOCTYPE, Markup, html}; use maud::{DOCTYPE, Markup, html};
use sqlx::FromRow;
use super::models::user::{AuthSession, User}; use super::models::user::{AuthSession, User};
use super::{AppError, AppState}; use super::{AppError, AppState};
use crate::db::DbId;
pub mod auth; pub mod auth;
pub mod contact; pub mod contact;
@ -16,10 +16,10 @@ pub mod ics;
pub mod journal; pub mod journal;
pub mod settings; pub mod settings;
#[derive(Debug, FromRow)] #[derive(Debug)]
struct ContactLink { struct ContactLink {
name: String, name: String,
contact_id: u32, contact_id: DbId,
} }
pub struct Layout { pub struct Layout {
contact_links: Vec<ContactLink>, contact_links: Vec<ContactLink>,
@ -39,7 +39,8 @@ impl FromRequestParts<AppState> for Layout {
.map_err(|_| anyhow::Error::msg("could not get session"))?; .map_err(|_| anyhow::Error::msg("could not get session"))?;
let user = auth_session.user.unwrap(); let user = auth_session.user.unwrap();
let contact_links: Vec<ContactLink> = sqlx::query_as( let contact_links = sqlx::query_as!(
ContactLink,
"select c.id as contact_id, "select c.id as contact_id,
coalesce(n.name, '(unnamed)') as name coalesce(n.name, '(unnamed)') as name
from contacts c from contacts c