1use std::sync::{
5 OnceLock,
6 atomic::{AtomicU32, Ordering},
7};
8
9use toasty::{Db, migration::MigrationSet, schema::ModelSet};
10
11pub const TEST_DATABASE_URL: &str = "TABLO_TEST_DATABASE_URL";
13
14pub async fn test_db(models: ModelSet) -> Db {
25 let mut builder = Db::builder();
26 builder.models(models);
27 let url = match std::env::var(TEST_DATABASE_URL) {
28 Ok(url) => {
29 builder.table_name_prefix(&table_prefix());
30 url
31 }
32 Err(_) => "sqlite::memory:".to_string(),
33 };
34 let db = builder.connect(&url).await.expect("connect");
35 db.push_schema().await.expect("push_schema");
36 db
37}
38
39pub async fn migrated_db(models: ModelSet, migrations: &MigrationSet) -> Db {
59 let url = match std::env::var(TEST_DATABASE_URL) {
60 Ok(url) => isolated(models.clone(), &url).await,
61 Err(_) => "sqlite::memory:".to_string(),
62 };
63 let mut builder = Db::builder();
64 builder.models(models);
65 let db = builder.connect(&url).await.expect("connect");
66 migrations.apply(&db).await.expect("apply the migrations");
67 db
68}
69
70async fn isolated(models: ModelSet, url: &str) -> String {
74 let (scheme, _) = url.split_once(':').unwrap_or((url, ""));
75 let create = match scheme {
76 "postgres" | "postgresql" => "SCHEMA",
77 "mysql" | "mariadb" => "DATABASE",
78 _ => return url.to_string(),
79 };
80 let name = table_prefix().trim_end_matches('_').to_string();
81 let mut builder = Db::builder();
82 builder.models(models);
83 let mut admin = builder.connect(url).await.expect("connect");
84 toasty::sql::statement(format!("CREATE {create} {name}"))
85 .exec(&mut admin)
86 .await
87 .expect("create the test's namespace");
88 if create == "SCHEMA" {
89 let separator = if url.contains('?') { '&' } else { '?' };
90 format!("{url}{separator}options=-c%20search_path%3D{name}")
91 } else {
92 with_database(url, &name)
93 }
94}
95
96fn with_database(url: &str, name: &str) -> String {
98 let (base, query) = match url.split_once('?') {
99 Some((base, query)) => (base, Some(query)),
100 None => (url, None),
101 };
102 let authority_start = base.find("://").map_or(0, |at| at + 3);
103 let path_start = base[authority_start..]
104 .find('/')
105 .map_or(base.len(), |at| authority_start + at);
106 let mut out = format!("{}/{name}", &base[..path_start]);
107 if let Some(query) = query {
108 out.push('?');
109 out.push_str(query);
110 }
111 out
112}
113
114fn table_prefix() -> String {
118 static RUN: OnceLock<u32> = OnceLock::new();
119 static NEXT: AtomicU32 = AtomicU32::new(0);
120 let run = RUN.get_or_init(|| {
121 let now = std::time::SystemTime::now()
122 .duration_since(std::time::UNIX_EPOCH)
123 .unwrap_or_default();
124 now.as_secs() as u32 ^ now.subsec_nanos()
125 });
126 let call = NEXT.fetch_add(1, Ordering::Relaxed);
127 format!("t{run:08x}{call}_")
128}
129
130#[cfg(test)]
131mod tests {
132 use super::with_database;
133
134 #[test]
135 fn a_mysql_url_names_the_tests_database() {
136 assert_eq!(
137 with_database("mysql://root:pw@localhost:3306/tablo", "t1"),
138 "mysql://root:pw@localhost:3306/t1"
139 );
140 assert_eq!(
141 with_database("mysql://localhost?ssl-mode=disabled", "t1"),
142 "mysql://localhost/t1?ssl-mode=disabled"
143 );
144 }
145}