mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-11-30 08:11:20 +00:00
diff check
This commit is contained in:
parent
8e0bbd61eb
commit
1692fd62b4
6 changed files with 130 additions and 57 deletions
13
Cargo.lock
generated
13
Cargo.lock
generated
|
@ -2821,6 +2821,7 @@ dependencies = [
|
||||||
"diesel-derive-newtype",
|
"diesel-derive-newtype",
|
||||||
"diesel_ltree",
|
"diesel_ltree",
|
||||||
"diesel_migrations",
|
"diesel_migrations",
|
||||||
|
"diff",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"i-love-jesus",
|
"i-love-jesus",
|
||||||
"lemmy_utils",
|
"lemmy_utils",
|
||||||
|
@ -2833,7 +2834,6 @@ dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_with",
|
"serde_with",
|
||||||
"serial_test",
|
"serial_test",
|
||||||
"string-interner",
|
|
||||||
"strum",
|
"strum",
|
||||||
"strum_macros",
|
"strum_macros",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
@ -5229,17 +5229,6 @@ dependencies = [
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "string-interner"
|
|
||||||
version = "0.17.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1c6a0d765f5807e98a091107bae0a56ea3799f66a5de47b2c84c94a39c09974e"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"hashbrown 0.14.3",
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "string_cache"
|
name = "string_cache"
|
||||||
version = "0.8.7"
|
version = "0.8.7"
|
||||||
|
|
|
@ -85,7 +85,7 @@ moka.workspace = true
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serial_test = { workspace = true }
|
serial_test = { workspace = true }
|
||||||
pretty_assertions = { workspace = true }
|
pretty_assertions = { workspace = true }
|
||||||
string-interner = { version = "0.17.0", features = ["inline-more", "backends"] }
|
diff = "0.1.13"
|
||||||
|
|
||||||
[package.metadata.cargo-machete]
|
[package.metadata.cargo-machete]
|
||||||
ignored = ["strum"]
|
ignored = ["strum"]
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod diff_checker;
|
mod diff_check;
|
||||||
|
|
||||||
use crate::schema::previously_run_sql;
|
use crate::schema::previously_run_sql;
|
||||||
use anyhow::{anyhow, Context};
|
use anyhow::{anyhow, Context};
|
||||||
|
@ -17,11 +17,9 @@ use diesel::{
|
||||||
RunQueryDsl,
|
RunQueryDsl,
|
||||||
};
|
};
|
||||||
use diesel_migrations::MigrationHarness;
|
use diesel_migrations::MigrationHarness;
|
||||||
use lemmy_utils::error::{LemmyError, LemmyResult};
|
use lemmy_utils::{error::LemmyResult, settings::SETTINGS};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
use lemmy_utils::settings::SETTINGS;
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
|
|
||||||
// In production, include migrations in the binary
|
// In production, include migrations in the binary
|
||||||
#[cfg(not(debug_assertions))]
|
#[cfg(not(debug_assertions))]
|
||||||
|
@ -47,21 +45,31 @@ const REPLACEABLE_SCHEMA: &[&str] = &[
|
||||||
include_str!("../replaceable_schema/triggers.sql"),
|
include_str!("../replaceable_schema/triggers.sql"),
|
||||||
];
|
];
|
||||||
|
|
||||||
struct MigrationHarnessWrapper<'a> {
|
struct MigrationHarnessWrapper<'a, 'b> {
|
||||||
conn: &'a mut PgConnection,
|
conn: &'a mut PgConnection,
|
||||||
|
options: &'b Options,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> MigrationHarness<Pg> for MigrationHarnessWrapper<'a> {
|
impl<'a, 'b> MigrationHarness<Pg> for MigrationHarnessWrapper<'a, 'b> {
|
||||||
fn run_migration(
|
fn run_migration(
|
||||||
&mut self,
|
&mut self,
|
||||||
migration: &dyn Migration<Pg>,
|
migration: &dyn Migration<Pg>,
|
||||||
) -> diesel::migration::Result<MigrationVersion<'static>> {
|
) -> diesel::migration::Result<MigrationVersion<'static>> {
|
||||||
|
let name = migration.name();
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
if self.options.enable_diff_check {
|
||||||
|
let before = diff_check::get_dump();
|
||||||
|
self.conn.run_migration(migration)?;
|
||||||
|
self.conn.revert_migration(migration)?;
|
||||||
|
diff_check::check_dump_diff(before, &format!("migrations/{name}/down.sql"));
|
||||||
|
}
|
||||||
|
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
|
|
||||||
let result = self.conn.run_migration(migration);
|
let result = self.conn.run_migration(migration);
|
||||||
|
|
||||||
let duration = start_time.elapsed().as_millis();
|
let duration = start_time.elapsed().as_millis();
|
||||||
let name = migration.name();
|
|
||||||
info!("{duration}ms run {name}");
|
info!("{duration}ms run {name}");
|
||||||
|
|
||||||
result
|
result
|
||||||
|
@ -71,6 +79,10 @@ impl<'a> MigrationHarness<Pg> for MigrationHarnessWrapper<'a> {
|
||||||
&mut self,
|
&mut self,
|
||||||
migration: &dyn Migration<Pg>,
|
migration: &dyn Migration<Pg>,
|
||||||
) -> diesel::migration::Result<MigrationVersion<'static>> {
|
) -> diesel::migration::Result<MigrationVersion<'static>> {
|
||||||
|
if self.options.enable_diff_check {
|
||||||
|
unimplemented!("diff check when reverting migrations");
|
||||||
|
}
|
||||||
|
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
|
|
||||||
let result = self.conn.revert_migration(migration);
|
let result = self.conn.revert_migration(migration);
|
||||||
|
@ -101,28 +113,15 @@ impl<'a, T: MigrationSource<Pg>> MigrationSource<Pg> for MigrationSourceRef<&'a
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct DiffChecker {
|
pub struct Options {
|
||||||
/// Maps a migration name to the schema that exists before the migration is applied
|
|
||||||
schema_before: BTreeMap<String, Schema>,
|
|
||||||
/// Stores strings
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct Schema {
|
|
||||||
indexes: BTreeMap<String, >
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct Options<'a> {
|
|
||||||
enable_forbid_diesel_cli_trigger: bool,
|
enable_forbid_diesel_cli_trigger: bool,
|
||||||
|
enable_diff_check: bool,
|
||||||
revert: bool,
|
revert: bool,
|
||||||
revert_amount: Option<u64>,
|
revert_amount: Option<u64>,
|
||||||
redo_after_revert: bool,
|
redo_after_revert: bool,
|
||||||
#[cfg(test)]
|
|
||||||
diff_checker: Option<&'a mut diff_checker::DiffChecker>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Options<'a> {
|
impl Options {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
fn enable_forbid_diesel_cli_trigger(mut self) -> Self {
|
fn enable_forbid_diesel_cli_trigger(mut self) -> Self {
|
||||||
self.enable_forbid_diesel_cli_trigger = true;
|
self.enable_forbid_diesel_cli_trigger = true;
|
||||||
|
@ -130,8 +129,8 @@ impl<'a> Options<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
fn diff_checker(mut self, diff_checker: &'a mut diff_checker::DiffChecker) -> Self {
|
fn enable_diff_check(mut self) -> Self {
|
||||||
self.diff_checker = Some(diff_checker);
|
self.enable_diff_check = true;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,11 +146,13 @@ impl<'a> Options<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO return struct with field `ran_replaceable_schema`
|
||||||
pub fn run(options: Options) -> LemmyResult<()> {
|
pub fn run(options: Options) -> LemmyResult<()> {
|
||||||
let db_url = SETTINGS.get_database_url();
|
let db_url = SETTINGS.get_database_url();
|
||||||
|
|
||||||
// Migrations don't support async connection, and this function doesn't need to be async
|
// Migrations don't support async connection, and this function doesn't need to be async
|
||||||
let mut conn = PgConnection::establish(db_url).with_context(|| "Error connecting to database")?;
|
let mut conn =
|
||||||
|
PgConnection::establish(&db_url).with_context(|| "Error connecting to database")?;
|
||||||
|
|
||||||
let new_sql = REPLACEABLE_SCHEMA.join("\n");
|
let new_sql = REPLACEABLE_SCHEMA.join("\n");
|
||||||
|
|
||||||
|
@ -180,24 +181,36 @@ pub fn run(options: Options) -> LemmyResult<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.transaction::<_, LemmyError, _>(|conn| {
|
// Disable the trigger that prevents the Diesel CLI from running migrations
|
||||||
let mut wrapper = MigrationHarnessWrapper { conn };
|
if !options.enable_forbid_diesel_cli_trigger {
|
||||||
|
conn.batch_execute("SET lemmy.enable_migrations TO 'on';")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Running without transaction allows pg_dump to see results of migrations
|
||||||
|
let run_in_transaction = !options.enable_diff_check;
|
||||||
|
|
||||||
|
let transaction = |conn: &mut PgConnection| -> LemmyResult<()> {
|
||||||
|
let mut wrapper = MigrationHarnessWrapper {
|
||||||
|
conn,
|
||||||
|
options: &options,
|
||||||
|
};
|
||||||
|
|
||||||
// * Prevent other lemmy_server processes from running this transaction simultaneously by repurposing
|
// * Prevent other lemmy_server processes from running this transaction simultaneously by repurposing
|
||||||
// the table created by `MigrationHarness::pending_migrations` as a lock target (this doesn't block
|
// the table created by `MigrationHarness::pending_migrations` as a lock target (this doesn't block
|
||||||
// normal use of the table)
|
// normal use of the table)
|
||||||
// * Drop `r` schema, so migrations don't need to be made to work both with and without things in
|
// * Drop `r` schema, so migrations don't need to be made to work both with and without things in
|
||||||
// it existing
|
// it existing
|
||||||
// * Disable the trigger that prevents the Diesel CLI from running migrations
|
|
||||||
info!("Waiting for lock...");
|
info!("Waiting for lock...");
|
||||||
|
|
||||||
let enable_migrations = if options.enable_forbid_diesel_cli_trigger {
|
let lock = if run_in_transaction {
|
||||||
""
|
"LOCK __diesel_schema_migrations IN SHARE UPDATE EXCLUSIVE MODE;"
|
||||||
} else {
|
} else {
|
||||||
"SET LOCAL lemmy.enable_migrations TO 'on';"
|
""
|
||||||
};
|
};
|
||||||
|
|
||||||
wrapper.conn.batch_execute(&format!("LOCK __diesel_schema_migrations IN SHARE UPDATE EXCLUSIVE MODE;DROP SCHEMA IF EXISTS r CASCADE;{enable_migrations}"))?;
|
wrapper
|
||||||
|
.conn
|
||||||
|
.batch_execute(&format!("{lock}DROP SCHEMA IF EXISTS r CASCADE;"))?;
|
||||||
|
|
||||||
info!("Running Database migrations (This may take a long time)...");
|
info!("Running Database migrations (This may take a long time)...");
|
||||||
|
|
||||||
|
@ -222,11 +235,28 @@ pub fn run(options: Options) -> LemmyResult<()> {
|
||||||
wrapper.run_pending_migrations(migration_source_ref)?;
|
wrapper.run_pending_migrations(migration_source_ref)?;
|
||||||
}
|
}
|
||||||
diesel::migration::Result::Ok(())
|
diesel::migration::Result::Ok(())
|
||||||
})().map_err(|e| anyhow!("Couldn't run DB Migrations: {e}"))?;
|
})()
|
||||||
|
.map_err(|e| anyhow!("Couldn't run DB Migrations: {e}"))?;
|
||||||
|
|
||||||
// Run replaceable_schema if newest migration was applied
|
// Run replaceable_schema if newest migration was applied
|
||||||
if !(options.revert && !options.redo_after_revert) {
|
if !(options.revert && !options.redo_after_revert) {
|
||||||
wrapper.conn
|
#[cfg(test)]
|
||||||
|
if options.enable_diff_check {
|
||||||
|
let before = diff_check::get_dump();
|
||||||
|
// todo move replaceable_schema dir path to let/const?
|
||||||
|
wrapper
|
||||||
|
.conn
|
||||||
|
.batch_execute(&new_sql)
|
||||||
|
.context("Couldn't run SQL files in crates/db_schema/replaceable_schema")?;
|
||||||
|
// todo move statement to const
|
||||||
|
wrapper
|
||||||
|
.conn
|
||||||
|
.batch_execute("DROP SCHEMA IF EXISTS r CASCADE;")?;
|
||||||
|
diff_check::check_dump_diff(before, "replaceable_schema");
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper
|
||||||
|
.conn
|
||||||
.batch_execute(&new_sql)
|
.batch_execute(&new_sql)
|
||||||
.context("Couldn't run SQL files in crates/db_schema/replaceable_schema")?;
|
.context("Couldn't run SQL files in crates/db_schema/replaceable_schema")?;
|
||||||
|
|
||||||
|
@ -238,7 +268,13 @@ pub fn run(options: Options) -> LemmyResult<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
};
|
||||||
|
|
||||||
|
if run_in_transaction {
|
||||||
|
conn.transaction(transaction)?;
|
||||||
|
} else {
|
||||||
|
transaction(&mut conn)?;
|
||||||
|
}
|
||||||
|
|
||||||
info!("Database migrations complete.");
|
info!("Database migrations complete.");
|
||||||
|
|
||||||
|
@ -256,24 +292,22 @@ mod tests {
|
||||||
fn test_schema_setup() -> LemmyResult<()> {
|
fn test_schema_setup() -> LemmyResult<()> {
|
||||||
let db_url = SETTINGS.get_database_url();
|
let db_url = SETTINGS.get_database_url();
|
||||||
let mut conn = PgConnection::establish(&db_url)?;
|
let mut conn = PgConnection::establish(&db_url)?;
|
||||||
let diff_checker = DiffChecker::default();
|
|
||||||
let options = || Options::default().diff_checker(&mut diff_checker);
|
|
||||||
|
|
||||||
// Start with consistent state by dropping everything
|
// Start with consistent state by dropping everything
|
||||||
conn.batch_execute("DROP OWNED BY CURRENT_USER;")?;
|
conn.batch_execute("DROP OWNED BY CURRENT_USER;")?;
|
||||||
|
|
||||||
// Run and revert all migrations, ensuring there's no mistakes in any down.sql file
|
// Check for mistakes in down.sql files
|
||||||
run(options())?;
|
run(Options::default().enable_diff_check())?;
|
||||||
run(options().revert(None))?;
|
|
||||||
|
|
||||||
// TODO also don't drop r, and maybe just directly call the migrationharness method here
|
// TODO also don't drop r, and maybe just directly call the migrationharness method here
|
||||||
|
run(Options::default().revert(None))?;
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
run(options().enable_forbid_diesel_cli_trigger()),
|
run(Options::default().enable_forbid_diesel_cli_trigger()),
|
||||||
Err(e) if e.to_string().contains("lemmy_server")
|
Err(e) if e.to_string().contains("lemmy_server")
|
||||||
));
|
));
|
||||||
|
|
||||||
// Previous run shouldn't stop this one from working
|
// Previous run shouldn't stop this one from working
|
||||||
run(options())?;
|
run(Options::default())?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
46
crates/db_schema/src/schema_setup/diff_check.rs
Normal file
46
crates/db_schema/src/schema_setup/diff_check.rs
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
use lemmy_utils::settings::SETTINGS;
|
||||||
|
use std::{
|
||||||
|
fmt::Write,
|
||||||
|
process::{Command, Stdio},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn get_dump() -> String {
|
||||||
|
let output = Command::new("pg_dump")
|
||||||
|
.args(["--schema-only"])
|
||||||
|
.env("DATABASE_URL", SETTINGS.get_database_url())
|
||||||
|
.stderr(Stdio::inherit())
|
||||||
|
.output()
|
||||||
|
.expect("failed to start pg_dump process");
|
||||||
|
|
||||||
|
// TODO: use exit_ok method when it's stable
|
||||||
|
assert!(output.status.success());
|
||||||
|
|
||||||
|
String::from_utf8(output.stdout).expect("pg_dump output is not valid UTF-8 text")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO add unit test for output
|
||||||
|
pub fn check_dump_diff(before: String, name: &str) {
|
||||||
|
let after = get_dump();
|
||||||
|
if after != before {
|
||||||
|
let mut output = format!("These changes need to be applied in {name}:");
|
||||||
|
let line_diffs = diff::lines(&after, &before);
|
||||||
|
for chunk in line_diffs.split(|line| matches!(line, diff::Result::Left("") | diff::Result::Right("") | diff::Result::Both("", _))) {
|
||||||
|
if chunk
|
||||||
|
.iter()
|
||||||
|
.all(|line| matches!(line, diff::Result::Both(_, _)))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
output.push_str("\n================");
|
||||||
|
for line in chunk {
|
||||||
|
match line {
|
||||||
|
diff::Result::Left(s) => write!(&mut output, "\n- {s}"),
|
||||||
|
diff::Result::Right(s) => write!(&mut output, "\n+ {s}"),
|
||||||
|
diff::Result::Both(s, _) => write!(&mut output, "\n {s}"),
|
||||||
|
}
|
||||||
|
.expect("failed to build string");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic!("{output}");
|
||||||
|
}
|
||||||
|
}
|
|
@ -116,6 +116,10 @@ FROM
|
||||||
WHERE
|
WHERE
|
||||||
c.id = ct.id;
|
c.id = ct.id;
|
||||||
|
|
||||||
|
-- Without this, `DROP EXTENSION` in down.sql throws an object dependency error if up.sql and down.sql
|
||||||
|
-- are run in the same database connection
|
||||||
|
DROP TABLE comment_temp;
|
||||||
|
|
||||||
-- Update the child counts
|
-- Update the child counts
|
||||||
UPDATE
|
UPDATE
|
||||||
comment_aggregates ca
|
comment_aggregates ca
|
||||||
|
@ -178,7 +182,7 @@ CREATE INDEX idx_path_gist ON comment USING gist (path);
|
||||||
|
|
||||||
-- Drop the parent_id column
|
-- Drop the parent_id column
|
||||||
ALTER TABLE comment
|
ALTER TABLE comment
|
||||||
DROP COLUMN parent_id CASCADE;
|
DROP COLUMN parent_id CASCADE,add column if not exists poop int;alter table post add column if not exists poop int;
|
||||||
|
|
||||||
ALTER TABLE comment ENABLE TRIGGER USER;
|
ALTER TABLE comment ENABLE TRIGGER USER;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue