OXYGENHUB
◆ v0.9.20 oxygenhub / crates / hub / tests / schema.rs
6.5 KiB · Rust · last changed copy-db --replace: empty a rehearsal's or a fresh hub's rows first 19 days ago source blame history raw
1
//! The two schemas agree (#211): every table and column SQLite's
2
//! migrations make, `migrations-pg` makes too, so a change written for one
3
//! engine and not the other fails here. Postgres builds only, as the
4
//! Postgres tests run (`OXYHUB_DATABASE_URL`, `OXYHUB_DATABASE_SCHEMA=*`).
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
/// The full-text index is each engine's own (FTS5's shadow tables, a
13
/// generated tsvector), and the migrations' ledger is sqlx's.
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
/// A SQLite hub with a few of each kind of value, copied into Postgres.
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
        // The fork comes first: its parent is filled in after.
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, false).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
    // New rows take ids after the copied ones.
111
    let next = oxygenhub::db::repos::create(&pool, "andy", "next", "", hub_api::Visibility::Public).await.unwrap();
112
    assert_eq!(next.id, 5);
113
    // Never into a database with rows.
114
    let again = oxygenhub::db::copy::from_sqlite(&pool, &path, false).await.unwrap_err().to_string();
115
    assert!(again.contains("already has rows"), "{again}");
116
    // With --replace: the rows here (the "next" repository) are emptied and
117
    // the copy is whole again, ids from the start.
118
    let again = oxygenhub::db::copy::from_sqlite(&pool, &path, true).await.map_err(|e| format!("{e:#}")).unwrap();
119
    assert_eq!(again.iter().find(|(t, _)| t == "repositories").map(|(_, n)| *n), Some(2));
120
    assert!(oxygenhub::db::repos::get(&pool, "andy", "next").await.unwrap().is_none());
121
    let after = oxygenhub::db::repos::create(&pool, "andy", "after", "", hub_api::Visibility::Public).await.unwrap();
122
    assert_eq!(after.id, 5);
123
}