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