From d3dab49af67f3395c5541358557f38ad64d02269 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Mon, 13 Apr 2026 22:31:22 -0400 Subject: [PATCH] static sqlx queries --- .claude/rules/code-style.md | 3 ++- CLAUDE.md | 2 +- .../insert_staff_audit_log.rs | 14 +++++----- .../insert_user_impersonation_request.rs | 14 +++++----- .../lookup_user_impersonation_request.rs | 27 ++++++------------- .../mark_impersonation_token_as_redeemed.rs | 6 ++--- .../create_impersonated_user_session.rs | 12 ++++----- .../endpoints/users/login_handler.rs | 3 ++- 8 files changed, 36 insertions(+), 45 deletions(-) diff --git a/.claude/rules/code-style.md b/.claude/rules/code-style.md index 4e1b5f8b48..2afc76a48b 100644 --- a/.claude/rules/code-style.md +++ b/.claude/rules/code-style.md @@ -6,7 +6,8 @@ - No minimum supported Rust version; use latest stable features freely - `#[macro_use] extern crate serde_derive;` is used in binary crates; library crates use `use serde_derive::{Serialize, Deserialize};` - Never use `println!` or `eprintln!` outside of tests; use `log` crate macros (`info!`, `warn!`, `error!`, `debug!`, `trace!`) instead -- Use `anyhow::Result` / `AnyhowResult` for fallible functions in application code +- Use `anyhow::Result` / `AnyhowResult` for fallible functions in application code; new database queries should use `Result` instead +- SQLx queries: prefer `sqlx::query!` and `sqlx::query_as!` compile-time checked macros over runtime `sqlx::query()` / `sqlx::query_as()`. The macros catch SQL typos, column mismatches, and type errors at build time. Use runtime queries only when the table isn't in the offline cache yet. - Enum Display/Debug: use `impl_enum_display_and_debug_using_to_str!` macro, not `derive_more::Display` (CLion doesn't understand it) - Fields that are optional use the `maybe_` prefix: `maybe_creator_user_token`, `maybe_prompt_token` - When implementing `fmt::Display` for error types, use the pattern: `write!(f, "{:?}", self)` diff --git a/CLAUDE.md b/CLAUDE.md index 3c05924808..8b91a4cb30 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -58,7 +58,7 @@ artcraft/ - Rust with no minimum supported version - Actix-web for HTTP services -- SQLx for MySQL and SQLite +- SQLx for MySQL and SQLite; prefer `sqlx::query!` / `sqlx::query_as!` compile-time checked macros over runtime `sqlx::query()` whenever possible - A mix of wreq and reqwest for Rust HTTP clients - Never use `println!` or `eprintln!` outside of tests; use `log` crate macros instead - When two crates export the same type name, alias imports with a suffix: `use foo::Bar as BarFoo;` diff --git a/crates/schema/database/mysql_queries/src/queries/staff_audit_logs/insert_staff_audit_log.rs b/crates/schema/database/mysql_queries/src/queries/staff_audit_logs/insert_staff_audit_log.rs index a15a4e389c..0021ff3b17 100644 --- a/crates/schema/database/mysql_queries/src/queries/staff_audit_logs/insert_staff_audit_log.rs +++ b/crates/schema/database/mysql_queries/src/queries/staff_audit_logs/insert_staff_audit_log.rs @@ -29,7 +29,7 @@ pub async fn insert_staff_audit_log<'a, 'c, E>( { let token = StaffAuditLogToken::generate(); - sqlx::query( + sqlx::query!( r#" INSERT INTO staff_audit_logs SET @@ -40,13 +40,13 @@ SET staff_user_token = ?, staff_ip_address = ? "#, + token.as_str(), + args.audit_action.to_str(), + args.maybe_entity_type.map(|ty| ty.to_str()), + args.maybe_entity_token, + args.staff_user_token.as_str(), + args.actor_ip_address, ) - .bind(token.as_str()) - .bind(args.audit_action.to_str()) - .bind(args.maybe_entity_type.map(|ty| ty.to_str())) - .bind(args.maybe_entity_token) - .bind(args.staff_user_token.as_str()) - .bind(args.actor_ip_address) .execute(args.mysql_executor) .await?; diff --git a/crates/schema/database/mysql_queries/src/queries/user_impersonation_requests/insert_user_impersonation_request.rs b/crates/schema/database/mysql_queries/src/queries/user_impersonation_requests/insert_user_impersonation_request.rs index 3a0d0d84f6..a58ebe06f8 100644 --- a/crates/schema/database/mysql_queries/src/queries/user_impersonation_requests/insert_user_impersonation_request.rs +++ b/crates/schema/database/mysql_queries/src/queries/user_impersonation_requests/insert_user_impersonation_request.rs @@ -26,7 +26,7 @@ pub async fn insert_user_impersonation_request<'a, 'c, E>( { let token = UserImpersonationRequestToken::generate(); - sqlx::query( + sqlx::query!( r#" INSERT INTO user_impersonation_requests SET @@ -37,13 +37,13 @@ SET ip_address_creation = ?, expires_at = ? "#, + token.as_str(), + args.impersonated_user_token.as_str(), + args.impersonator_user_token.as_str(), + args.public_impersonation_token, + args.ip_address_creation, + args.expires_at, ) - .bind(token.as_str()) - .bind(args.impersonated_user_token.as_str()) - .bind(args.impersonator_user_token.as_str()) - .bind(args.public_impersonation_token) - .bind(args.ip_address_creation) - .bind(args.expires_at) .execute(args.mysql_executor) .await?; diff --git a/crates/schema/database/mysql_queries/src/queries/user_impersonation_requests/lookup_user_impersonation_request.rs b/crates/schema/database/mysql_queries/src/queries/user_impersonation_requests/lookup_user_impersonation_request.rs index c99f46f59d..f7639840eb 100644 --- a/crates/schema/database/mysql_queries/src/queries/user_impersonation_requests/lookup_user_impersonation_request.rs +++ b/crates/schema/database/mysql_queries/src/queries/user_impersonation_requests/lookup_user_impersonation_request.rs @@ -1,5 +1,5 @@ use chrono::{DateTime, Utc}; -use sqlx::{FromRow, MySqlPool, Row}; +use sqlx::MySqlPool; use tokens::tokens::users::UserToken; @@ -17,35 +17,24 @@ pub async fn lookup_user_impersonation_request( public_impersonation_token: &str, mysql_pool: &MySqlPool, ) -> Result, sqlx::Error> { - let maybe_row = sqlx::query( + let maybe_record = sqlx::query_as!( + UserImpersonationRequestRecord, r#" SELECT token, - impersonated_user_token, - impersonator_user_token, + impersonated_user_token as `impersonated_user_token: UserToken`, + impersonator_user_token as `impersonator_user_token: UserToken`, public_impersonation_token, - is_redeemed, + is_redeemed as `is_redeemed: bool`, expires_at FROM user_impersonation_requests WHERE public_impersonation_token = ? LIMIT 1 "#, + public_impersonation_token, ) - .bind(public_impersonation_token) .fetch_optional(mysql_pool) .await?; - let row = match maybe_row { - Some(row) => row, - None => return Ok(None), - }; - - Ok(Some(UserImpersonationRequestRecord { - token: row.get("token"), - impersonated_user_token: UserToken::new_from_str(row.get("impersonated_user_token")), - impersonator_user_token: UserToken::new_from_str(row.get("impersonator_user_token")), - public_impersonation_token: row.get("public_impersonation_token"), - is_redeemed: row.get("is_redeemed"), - expires_at: row.get("expires_at"), - })) + Ok(maybe_record) } diff --git a/crates/schema/database/mysql_queries/src/queries/user_impersonation_requests/mark_impersonation_token_as_redeemed.rs b/crates/schema/database/mysql_queries/src/queries/user_impersonation_requests/mark_impersonation_token_as_redeemed.rs index 9950a14ad6..a7fbe3bf58 100644 --- a/crates/schema/database/mysql_queries/src/queries/user_impersonation_requests/mark_impersonation_token_as_redeemed.rs +++ b/crates/schema/database/mysql_queries/src/queries/user_impersonation_requests/mark_impersonation_token_as_redeemed.rs @@ -17,7 +17,7 @@ pub async fn mark_impersonation_token_as_redeemed<'a, 'c, E>( ) -> Result<(), sqlx::Error> where E: 'a + Executor<'c, Database = MySql> { - sqlx::query( + sqlx::query!( r#" UPDATE user_impersonation_requests SET @@ -25,9 +25,9 @@ SET ip_address_redemption = ? WHERE public_impersonation_token = ? "#, + args.ip_address_redemption, + args.public_impersonation_token, ) - .bind(args.ip_address_redemption) - .bind(args.public_impersonation_token) .execute(args.mysql_executor) .await?; diff --git a/crates/schema/database/mysql_queries/src/queries/users/user_sessions/create_impersonated_user_session.rs b/crates/schema/database/mysql_queries/src/queries/users/user_sessions/create_impersonated_user_session.rs index b194d36e8c..0da5e6e8d0 100644 --- a/crates/schema/database/mysql_queries/src/queries/users/user_sessions/create_impersonated_user_session.rs +++ b/crates/schema/database/mysql_queries/src/queries/users/user_sessions/create_impersonated_user_session.rs @@ -29,7 +29,7 @@ pub async fn create_impersonated_user_session<'a, 'c, E>( { let session_token = UserSessionToken::generate(); - sqlx::query( + sqlx::query!( r#" INSERT INTO user_sessions ( token, @@ -40,12 +40,12 @@ INSERT INTO user_sessions ( ) VALUES (?, ?, ?, ?, ?) "#, + session_token.as_str(), + args.user_token.as_str(), + args.impersonator_user_token.as_str(), + args.ip_address, + args.expires_at, ) - .bind(session_token.as_str()) - .bind(args.user_token.as_str()) - .bind(args.impersonator_user_token.as_str()) - .bind(args.ip_address) - .bind(args.expires_at) .execute(args.mysql_executor) .await?; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/users/login_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/users/login_handler.rs index f1a9a77419..4a61c95358 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/users/login_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/users/login_handler.rs @@ -32,6 +32,7 @@ use utoipa::ToSchema; use artcraft_api_defs::users::login::{LoginErrorType, LoginRequest, LoginSuccessResponse}; use password::errors::password_confirm_error::PasswordConfirmError; +use tokens::tokens::users::UserToken; use crate::http_server::session::lookup::user_session_feature_flags::UserSessionFeatureFlags; use crate::util::enroll_in_studio::enroll_in_studio; @@ -224,7 +225,7 @@ pub async fn login_handler( /// Returns the impersonated user's session token on success, or an error if the /// impersonation token is invalid, expired, already redeemed, or doesn't match. async fn try_impersonation_login( - target_user_token: &tokens::tokens::users::UserToken, + target_user_token: &UserToken, password: &str, ip_address: &str, mysql_pool: &MySqlPool,