| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
#![cfg(feature = "postgres")] |
| 6 |
|
| 7 |
use oxygenhub::config::Config; |
| 8 |
use std::collections::{BTreeMap, BTreeSet}; |
| 9 |
|
| 10 |
type Tables = BTreeMap<String, BTreeSet<String>>; |
| 11 |
|
| 12 |
|
| 13 |
|
| 14 |
fn compared(table: &str) -> bool { |
| 15 |
!table.starts_with("search") && !table.starts_with("_sqlx") |
| 16 |
} |
| 17 |
|
| 18 |
async fn sqlite() -> Tables { |
| 19 |
use sqlx::Connection; |
| 20 |
let mut conn = sqlx::SqliteConnection::connect("sqlite::memory:").await.unwrap(); |
| 21 |
sqlx::migrate!("./migrations").run(&mut conn).await.unwrap(); |
| 22 |
let names: Vec<String> = sqlx::query_scalar("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'") |
| 23 |
.fetch_all(&mut conn) |
| 24 |
.await |
| 25 |
.unwrap(); |
| 26 |
let mut out = Tables::new(); |
| 27 |
for t in names.into_iter().filter(|t| compared(t)) { |
| 28 |
let cols: Vec<String> = |
| 29 |
sqlx::query_scalar(&format!("SELECT name FROM pragma_table_info('{t}')")).fetch_all(&mut conn).await.unwrap(); |
| 30 |
out.insert(t, cols.into_iter().collect()); |
| 31 |
} |
| 32 |
out |
| 33 |
} |
| 34 |
|
| 35 |
async fn postgres() -> Tables { |
| 36 |
let dir = tempfile::tempdir().unwrap(); |
| 37 |
let config = Config::load(dir.path()).unwrap(); |
| 38 |
let pool = oxygenhub::db::open(&config).await.unwrap(); |
| 39 |
let rows: Vec<(String, String)> = sqlx::query_as( |
| 40 |
"SELECT CAST(t.relname AS TEXT), CAST(a.attname AS TEXT) FROM pg_attribute a JOIN pg_class t ON t.oid = a.attrelid \ |
| 41 |
WHERE t.relkind = 'r' AND t.relnamespace = CAST(current_schema() AS regnamespace) AND a.attnum > 0 AND NOT a.attisdropped", |
| 42 |
) |
| 43 |
.fetch_all(&pool) |
| 44 |
.await |
| 45 |
.unwrap(); |
| 46 |
let mut out = Tables::new(); |
| 47 |
for (t, c) in rows.into_iter().filter(|(t, _)| compared(t)) { |
| 48 |
out.entry(t).or_default().insert(c); |
| 49 |
} |
| 50 |
out |
| 51 |
} |
| 52 |
|
| 53 |
#[tokio::test] |
| 54 |
async fn the_postgres_schema_has_every_table_and_column_sqlite_has() { |
| 55 |
let (lite, pg) = (sqlite().await, postgres().await); |
| 56 |
let only = |a: &Tables, b: &Tables| -> Vec<String> { |
| 57 |
a.iter() |
| 58 |
.flat_map(|(t, cols)| match b.get(t) { |
| 59 |
None => vec![format!("table {t}")], |
| 60 |
Some(other) => cols.difference(other).map(|c| format!("{t}.{c}")).collect(), |
| 61 |
}) |
| 62 |
.collect() |
| 63 |
}; |
| 64 |
assert_eq!(only(&lite, &pg), Vec::<String>::new(), "in SQLite's migrations but not migrations-pg"); |
| 65 |
assert_eq!(only(&pg, &lite), Vec::<String>::new(), "in migrations-pg but not SQLite's migrations"); |
| 66 |
} |
| 67 |
|
| 68 |
|
| 69 |
#[tokio::test] |
| 70 |
async fn a_sqlite_hub_copies_into_postgres_ids_flags_forks_and_all() { |
| 71 |
use sqlx::Connection; |
| 72 |
let old = tempfile::tempdir().unwrap(); |
| 73 |
let path = old.path().join("hub.db"); |
| 74 |
let options = sqlx::sqlite::SqliteConnectOptions::new().filename(&path).create_if_missing(true); |
| 75 |
let mut lite = sqlx::SqliteConnection::connect_with(&options).await.unwrap(); |
| 76 |
sqlx::migrate!("./migrations").run(&mut lite).await.unwrap(); |
| 77 |
for sql in [ |
| 78 |
"INSERT INTO accounts (id, username, email, password_hash, session_salt, is_admin, created_ms) VALUES (7, 'Andy', '[email protected]', 'h', 's', 1, 5)", |
| 79 |
"INSERT INTO accounts (id, username, email, password_hash, session_salt, banned, created_ms) VALUES (9, 'bob', '[email protected]', 'h', 's', 1, 6)", |
| 80 |
|
| 81 |
"INSERT INTO repositories (id, owner, name, created_ms, updated_ms) VALUES (4, 'andy', 'game', 1, 1)", |
| 82 |
"INSERT INTO repositories (id, owner, name, created_ms, updated_ms, archived) VALUES (3, 'bob', 'engine', 1, 1, 1)", |
| 83 |
"UPDATE repositories SET forked_from = 3 WHERE id = 4", |
| 84 |
"INSERT INTO flow_secrets (scope, scope_id, name, value, updated_ms) VALUES ('repo', 3, 'KEY', X'00FF10', 1)", |
| 85 |
"INSERT INTO search (kind, repo_id, target_id, number, title, body) VALUES ('issue', 3, 1, 1, 'Sprites flicker', 'the sprites flicker on level two')", |
| 86 |
] { |
| 87 |
sqlx::query(sql).execute(&mut lite).await.unwrap(); |
| 88 |
} |
| 89 |
lite.close().await.unwrap(); |
| 90 |
|
| 91 |
let dir = tempfile::tempdir().unwrap(); |
| 92 |
let config = Config::load(dir.path()).unwrap(); |
| 93 |
let pool = oxygenhub::db::open(&config).await.unwrap(); |
| 94 |
let copied = oxygenhub::db::copy::from_sqlite(&pool, &path).await.map_err(|e| format!("{e:#}")).unwrap(); |
| 95 |
let count = |t: &str| copied.iter().find(|(c, _)| c == t).map(|(_, n)| *n); |
| 96 |
assert_eq!((count("accounts"), count("repositories"), count("search")), (Some(2), Some(2), Some(1))); |
| 97 |
|
| 98 |
let flags: Vec<(String, bool, bool)> = |
| 99 |
sqlx::query_as("SELECT username, is_admin, banned FROM accounts WHERE username = 'andy' OR username = 'BOB' ORDER BY id") |
| 100 |
.fetch_all(&pool) |
| 101 |
.await |
| 102 |
.unwrap(); |
| 103 |
assert_eq!(flags, [("Andy".to_string(), true, false), ("bob".to_string(), false, true)], "flags, and names without case"); |
| 104 |
let game = oxygenhub::db::repos::get(&pool, "andy", "game").await.unwrap().unwrap(); |
| 105 |
assert_eq!((game.id, game.forked_from), (4, Some(3))); |
| 106 |
let secret: Vec<u8> = sqlx::query_scalar("SELECT value FROM flow_secrets WHERE name = 'KEY'").fetch_one(&pool).await.unwrap(); |
| 107 |
assert_eq!(secret, [0x00, 0xff, 0x10]); |
| 108 |
let hits = oxygenhub::db::notify::search(&pool, "flickering sprite", None, 5).await.unwrap(); |
| 109 |
assert_eq!(hits.iter().map(|h| h.title.as_str()).collect::<Vec<_>>(), ["Sprites flicker"]); |
| 110 |
|
| 111 |
let next = oxygenhub::db::repos::create(&pool, "andy", "next", "", hub_api::Visibility::Public).await.unwrap(); |
| 112 |
assert_eq!(next.id, 5); |
| 113 |
|
| 114 |
let again = oxygenhub::db::copy::from_sqlite(&pool, &path).await.unwrap_err().to_string(); |
| 115 |
assert!(again.contains("already has rows"), "{again}"); |
| 116 |
} |