| d23464c0 Andy McCall ↶ |
1 |
|
| 3b1bc27f Andy McCall |
2 |
|
| 3 |
|
| d23464c0 Andy McCall ↶ |
4 |
|
| 19bb31ff Andy McCall ↶ |
5 |
pub mod accounts; |
| 25ab8c73 Andy McCall ↶ |
6 |
pub mod activity; |
| 4e2cad00 Andy McCall ↶ |
7 |
#[cfg(feature = "postgres")] |
| 8 |
pub mod copy; |
| 42fa9554 Andy McCall ↶ |
9 |
pub mod flow; |
| b64e2537 Andy McCall ↶ |
10 |
pub mod hooks; |
| 18e381ea Andy McCall ↶ |
11 |
pub mod issues; |
| 12 |
pub mod notify; |
| e1ec9fbf Andy McCall ↶ |
13 |
pub mod projects; |
| de505df5 Andy McCall ↶ |
14 |
pub mod proposals; |
| 0ee5c3fb Andy McCall ↶ |
15 |
pub mod push_protection; |
| 3f4b0ea6 Andy McCall ↶ |
16 |
pub mod releases; |
| d23464c0 Andy McCall ↶ |
17 |
pub mod repos; |
| b6b7e4cf Andy McCall ↶ |
18 |
pub mod runners; |
| 0024ccb1 Andy McCall ↶ |
19 |
pub mod sessions; |
| 8dcaeeb8 Andy McCall ↶ |
20 |
pub mod terraform; |
| 965b950f Andy McCall ↶ |
21 |
pub mod tlog; |
| d23464c0 Andy McCall ↶ |
22 |
|
| 3b1bc27f Andy McCall |
23 |
#[cfg(not(feature = "postgres"))] |
| 24 |
pub use sqlite::{begin_write, open, Db}; |
| d23464c0 Andy McCall ↶ |
25 |
|
| 3b1bc27f Andy McCall |
26 |
#[cfg(feature = "postgres")] |
| 27 |
pub use postgres::{begin_write, open, Db}; |
| d23464c0 Andy McCall ↶ |
28 |
|
| 3b1bc27f Andy McCall |
29 |
pub type Pool = sqlx::Pool<Db>; |
| eae4a974 Andy McCall ↶ |
30 |
|
| 3b1bc27f Andy McCall |
31 |
#[cfg(not(feature = "postgres"))] |
| 32 |
mod sqlite { |
| 33 |
use super::Pool; |
| 34 |
use crate::config::Config; |
| 35 |
use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions, SqliteSynchronous}; |
| 36 |
|
| 37 |
pub type Db = sqlx::Sqlite; |
| 38 |
|
| 39 |
|
| 40 |
|
| 41 |
|
| 42 |
|
| 43 |
const BUSY_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30); |
| 44 |
|
| 45 |
|
| 46 |
pub async fn open(config: &Config) -> anyhow::Result<Pool> { |
| 47 |
let options = SqliteConnectOptions::new() |
| 48 |
.filename(config.db_path()) |
| 49 |
.create_if_missing(true) |
| 50 |
.journal_mode(SqliteJournalMode::Wal) |
| 51 |
.synchronous(SqliteSynchronous::Normal) |
| 52 |
.foreign_keys(true) |
| 53 |
.busy_timeout(BUSY_TIMEOUT); |
| 54 |
migrate(options.clone()).await?; |
| 55 |
let pool = SqlitePoolOptions::new().max_connections(8).connect_with(options).await?; |
| 56 |
Ok(pool) |
| 57 |
} |
| 58 |
|
| 59 |
|
| 60 |
|
| 61 |
|
| 62 |
|
| 63 |
async fn migrate(options: SqliteConnectOptions) -> anyhow::Result<()> { |
| 64 |
use sqlx::Connection; |
| 65 |
let mut conn = sqlx::SqliteConnection::connect_with(&options.foreign_keys(false)).await?; |
| 66 |
sqlx::migrate!("./migrations").run(&mut conn).await?; |
| 67 |
let broken: Vec<(String,)> = sqlx::query_as("SELECT \"table\" FROM pragma_foreign_key_check").fetch_all(&mut conn).await?; |
| 68 |
if let Some((table,)) = broken.first() { |
| 69 |
anyhow::bail!("after the migrations, {} rows break a foreign key (the first in {table})", broken.len()); |
| 70 |
} |
| 71 |
conn.close().await?; |
| 72 |
Ok(()) |
| 73 |
} |
| 74 |
|
| 75 |
|
| 76 |
|
| 77 |
|
| 78 |
|
| 79 |
pub async fn begin_write(pool: &Pool) -> sqlx::Result<sqlx::Transaction<'static, Db>> { |
| 80 |
pool.begin_with("BEGIN IMMEDIATE").await |
| 81 |
} |
| d23464c0 Andy McCall ↶ |
82 |
} |
| 3b1bc27f Andy McCall |
83 |
|
| 84 |
|
| 85 |
|
| 86 |
|
| 87 |
#[cfg(feature = "postgres")] |
| 88 |
mod postgres { |
| 89 |
use super::Pool; |
| 90 |
use crate::config::Config; |
| 91 |
use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; |
| 92 |
use sqlx::Executor; |
| d23464c0 Andy McCall ↶ |
93 |
|
| 3b1bc27f Andy McCall |
94 |
pub type Db = sqlx::Postgres; |
| 95 |
|
| 96 |
|
| 97 |
pub async fn open(config: &Config) -> anyhow::Result<Pool> { |
| 98 |
if config.database.url.is_empty() { |
| 99 |
anyhow::bail!("this hub is built for PostgreSQL: set [database] url (or OXYHUB_DATABASE_URL)"); |
| 100 |
} |
| 101 |
let options: PgConnectOptions = config.database.url.parse()?; |
| 102 |
let schema = schema_name(config); |
| 103 |
if let Some(schema) = &schema { |
| 104 |
let pool = PgPoolOptions::new().max_connections(1).connect_with(options.clone()).await?; |
| 105 |
pool.execute(format!("CREATE SCHEMA IF NOT EXISTS \"{schema}\"").as_str()).await?; |
| 106 |
pool.close().await; |
| 107 |
} |
| 108 |
let path = match &schema { |
| 109 |
|
| 110 |
Some(s) => format!("{s},public"), |
| 111 |
None => "public".to_string(), |
| 112 |
}; |
| 113 |
let options = options.options([("search_path", path.as_str())]); |
| 114 |
let pool = PgPoolOptions::new().max_connections(16).connect_with(options).await?; |
| 115 |
extension(&pool).await?; |
| 116 |
sqlx::migrate!("./migrations-pg").run(&pool).await?; |
| 117 |
Ok(pool) |
| 745d225d Andy McCall ↶ |
118 |
} |
| 3b1bc27f Andy McCall |
119 |
|
| 120 |
|
| 121 |
|
| 122 |
async fn extension(pool: &Pool) -> anyhow::Result<()> { |
| 123 |
let made = pool.execute("CREATE EXTENSION IF NOT EXISTS citext WITH SCHEMA public").await; |
| 124 |
if let Err(e) = made { |
| 125 |
let there: bool = |
| 126 |
sqlx::query_scalar("SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'citext')").fetch_one(pool).await?; |
| 127 |
if !there { |
| 128 |
return Err(e.into()); |
| 129 |
} |
| 130 |
} |
| 131 |
Ok(()) |
| 132 |
} |
| 133 |
|
| 134 |
|
| 135 |
|
| 136 |
fn schema_name(config: &Config) -> Option<String> { |
| 137 |
let s = match config.database.schema.as_str() { |
| 138 |
"" | "public" => return None, |
| 139 |
"*" => { |
| 140 |
let dir = config.data.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default(); |
| 141 |
format!("hub_{dir}") |
| 142 |
} |
| 143 |
s => s.to_string(), |
| 144 |
}; |
| 145 |
Some(s.chars().map(|c| if c.is_ascii_alphanumeric() { c.to_ascii_lowercase() } else { '_' }).collect()) |
| 146 |
} |
| 147 |
|
| 148 |
|
| 149 |
|
| 150 |
|
| 151 |
pub async fn begin_write(pool: &Pool) -> sqlx::Result<sqlx::Transaction<'static, Db>> { |
| 152 |
let mut tx = pool.begin().await?; |
| 153 |
sqlx::query("SELECT pg_advisory_xact_lock(7300211)").execute(&mut *tx).await?; |
| 154 |
Ok(tx) |
| 155 |
} |
| 745d225d Andy McCall ↶ |
156 |
} |
| 157 |
|
| 3b1bc27f Andy McCall |
158 |
|
| 159 |
|
| 160 |
pub fn json_text(col: &str, key: &str) -> String { |
| 161 |
if cfg!(feature = "postgres") { |
| 162 |
format!("(CAST(NULLIF({col}, '') AS json) ->> '{key}')") |
| 163 |
} else { |
| 164 |
format!("json_extract({col}, '$.{key}')") |
| 165 |
} |
| 166 |
} |
| 167 |
|
| d23464c0 Andy McCall ↶ |
168 |
|
| 169 |
pub fn now_ms() -> i64 { |
| 170 |
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_millis() as i64).unwrap_or(0) |
| 941abdd6 Andy McCall ↶ |
171 |
} |