use std::ops::DerefMut;
use log::info;
use sqlx::{Executor, SqliteConnection};
use crate::utils::apierror::ApiError;
use crate::utils::db::{AppTransaction, Migration, MigrationContent, MigrationError, VersionNumber, SCHEMA_METADATA_VERSION};
const MIGRATIONS: &[Migration<'static>] = &[
Migration {
target: "1.1.0",
content: MigrationContent::Sql(include_bytes!("v1.1.0.sql")),
},
Migration {
target: "1.2.0",
content: MigrationContent::Sql(include_bytes!("v1.2.0.sql")),
},
Migration {
target: "1.3.0",
content: MigrationContent::Sql(include_bytes!("v1.3.0.sql")),
},
Migration {
target: "1.4.0",
content: MigrationContent::Sql(include_bytes!("v1.4.0.sql")),
},
Migration {
target: "1.5.0",
content: MigrationContent::Sql(include_bytes!("v1.5.0.sql")),
},
Migration {
target: "1.6.0",
content: MigrationContent::Sql(include_bytes!("v1.6.0.sql")),
},
Migration {
target: "1.7.0",
content: MigrationContent::Sql(include_bytes!("v1.7.0.sql")),
},
Migration {
target: "1.7.1",
content: MigrationContent::Sql(include_bytes!("v1.7.1.sql")),
},
Migration {
target: "1.8.0",
content: MigrationContent::Sql(include_bytes!("v1.8.0.sql")),
},
Migration {
target: "1.9.0",
content: MigrationContent::Sql(include_bytes!("v1.9.0.sql")),
},
Migration {
target: "1.10.0",
content: MigrationContent::Sql(include_bytes!("v1.10.0.sql")),
},
];
async fn get_schema_metadata(connection: &mut SqliteConnection, name_input: &str) -> Result<Option<String>, sqlx::Error> {
let row = sqlx::query!("SELECT value FROM SchemaMetadata WHERE name = $1 LIMIT 1", name_input)
.fetch_optional(connection)
.await?;
Ok(row.map(|row| row.value))
}
#[allow(clippy::explicit_deref_methods)]
async fn set_schema_metadata(mut connection: &mut SqliteConnection, n: &str, v: &str) -> Result<(), sqlx::Error> {
let row = sqlx::query!("SELECT value FROM SchemaMetadata WHERE name = $1 LIMIT 1", n)
.fetch_optional(connection.deref_mut())
.await?;
if row.is_none() {
sqlx::query!("INSERT INTO SchemaMetadata (name, value) VALUES ($1, $2)", n, v)
.execute(connection)
.await?;
} else {
sqlx::query!("UPDATE SchemaMetadata SET value = $2 WHERE name = $1", n, v)
.execute(connection)
.await?;
}
Ok(())
}
const CREATE_METADATA_TABLE_SQL: &str = "CREATE TABLE IF NOT EXISTS SchemaMetadata (
name TEXT NOT NULL PRIMARY KEY,
value TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS SchemaMetadataIndex ON SchemaMetadata(name);";
async fn migrate_db(transaction: AppTransaction, migrations: &[Migration<'_>]) -> Result<(), MigrationError> {
let current_version = match get_schema_metadata(&mut *transaction.borrow().await, SCHEMA_METADATA_VERSION).await {
Ok(Some(version)) => Some(version),
Ok(None) => None,
_ => {
transaction.borrow().await.execute(CREATE_METADATA_TABLE_SQL).await?;
None
}
};
let start_from = match current_version {
Some(version) => {
info!("Database schema version = {}", version);
let version: VersionNumber = version.as_str().try_into()?;
let mut result = 0;
for (index, migration) in migrations.iter().enumerate().rev() {
let target: VersionNumber = migration.target.try_into()?;
if version >= target {
result = index + 1;
break;
}
}
result
}
None => 0,
};
if start_from >= migrations.len() {
return Ok(());
}
for migration in &migrations[start_from..] {
info!("Database migrating to {} ...", migration.target);
match &migration.content {
MigrationContent::Sql(script) => {
let script = String::from_utf8_lossy(script);
transaction.borrow().await.execute(script.as_ref()).await?;
}
}
set_schema_metadata(&mut *transaction.borrow().await, SCHEMA_METADATA_VERSION, migration.target).await?;
}
info!("Database successfully migrated.");
Ok(())
}
pub async fn migrate_to_last(transaction: AppTransaction) -> Result<i32, ApiError> {
migrate_db(transaction, MIGRATIONS).await?;
Ok(0)
}