diff --git a/foods.db b/foods.db index c8e49e5..81ff588 100644 Binary files a/foods.db and b/foods.db differ diff --git a/src/get_foods.sql b/src/get_foods.sql index dbd3e94..69523a1 100644 --- a/src/get_foods.sql +++ b/src/get_foods.sql @@ -8,3 +8,5 @@ SELECT color FROM food +ORDER BY + sort_order, name; diff --git a/src/get_version.sql b/src/get_version.sql new file mode 100644 index 0000000..4edeca1 --- /dev/null +++ b/src/get_version.sql @@ -0,0 +1 @@ +PRAGMA user_version; diff --git a/src/main.rs b/src/main.rs index d1d1eb7..3ab24fc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,7 @@ use axum::{ routing::{get, post}, }; use parking_lot::Mutex; -use rusqlite::{CachedStatement, Connection, Row}; +use rusqlite::{CachedStatement, Connection, Row, Transaction}; use tower_http::trace::TraceLayer; use tower_request_id::{RequestId, RequestIdLayer}; use tracing::{debug, error, info, info_span}; @@ -123,6 +123,50 @@ impl<'conn> PreparedStatements { type ConnState = Arc>; +fn get_version(tx: &Transaction) -> rusqlite::Result { + tx.query_one(include_str!("get_version.sql"), (), |row| row.get(0)) +} + +fn get_migrations() -> [&'static str; 4] { + [ + include_str!("migrations/1.sql"), + include_str!("migrations/2.sql"), + include_str!("migrations/3.sql"), + include_str!("migrations/4.sql"), + ] +} + +fn do_migrations(conn: &mut Connection) -> rusqlite::Result<()> { + let migrations = get_migrations(); + let num_migrations = migrations.len(); + let tx = conn.transaction()?; + let version = get_version(&tx)?; + if version < migrations.len() { + info!( + migrations_to_apply = num_migrations - version, + "need to apply some migrations" + ); + let mut mig_number = version; + for migration in migrations.iter().skip(version) { + mig_number += 1; + info!(mig_number, "applying migration"); + debug!(migration = migration); + tx.execute_batch(migration)?; + if get_version(&tx)? != mig_number { + panic!( + "expected user_version to eq {} after applying migration {}. maybe a missing 'PRAGMA user_version =' ?", + mig_number, mig_number + ); + } + } + tx.commit()?; + info!("applied all migrations"); + } else { + info!("no migrations to apply"); + } + Ok(()) +} + #[tokio::main] async fn main() -> Result<(), std::io::Error> { tracing_subscriber::registry() @@ -142,13 +186,15 @@ async fn main() -> Result<(), std::io::Error> { let db_connecion_str = "./foods.db".to_string(); debug!(db_connecion_str, "opening database"); - let conn = Connection::open(db_connecion_str).expect("failed to open database"); + let mut conn = Connection::open(db_connecion_str).expect("failed to open database"); if let Err(e) = conn.execute(include_str!("create_tables.sql"), ()) { error!(?e, "failed to create tables"); panic!("failed to create tables: {:#?}", e); } + do_migrations(&mut conn).expect("failed to do database migrations"); + PreparedStatements::check(&conn).expect("failed to prepare sql statements"); let app = Router::new() diff --git a/src/migrations/1.sql b/src/migrations/1.sql new file mode 100644 index 0000000..b5d5cff --- /dev/null +++ b/src/migrations/1.sql @@ -0,0 +1,6 @@ +ALTER TABLE + food +ADD COLUMN + portion_weight INTEGER NOT NULL DEFAULT 100 CHECK (portion_weight > 0); + +PRAGMA user_version = 1; diff --git a/src/migrations/2.sql b/src/migrations/2.sql new file mode 100644 index 0000000..755c45c --- /dev/null +++ b/src/migrations/2.sql @@ -0,0 +1,23 @@ +ALTER TABLE + food +ADD COLUMN + -- per 100g + protein REAL NOT NULL DEFAULT 5.0 CHECK (protein > 0); + +ALTER TABLE + food +ADD COLUMN + -- per 100g + fiber REAL NOT NULL DEFAULT 5.0 CHECK (fiber > 0); + +ALTER TABLE + food +ADD COLUMN + protein_per_portion REAL NOT NULL GENERATED ALWAYS AS ((protein / 100) * portion_weight) VIRTUAL; + +ALTER TABLE + food +ADD COLUMN + fiber_per_portion REAL NOT NULL GENERATED ALWAYS AS ((fiber / 100) * portion_weight) VIRTUAL; + +PRAGMA user_version = 2; diff --git a/src/migrations/3.sql b/src/migrations/3.sql new file mode 100644 index 0000000..46c6d51 --- /dev/null +++ b/src/migrations/3.sql @@ -0,0 +1,6 @@ +ALTER TABLE + food +ADD COLUMN + sort_order INTEGER NOT NULL DEFAULT 0; + +PRAGMA user_version = 3; diff --git a/src/migrations/4.sql b/src/migrations/4.sql new file mode 100644 index 0000000..c6d9caa --- /dev/null +++ b/src/migrations/4.sql @@ -0,0 +1,23 @@ +CREATE TABLE "sqlb_temp_table_1" ( + "id" INTEGER, + "portion" TEXT NOT NULL, + "name" TEXT NOT NULL, + "kc_per_serving" INTEGER NOT NULL DEFAULT 0, + "target_servings" INTEGER NOT NULL DEFAULT 1, + "actual_servings" INTEGER NOT NULL DEFAULT 0, + "color" TEXT NOT NULL DEFAULT 'white', + "portion_weight" INTEGER NOT NULL DEFAULT 100 CHECK("portion_weight" > 0), + "protein" REAL NOT NULL DEFAULT 5.0 CHECK("protein" >= 0), + "fiber" REAL NOT NULL DEFAULT 5.0 CHECK("fiber" >= 0), + "protein_per_portion" REAL NOT NULL GENERATED ALWAYS AS (("protein" / 100) * "portion_weight") VIRTUAL, + "fiber_per_portion" REAL NOT NULL GENERATED ALWAYS AS (("fiber" / 100) * "portion_weight") VIRTUAL, + "sort_order" INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY("id") +) STRICT; +INSERT INTO "main"."sqlb_temp_table_1" ("actual_servings","color","fiber","id","kc_per_serving","name","portion","portion_weight","protein","sort_order","target_servings") SELECT "actual_servings","color","fiber","id","kc_per_serving","name","portion","portion_weight","protein","sort_order","target_servings" FROM "main"."food"; +PRAGMA defer_foreign_keys = '1'; +DROP TABLE "main"."food"; +ALTER TABLE "main"."sqlb_temp_table_1" RENAME TO "food"; +PRAGMA defer_foreign_keys = '0'; + +PRAGMA user_version = 4;