From 37294772415b4d56ab9cddf279e07cdbd41506e9 Mon Sep 17 00:00:00 2001 From: "Claude Sonnet 4.6" Date: Mon, 13 Apr 2026 07:59:07 +0000 Subject: [PATCH 1/3] feat/server: add automatic DB migrations via refinery - Add refinery dep with mysql_async feature - Add migrations/V1__init.sql with user, user_token and note tables - Run migrations at server startup before binding the listener - Change main() to return anyhow::Result<()> --- notto-server/Cargo.toml | 1 + notto-server/migrations/V1__init.sql | 39 ++++++++++++++++++++++++++++ notto-server/src/main.rs | 20 +++++++++++++- 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 notto-server/migrations/V1__init.sql diff --git a/notto-server/Cargo.toml b/notto-server/Cargo.toml index feee1ca..e88e716 100644 --- a/notto-server/Cargo.toml +++ b/notto-server/Cargo.toml @@ -14,3 +14,4 @@ serde = { version = "1", features = ["derive"] } tokio = { version="1.48", features = ["rt-multi-thread"]} hex = "0.4" anyhow = "1.0" +refinery = { version = "0.8", features = ["mysql_async"] } diff --git a/notto-server/migrations/V1__init.sql b/notto-server/migrations/V1__init.sql new file mode 100644 index 0000000..a2fc822 --- /dev/null +++ b/notto-server/migrations/V1__init.sql @@ -0,0 +1,39 @@ +CREATE TABLE IF NOT EXISTS `user` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `username` VARCHAR(255) NOT NULL, + `stored_password_hash` TEXT NOT NULL, + `stored_recovery_hash` TEXT NOT NULL, + `encrypted_mek_password` BLOB NOT NULL, + `mek_password_nonce` BLOB NOT NULL, + `encrypted_mek_recovery` BLOB NOT NULL, + `mek_recovery_nonce` BLOB NOT NULL, + `salt_auth` VARCHAR(255) NOT NULL, + `salt_data` VARCHAR(255) NOT NULL, + `salt_recovery_auth` VARCHAR(255) NOT NULL, + `salt_recovery_data` VARCHAR(255) NOT NULL, + `salt_server_auth` VARCHAR(255) NOT NULL, + `salt_server_recovery` VARCHAR(255) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uq_username` (`username`) +); + +CREATE TABLE IF NOT EXISTS `user_token` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `id_user` INT UNSIGNED NOT NULL, + `token` BLOB NOT NULL, + PRIMARY KEY (`id`), + FOREIGN KEY (`id_user`) REFERENCES `user` (`id`) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS `note` ( + `uuid` VARCHAR(36) NOT NULL, + `id_user` INT UNSIGNED NOT NULL, + `content` MEDIUMBLOB NOT NULL, + `nonce` BLOB NOT NULL, + `metadata` BLOB NOT NULL, + `metadata_nonce` BLOB NOT NULL, + `updated_at` BIGINT NOT NULL, + `deleted` TINYINT(1) NOT NULL DEFAULT 0, + PRIMARY KEY (`uuid`, `id_user`), + FOREIGN KEY (`id_user`) REFERENCES `user` (`id`) ON DELETE CASCADE +); diff --git a/notto-server/src/main.rs b/notto-server/src/main.rs index 062c391..b5a4401 100644 --- a/notto-server/src/main.rs +++ b/notto-server/src/main.rs @@ -17,6 +17,10 @@ use crate::schema::User; mod schema; +mod embedded { + refinery::embed_migrations!("migrations"); +} + /// Application error returned by all handlers. /// Internal errors are logged server-side and return a generic 500 to the client. pub struct AppError { @@ -82,7 +86,7 @@ impl From for AppError { } #[tokio::main] -async fn main() { +async fn main() -> anyhow::Result<()> { dotenv().ok(); //Env var should be like mysql://user:pass%20word@localhost/database_name let pool = Pool::new( @@ -91,6 +95,18 @@ async fn main() { .as_str(), ); + let mut conn = pool + .get_conn() + .await + .context("Failed to get DB connection for migrations")?; + + embedded::migrations::runner() + .run_async(&mut conn) + .await + .context("Failed to run database migrations")?; + + drop(conn); + let app = Router::new() .route("/notes", post(send_notes)) .route("/notes", get(select_notes)) @@ -112,6 +128,8 @@ async fn main() { axum::serve(listener, app) .await .expect("Server error"); + + Ok(()) } /// Verifies that `token` matches one of the stored tokens for `username`. From fdc6ea4a451de97474154655f7a88189e96b7045 Mon Sep 17 00:00:00 2001 From: "Claude Sonnet 4.6" Date: Mon, 13 Apr 2026 12:16:26 +0000 Subject: [PATCH 2/3] fix/server: replace refinery with custom migration runner refinery-core 0.8 only supports mysql_async <= 0.35 while the project uses 0.36, causing an AsyncMigrate trait bound conflict. Replace with a minimal migration runner in migrations.rs that: - Creates schema_migrations table for version tracking - Runs each unapplied SQL file in order using the existing Conn - Records applied versions with timestamps - Embeds SQL files at compile time via include_str! --- notto-server/Cargo.toml | 1 - notto-server/src/main.rs | 7 ++--- notto-server/src/migrations.rs | 53 ++++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 6 deletions(-) create mode 100644 notto-server/src/migrations.rs diff --git a/notto-server/Cargo.toml b/notto-server/Cargo.toml index e88e716..feee1ca 100644 --- a/notto-server/Cargo.toml +++ b/notto-server/Cargo.toml @@ -14,4 +14,3 @@ serde = { version = "1", features = ["derive"] } tokio = { version="1.48", features = ["rt-multi-thread"]} hex = "0.4" anyhow = "1.0" -refinery = { version = "0.8", features = ["mysql_async"] } diff --git a/notto-server/src/main.rs b/notto-server/src/main.rs index b5a4401..01d6b34 100644 --- a/notto-server/src/main.rs +++ b/notto-server/src/main.rs @@ -17,9 +17,7 @@ use crate::schema::User; mod schema; -mod embedded { - refinery::embed_migrations!("migrations"); -} +mod migrations; /// Application error returned by all handlers. /// Internal errors are logged server-side and return a generic 500 to the client. @@ -100,8 +98,7 @@ async fn main() -> anyhow::Result<()> { .await .context("Failed to get DB connection for migrations")?; - embedded::migrations::runner() - .run_async(&mut conn) + migrations::run(&mut conn) .await .context("Failed to run database migrations")?; diff --git a/notto-server/src/migrations.rs b/notto-server/src/migrations.rs new file mode 100644 index 0000000..816ab60 --- /dev/null +++ b/notto-server/src/migrations.rs @@ -0,0 +1,53 @@ +use anyhow::{Context, Result}; +use mysql_async::{Conn, prelude::Queryable}; + +/// Each migration is a (version, sql) pair. Version must be monotonically increasing. +/// Append new entries here to add future migrations; never edit existing ones. +static MIGRATIONS: &[(u32, &str)] = &[ + (1, include_str!("../migrations/V1__init.sql")), +]; + +/// Creates the tracking table if absent, then runs every migration whose version +/// is not yet recorded, in order. +pub async fn run(conn: &mut Conn) -> Result<()> { + conn.query_drop( + "CREATE TABLE IF NOT EXISTS `schema_migrations` ( + `version` INT UNSIGNED NOT NULL, + `applied_at` BIGINT NOT NULL, + PRIMARY KEY (`version`) + )", + ) + .await + .context("Failed to create schema_migrations table")?; + + let applied: Vec = conn + .query("SELECT version FROM schema_migrations ORDER BY version") + .await + .context("Failed to query applied migrations")?; + + for (version, sql) in MIGRATIONS { + if applied.contains(version) { + continue; + } + + for statement in sql.split(';').map(str::trim).filter(|s| !s.is_empty()) { + conn.query_drop(statement) + .await + .with_context(|| format!("Migration V{version} failed on statement: {statement}"))?; + } + + conn.exec_drop( + "INSERT INTO schema_migrations (version, applied_at) VALUES (:version, :applied_at)", + mysql_async::params! { + "version" => version, + "applied_at" => chrono::Local::now().to_utc().timestamp(), + }, + ) + .await + .with_context(|| format!("Failed to record migration V{version}"))?; + + println!("Applied migration V{version}"); + } + + Ok(()) +} From e33ba6d2331c3f82ffe256bc3c95f7f2b7917a68 Mon Sep 17 00:00:00 2001 From: "Claude Sonnet 4.6" Date: Mon, 13 Apr 2026 12:36:04 +0000 Subject: [PATCH 3/3] fix/server: import params macro in migrations.rs --- notto-server/src/migrations.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/notto-server/src/migrations.rs b/notto-server/src/migrations.rs index 816ab60..a8eff5a 100644 --- a/notto-server/src/migrations.rs +++ b/notto-server/src/migrations.rs @@ -1,5 +1,5 @@ use anyhow::{Context, Result}; -use mysql_async::{Conn, prelude::Queryable}; +use mysql_async::{Conn, params, prelude::Queryable}; /// Each migration is a (version, sql) pair. Version must be monotonically increasing. /// Append new entries here to add future migrations; never edit existing ones. @@ -38,7 +38,7 @@ pub async fn run(conn: &mut Conn) -> Result<()> { conn.exec_drop( "INSERT INTO schema_migrations (version, applied_at) VALUES (:version, :applied_at)", - mysql_async::params! { + params! { "version" => version, "applied_at" => chrono::Local::now().to_utc().timestamp(), },