migrator/migrations/
m20250516154702_automerge_storage.rs1use 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 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 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}