-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Expand file tree
/
Copy pathmigration.rs
More file actions
244 lines (210 loc) · 7.54 KB
/
Copy pathmigration.rs
File metadata and controls
244 lines (210 loc) · 7.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
//! Embedded SQLx migrations for Buzz.
//!
//! Fresh deployments apply the checked-in SQL files under `migrations/`.
//! Existing pre-SQLx deployments are baselined when core Buzz tables already
//! exist but `_sqlx_migrations` does not, so startup will not try to replay the
//! initial schema over a live database.
use sqlx::PgPool;
use crate::Result;
static MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!("../../migrations");
#[cfg(test)]
static SCHEMA_SQL: &str = include_str!("../../../schema/schema.sql");
const BASELINE_MIGRATION_VERSIONS: &[i64] = &[1, 2];
/// Run all pending Buzz database migrations.
pub async fn run_migrations(pool: &PgPool) -> Result<()> {
baseline_existing_database(pool).await?;
MIGRATOR.run(pool).await?;
Ok(())
}
async fn baseline_existing_database(pool: &PgPool) -> Result<()> {
if migrations_table_exists(pool).await? || !pre_sqlx_schema_exists(pool).await? {
return Ok(());
}
ensure_migrations_table(pool).await?;
for version in BASELINE_MIGRATION_VERSIONS {
let migration = MIGRATOR
.iter()
.find(|migration| migration.version == *version)
.expect("baseline migration version must exist in embedded migrator");
sqlx::query(
r#"
INSERT INTO _sqlx_migrations
(version, description, success, checksum, execution_time)
VALUES ($1, $2, TRUE, $3, 0)
ON CONFLICT (version) DO NOTHING
"#,
)
.bind(migration.version)
.bind(&*migration.description)
.bind(&*migration.checksum)
.execute(pool)
.await?;
}
tracing::info!(
versions = ?BASELINE_MIGRATION_VERSIONS,
"Baselined existing Buzz database for SQLx migrations"
);
Ok(())
}
async fn migrations_table_exists(pool: &PgPool) -> Result<bool> {
let exists = sqlx::query_scalar::<_, bool>(
r#"
SELECT EXISTS (
SELECT 1
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = '_sqlx_migrations'
)
"#,
)
.fetch_one(pool)
.await?;
Ok(exists)
}
async fn pre_sqlx_schema_exists(pool: &PgPool) -> Result<bool> {
let exists = sqlx::query_scalar::<_, bool>(
r#"
SELECT EXISTS (
SELECT 1
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'events'
) AND EXISTS (
SELECT 1
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'channels'
)
"#,
)
.fetch_one(pool)
.await?;
Ok(exists)
}
async fn ensure_migrations_table(pool: &PgPool) -> Result<()> {
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS _sqlx_migrations (
version BIGINT PRIMARY KEY,
description TEXT NOT NULL,
installed_on TIMESTAMPTZ NOT NULL DEFAULT now(),
success BOOLEAN NOT NULL,
checksum BYTEA NOT NULL,
execution_time BIGINT NOT NULL
)
"#,
)
.execute(pool)
.await?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use sqlx::PgPool;
const TEST_DB_URL: &str = "postgres://buzz:buzz_dev@localhost:5432/buzz";
#[test]
fn embedded_migrator_contains_initial_schema_and_d_tag_backfill() {
let migrations: Vec<_> = MIGRATOR.iter().collect();
assert_eq!(migrations.len(), 2);
assert_eq!(migrations[0].version, 1);
assert_eq!(&*migrations[0].description, "initial schema");
assert!(
migrations[0].sql.as_str().contains("CREATE TABLE channels"),
"initial schema migration should include Buzz core tables"
);
assert!(
migrations[0]
.sql
.as_str()
.contains("CREATE TABLE IF NOT EXISTS relay_members"),
"initial schema migration should include relay_members"
);
assert_eq!(migrations[1].version, 2);
assert_eq!(&*migrations[1].description, "backfill d tag");
assert!(
migrations[1].sql.as_str().contains("UPDATE events"),
"second migration should backfill existing event rows"
);
}
async fn connect_test_pool() -> PgPool {
let database_url = std::env::var("BUZZ_TEST_DATABASE_URL")
.or_else(|_| std::env::var("DATABASE_URL"))
.unwrap_or_else(|_| TEST_DB_URL.to_owned());
PgPool::connect(&database_url)
.await
.expect("connect to test DB")
}
async fn reset_public_schema(pool: &PgPool) {
sqlx::query("DROP SCHEMA IF EXISTS public CASCADE")
.execute(pool)
.await
.expect("drop public schema");
sqlx::query("CREATE SCHEMA IF NOT EXISTS public")
.execute(pool)
.await
.expect("create public schema");
}
async fn applied_versions(pool: &PgPool) -> Vec<i64> {
sqlx::query_scalar::<_, i64>(
"SELECT version FROM _sqlx_migrations WHERE success ORDER BY version",
)
.fetch_all(pool)
.await
.expect("read applied migrations")
}
#[tokio::test]
#[ignore = "requires Postgres"]
async fn run_migrations_applies_embedded_versions_on_fresh_database() {
let pool = connect_test_pool().await;
reset_public_schema(&pool).await;
run_migrations(&pool).await.expect("run migrations");
assert_eq!(applied_versions(&pool).await, vec![1, 2]);
let events_exists = sqlx::query_scalar::<_, bool>(
"SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'events')",
)
.fetch_one(&pool)
.await
.expect("check events table");
assert!(events_exists);
}
#[tokio::test]
#[ignore = "requires Postgres"]
async fn run_migrations_baselines_existing_schema_and_preserves_allowlist_backfill_path() {
let pool = connect_test_pool().await;
reset_public_schema(&pool).await;
sqlx::raw_sql(SCHEMA_SQL)
.execute(&pool)
.await
.expect("load pre-SQLx schema snapshot");
sqlx::query(
"INSERT INTO pubkey_allowlist (pubkey, added_at) VALUES (decode($1, 'hex'), now())",
)
.bind("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
.execute(&pool)
.await
.expect("seed legacy allowlist row");
run_migrations(&pool).await.expect("baseline migrations");
assert_eq!(applied_versions(&pool).await, vec![1, 2]);
let allowlist_count = sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM pubkey_allowlist")
.fetch_one(&pool)
.await
.expect("count allowlist rows");
assert_eq!(
allowlist_count, 1,
"baseline must not drop legacy allowlist rows before relay startup backfills them"
);
let inserted = crate::relay_members::backfill_from_allowlist(&pool)
.await
.expect("backfill legacy allowlist rows");
assert_eq!(inserted, 1);
let relay_member_count = sqlx::query_scalar::<_, i64>(
"SELECT COUNT(*) FROM relay_members WHERE pubkey = $1 AND role = 'member'",
)
.bind("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
.fetch_one(&pool)
.await
.expect("count backfilled relay member");
assert_eq!(relay_member_count, 1);
}
}