migrator/migrations/
m20250516154702_automerge_storage.rs

1use sqlx::{PgConnection, Postgres};
2use sqlx_migrator::Migration;
3use sqlx_migrator::Operation;
4use sqlx_migrator::error::Error;
5use sqlx_migrator::vec_box;
6use std::env;
7use std::path::{Path, PathBuf};
8use std::process::Command;
9use std::process::ExitStatus;
10
11pub(crate) struct AutomergeStorage;
12#[async_trait::async_trait]
13impl Migration<Postgres> for AutomergeStorage {
14    fn app(&self) -> &str {
15        "backend"
16    }
17    fn name(&self) -> &str {
18        "20250516154702_automerge_storage"
19    }
20    fn parents(&self) -> Vec<Box<dyn Migration<Postgres>>> {
21        vec![]
22    }
23    fn operations(&self) -> Vec<Box<dyn Operation<Postgres>>> {
24        vec_box![MigrationOperation]
25    }
26
27    fn is_atomic(&self) -> bool {
28        false
29    }
30}
31
32struct MigrationOperation;
33#[async_trait::async_trait]
34impl Operation<Postgres> for MigrationOperation {
35    async fn up(&self, conn: &mut PgConnection) -> Result<(), Error> {
36        // This up migration is not able to be run inside a transaction because it's calling an external
37        // script, so we have to handle rollback on failure ourselves. Fortunately the use of IF EXISTS
38        // in the down migration means that it will handle the cleanup regardless of where the up
39        // migratoin fails
40        let inner: Result<(), Error> = async {
41            sqlx::query(
42                "
43                CREATE TABLE storage (
44                    key text[] PRIMARY KEY,
45                    data bytea NOT NULL
46                );
47                ",
48            )
49            .execute(&mut *conn)
50            .await?;
51
52            sqlx::query(
53                "
54                ALTER TABLE snapshots ADD COLUMN doc_id TEXT;
55                ",
56            )
57            .execute(&mut *conn)
58            .await?;
59
60            // INVOCATION_ID will be set when the program is running from inside a systemd container
61            if env::var_os("INVOCATION_ID").is_some() {
62                run_automerge_migration_during_deployment()?;
63            } else {
64                run_automerge_migration_during_development()?;
65            }
66
67            sqlx::query(
68                "
69                ALTER TABLE snapshots ALTER COLUMN doc_id SET NOT NULL;
70                ",
71            )
72            .execute(&mut *conn)
73            .await?;
74
75            Ok(())
76        }
77        .await;
78
79        match inner {
80            Ok(()) => Ok(()),
81            Err(e) => {
82                self.down(conn).await?;
83                Err(e)
84            }
85        }
86    }
87
88    async fn down(&self, conn: &mut PgConnection) -> Result<(), Error> {
89        sqlx::query(
90            "
91            DROP TABLE IF EXISTS storage;
92            ",
93        )
94        .execute(&mut *conn)
95        .await?;
96
97        sqlx::query(
98            "
99            ALTER TABLE snapshots DROP COLUMN IF EXISTS doc_id;
100            ",
101        )
102        .execute(&mut *conn)
103        .await?;
104
105        Ok(())
106    }
107}
108
109fn run_automerge_migration_during_development()
110-> Result<(), Box<dyn std::error::Error + Send + Sync>> {
111    let cwd = env::current_dir().map_err(|e| format!("Failed to get current directory: {e}"))?;
112
113    let git_root = find_git_root(&cwd).ok_or("No .git root found")?;
114
115    let automerge_server_dir = git_root.join("packages").join("automerge-doc-server");
116
117    let status = Command::new("npm")
118        .args(["run", "main", "--", "--migrate", "automerge_storage"])
119        .current_dir(&automerge_server_dir)
120        .status()
121        .map_err(|e| format!("Failed to run `npm run migrate-storage`: {e}"))?;
122
123    check_status(status, "`npm run migrate-storage`")?;
124
125    Ok(())
126}
127
128fn run_automerge_migration_during_deployment()
129-> Result<(), Box<dyn std::error::Error + Send + Sync>> {
130    let status = Command::new("automerge-doc-server")
131        .args(["--migrate", "automerge_storage"])
132        .status()
133        .map_err(|e| format!("Failed to run `automerge-doc-server`: {e}"))?;
134
135    check_status(status, "`automerge-doc-server --migrate automerge_storage`")?;
136
137    Ok(())
138}
139
140fn check_status(
141    status: ExitStatus,
142    command: &str,
143) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
144    if status.success() {
145        println!("{command} succeeded");
146        Ok(())
147    } else {
148        Err(format!("{command} failed with exit code {:?}", status.code()).into())
149    }
150}
151
152fn find_git_root(start: impl AsRef<Path>) -> Option<PathBuf> {
153    let mut dir = start.as_ref().canonicalize().ok()?;
154
155    loop {
156        if dir.join(".git").is_dir() {
157            return Some(dir);
158        }
159
160        if !dir.pop() {
161            break;
162        }
163    }
164
165    None
166}