ratings.rs (2398B)
1 //! Rating repository functions with OpenTelemetry instrumentation 2 //! 3 //! This module contains database operations for product ratings, 4 //! separated from HTTP handlers. 5 6 use sqlx::PgPool; 7 use tracing::instrument; 8 use uuid::Uuid; 9 10 use crate::models::{Rating, UpsertRatingRequest}; 11 12 /// Check if a product exists and is not deleted 13 #[instrument( 14 name = "SELECT products", 15 skip(pool), 16 fields( 17 db.system.name = "postgresql", 18 db.namespace = "products", 19 db.operation.name = "SELECT", 20 db.collection.name = "products", 21 db.query.text = "SELECT EXISTS(SELECT 1 FROM products WHERE uuid = $1 AND deleted_at IS NULL)", 22 otelmart.product.uuid = %product_uuid 23 ) 24 )] 25 pub async fn product_exists(pool: &PgPool, product_uuid: Uuid) -> Result<bool, sqlx::Error> { 26 let result: Option<(bool,)> = sqlx::query_as( 27 "SELECT EXISTS(SELECT 1 FROM products WHERE uuid = $1 AND deleted_at IS NULL)", 28 ) 29 .bind(product_uuid) 30 .fetch_optional(pool) 31 .await?; 32 33 Ok(result.map(|x| x.0).unwrap_or(false)) 34 } 35 36 /// Create or update a product rating 37 /// 38 /// Uses PostgreSQL's ON CONFLICT clause for atomic upsert. 39 /// The unique constraint on (product_id, user_id) ensures one rating per user per product. 40 #[instrument( 41 name = "UPSERT ratings", 42 skip(pool, payload), 43 fields( 44 db.system.name = "postgresql", 45 db.namespace = "products", 46 db.operation.name = "INSERT", 47 db.collection.name = "ratings", 48 db.query.text = "INSERT INTO ratings (...) VALUES (...) ON CONFLICT DO UPDATE ... RETURNING *", 49 otelmart.product.uuid = %product_uuid, 50 otelmart.rating.value = payload.rating 51 ) 52 )] 53 pub async fn upsert_rating( 54 pool: &PgPool, 55 product_uuid: Uuid, 56 payload: &UpsertRatingRequest, 57 ) -> Result<Rating, sqlx::Error> { 58 sqlx::query_as::<_, Rating>( 59 r#" 60 INSERT INTO ratings (product_id, user_id, rating, review) 61 VALUES ($1, $2, $3, $4) 62 ON CONFLICT (product_id, user_id) 63 DO UPDATE SET 64 rating = EXCLUDED.rating, 65 review = EXCLUDED.review, 66 updated_at = NOW() 67 RETURNING id, uuid, product_id, user_id, rating, review, created_at, updated_at 68 "#, 69 ) 70 .bind(product_uuid) 71 .bind(payload.user_id) 72 .bind(payload.rating) 73 .bind(&payload.review) 74 .fetch_one(pool) 75 .await 76 }