db.rs (3500B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2025, 2026 Taler Systems SA 4 5 TALER is free software; you can redistribute it and/or modify it under the 6 terms of the GNU Affero General Public License as published by the Free Software 7 Foundation; either version 3, or (at your option) any later version. 8 9 TALER is distributed in the hope that it will be useful, but WITHOUT ANY 10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 12 13 You should have received a copy of the GNU Affero General Public License along with 14 TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 17 use std::{path::Path, str::FromStr as _}; 18 19 use sqlx::{ 20 Connection, PgConnection, PgPool, Postgres, pool::PoolConnection, postgres::PgConnectOptions, 21 }; 22 use taler_api::config::DbCfg; 23 use taler_common::{ 24 config::{Config, parser::ConfigSource}, 25 db::{dbinit, pool}, 26 }; 27 use tracing::info; 28 29 use crate::setup_tracing; 30 31 /// Create a reusable test database and run dbinit for a Taler component 32 pub async fn db_test_setup(src: ConfigSource) -> (PoolConnection<Postgres>, PgPool) { 33 let cfg = Config::load(src, None::<&str>).unwrap(); 34 let name = format!("{}db-postgres", src.component_name); 35 let sect = cfg.section(&name); 36 let db_cfg = DbCfg::parse(sect).unwrap(); 37 db_test_setup_manual(db_cfg.sql_dir.as_ref(), src.component_name).await 38 } 39 40 /// Create a reusable test database and run dbinit for a Taler component 41 pub async fn db_test_setup_manual( 42 sql_dir: &Path, 43 component_name: &str, 44 ) -> (PoolConnection<Postgres>, PgPool) { 45 setup_tracing(); 46 let cfg = test_db().await; 47 let pool = pool(cfg, &component_name.replace("-", "_")).await.unwrap(); 48 let mut conn = pool.acquire().await.unwrap(); 49 50 dbinit(&mut conn, sql_dir, component_name, true) 51 .await 52 .unwrap(); 53 54 (conn, pool) 55 } 56 57 /// Create a temporary test database that will be reuse by future tests 58 pub async fn test_db() -> PgConnectOptions { 59 let mut conn = PgConnection::connect("postgres:///taler_rust_check") 60 .await 61 .unwrap(); 62 63 // Find a free slot via Advisory Locks 64 let row: Option<(String, bool)> = sqlx::query_as( 65 " 66 SELECT 67 v.db_name, 68 EXISTS ( 69 SELECT 1 70 FROM pg_catalog.pg_database 71 WHERE datname = v.db_name 72 ) AS already_exists 73 FROM generate_series(0, 1000) AS id 74 CROSS JOIN LATERAL ( 75 SELECT CASE 76 WHEN id = 0 THEN 'taler_rust_test' 77 ELSE 'taler_rust_test_' || id 78 END AS db_name 79 ) AS v 80 WHERE pg_try_advisory_lock(id) 81 LIMIT 1; 82 ", 83 ) 84 .fetch_optional(&mut conn) 85 .await 86 .unwrap(); 87 88 let Some((name, exists)) = row else { 89 panic!("Could not find a free database slot after 1000 attempts.") 90 }; 91 if !exists { 92 sqlx::raw_sql(&format!("CREATE DATABASE {name}")) 93 .execute(&mut conn) 94 .await 95 .unwrap(); 96 } 97 // We need this connection to stay open to keep the advisory lock in place 98 // Leaking it is OK in tests 99 std::mem::forget(conn); 100 let db_url = format!("postgresql:///{name}"); 101 info!(target: "test", "Running on db {db_url}"); 102 PgConnectOptions::from_str(&db_url).unwrap() 103 }