[Database] Add 'creation_datetime' to User + some little things
This commit is contained in:
parent
91ab379718
commit
7a09e2360e
14 changed files with 179 additions and 131 deletions
211
backend/src/data/db/mod.rs
Normal file
211
backend/src/data/db/mod.rs
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
use std::{
|
||||
fmt,
|
||||
fs::{self, File},
|
||||
io::Read,
|
||||
path::Path,
|
||||
str::FromStr,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use sqlx::{
|
||||
sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions, SqliteSynchronous},
|
||||
Pool, Sqlite, Transaction,
|
||||
};
|
||||
use thiserror::Error;
|
||||
use tracing::{event, Level};
|
||||
|
||||
use crate::consts;
|
||||
|
||||
pub mod recipe;
|
||||
pub mod user;
|
||||
|
||||
const CURRENT_DB_VERSION: u32 = 1;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum DBError {
|
||||
#[error("Sqlx error: {0}")]
|
||||
Sqlx(#[from] sqlx::Error),
|
||||
|
||||
#[error(
|
||||
"Unsupported database version: {0} (application version: {current})",
|
||||
current = CURRENT_DB_VERSION
|
||||
)]
|
||||
UnsupportedVersion(u32),
|
||||
|
||||
#[error("Unknown error: {0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl DBError {
|
||||
fn from_dyn_error(error: Box<dyn std::error::Error>) -> Self {
|
||||
DBError::Other(error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, DBError>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Connection {
|
||||
pool: Pool<Sqlite>,
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
pub async fn new() -> Result<Connection> {
|
||||
let path = Path::new(consts::DB_DIRECTORY).join(consts::DB_FILENAME);
|
||||
Self::new_from_file(path).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub async fn new_in_memory() -> Result<Connection> {
|
||||
Self::create_connection(SqlitePoolOptions::new().connect("sqlite::memory:").await?).await
|
||||
}
|
||||
|
||||
pub async fn new_from_file<P: AsRef<Path>>(file: P) -> Result<Connection> {
|
||||
if let Some(data_dir) = file.as_ref().parent() {
|
||||
if !data_dir.exists() {
|
||||
fs::DirBuilder::new().create(data_dir).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
let options = SqliteConnectOptions::from_str(&format!(
|
||||
"sqlite://{}",
|
||||
file.as_ref().to_str().unwrap()
|
||||
))?
|
||||
.journal_mode(SqliteJournalMode::Wal) // TODO: use 'Wal2' when available.
|
||||
.create_if_missing(true)
|
||||
.busy_timeout(Duration::from_secs(10))
|
||||
.foreign_keys(true)
|
||||
.synchronous(SqliteSynchronous::Normal);
|
||||
|
||||
Self::create_connection(
|
||||
SqlitePoolOptions::new()
|
||||
.max_connections(consts::MAX_DB_CONNECTION)
|
||||
.connect_with(options)
|
||||
.await?,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn create_connection(pool: Pool<Sqlite>) -> Result<Connection> {
|
||||
let connection = Connection { pool };
|
||||
connection.create_or_update_db().await?;
|
||||
Ok(connection)
|
||||
}
|
||||
|
||||
async fn tx(&self) -> Result<Transaction<Sqlite>> {
|
||||
self.pool.begin().await.map_err(DBError::from)
|
||||
}
|
||||
|
||||
/// Called after the connection has been established for creating or updating the database.
|
||||
/// The 'Version' table tracks the current state of the database.
|
||||
async fn create_or_update_db(&self) -> Result<()> {
|
||||
let mut tx = self.tx().await?; //con.transaction()?;
|
||||
|
||||
// Check current database version. (Version 0 corresponds to an empty database).
|
||||
let mut version = match sqlx::query(
|
||||
r#"
|
||||
SELECT [name] FROM [sqlite_master]
|
||||
WHERE [type] = 'table' AND [name] = 'Version'
|
||||
"#,
|
||||
)
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
{
|
||||
Ok(_) => sqlx::query_scalar("SELECT [version] FROM [Version] ORDER BY [id] DESC")
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?
|
||||
.unwrap_or(0),
|
||||
Err(_) => 0, // If the database doesn't exist.
|
||||
};
|
||||
|
||||
while Self::update_to_next_version(version, &mut tx).await? {
|
||||
version += 1;
|
||||
}
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_to_next_version(
|
||||
current_version: u32,
|
||||
tx: &mut Transaction<'_, Sqlite>,
|
||||
) -> Result<bool> {
|
||||
let next_version = current_version + 1;
|
||||
|
||||
if next_version <= CURRENT_DB_VERSION {
|
||||
event!(Level::INFO, "Update to version {}...", next_version);
|
||||
}
|
||||
|
||||
async fn update_version(to_version: u32, tx: &mut Transaction<'_, Sqlite>) -> Result<()> {
|
||||
sqlx::query(
|
||||
"INSERT INTO [Version] ([version], [datetime]) VALUES ($1, datetime('now'))",
|
||||
)
|
||||
.bind(to_version)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ok(updated: bool) -> Result<bool> {
|
||||
if updated {
|
||||
event!(Level::INFO, "Version updated");
|
||||
}
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
match next_version {
|
||||
1 => {
|
||||
let sql_file = consts::SQL_FILENAME.replace("{VERSION}", &next_version.to_string());
|
||||
sqlx::query(&load_sql_file(&sql_file)?)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
update_version(next_version, tx).await?;
|
||||
|
||||
ok(true)
|
||||
}
|
||||
|
||||
// Version 2 doesn't exist yet.
|
||||
2 => ok(false),
|
||||
|
||||
v => Err(DBError::UnsupportedVersion(v)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a given SQL file.
|
||||
pub async fn execute_file<P: AsRef<Path> + fmt::Display>(&self, file: P) -> Result<()> {
|
||||
let sql = load_sql_file(file)?;
|
||||
sqlx::query(&sql)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(DBError::from)
|
||||
}
|
||||
|
||||
pub async fn execute_sql<'a>(
|
||||
&self,
|
||||
query: sqlx::query::Query<'a, Sqlite, sqlx::sqlite::SqliteArguments<'a>>,
|
||||
) -> Result<u64> {
|
||||
query
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map(|db_result| db_result.rows_affected())
|
||||
.map_err(DBError::from)
|
||||
}
|
||||
|
||||
// pub async fn execute_sql_and_fetch_all<'a>(
|
||||
// &self,
|
||||
// query: sqlx::query::Query<'a, Sqlite, sqlx::sqlite::SqliteArguments<'a>>,
|
||||
// ) -> Result<Vec<SqliteRow>> {
|
||||
// query.fetch_all(&self.pool).await.map_err(DBError::from)
|
||||
// }
|
||||
}
|
||||
|
||||
fn load_sql_file<P: AsRef<Path> + fmt::Display>(sql_file: P) -> Result<String> {
|
||||
let mut file = File::open(&sql_file)
|
||||
.map_err(|err| DBError::Other(format!("Cannot open SQL file ({}): {}", &sql_file, err)))?;
|
||||
let mut sql = String::new();
|
||||
file.read_to_string(&mut sql)
|
||||
.map_err(|err| DBError::Other(format!("Cannot read SQL file ({}) : {}", &sql_file, err)))?;
|
||||
Ok(sql)
|
||||
}
|
||||
|
|
@ -628,14 +628,15 @@ mod tests {
|
|||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO [User]
|
||||
([id], [email], [name], [password], [validation_token_datetime], [validation_token])
|
||||
([id], [email], [name], [creation_datetime], [password], [validation_token_datetime], [validation_token])
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6)
|
||||
($1, $2, $3, $4, $5, $6, $7)
|
||||
"#
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind("paul@atreides.com")
|
||||
.bind("paul")
|
||||
.bind("")
|
||||
.bind("$argon2id$v=19$m=4096,t=3,p=1$G4fjepS05MkRbTqEImUdYg$GGziE8uVQe1L1oFHk37lBno10g4VISnVqynSkLCH3Lc")
|
||||
.bind("2022-11-29 22:05:04.121407300+00:00")
|
||||
.bind(None::<&str>) // 'null'.
|
||||
|
|
|
|||
|
|
@ -222,11 +222,12 @@ WHERE [id] = $1
|
|||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO [User]
|
||||
([email], [validation_token], [validation_token_datetime], [password])
|
||||
VALUES ($1, $2, $3, $4)
|
||||
([email], [creation_datetime], [validation_token], [validation_token_datetime], [password])
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
"#,
|
||||
)
|
||||
.bind(email)
|
||||
.bind(Utc::now())
|
||||
.bind(&token)
|
||||
.bind(datetime)
|
||||
.bind(hashed_password)
|
||||
|
|
@ -509,11 +510,12 @@ mod tests {
|
|||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO
|
||||
[User] ([id], [email], [name], [password], [validation_token_datetime], [validation_token])
|
||||
[User] ([id], [email], [name], [creation_datetime], [password], [validation_token_datetime], [validation_token])
|
||||
VALUES (
|
||||
1,
|
||||
'paul@atreides.com',
|
||||
'paul',
|
||||
'',
|
||||
'$argon2id$v=19$m=4096,t=3,p=1$1vtXcacYjUHZxMrN6b2Xng$wW8Z59MIoMcsIljnjHmxn3EBcc5ymEySZPUVXHlRxcY',
|
||||
0,
|
||||
NULL
|
||||
|
|
@ -557,10 +559,11 @@ INSERT INTO
|
|||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO [User]
|
||||
([id], [email], [name], [password], [validation_token_datetime], [validation_token])
|
||||
([id], [email], [creation_datetime], [name], [password], [validation_token_datetime], [validation_token])
|
||||
VALUES (
|
||||
1,
|
||||
'paul@atreides.com',
|
||||
'',
|
||||
'paul',
|
||||
'$argon2id$v=19$m=4096,t=3,p=1$1vtXcacYjUHZxMrN6b2Xng$wW8Z59MIoMcsIljnjHmxn3EBcc5ymEySZPUVXHlRxcY',
|
||||
0,
|
||||
|
|
@ -896,17 +899,11 @@ VALUES (
|
|||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO [User]
|
||||
([id], [email], [name], [password], [validation_token_datetime], [validation_token])
|
||||
([id], [email], [name], [creation_datetime], [password], [validation_token_datetime], [validation_token])
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6)
|
||||
(1, 'paul@atreides.com', 'paul', '', '$argon2id$v=19$m=4096,t=3,p=1$G4fjepS05MkRbTqEImUdYg$GGziE8uVQe1L1oFHk37lBno10g4VISnVqynSkLCH3Lc', '2022-11-29 22:05:04.121407300+00:00', NULL)
|
||||
"#
|
||||
)
|
||||
.bind(1)
|
||||
.bind("paul@atreides.com")
|
||||
.bind("paul")
|
||||
.bind("$argon2id$v=19$m=4096,t=3,p=1$G4fjepS05MkRbTqEImUdYg$GGziE8uVQe1L1oFHk37lBno10g4VISnVqynSkLCH3Lc")
|
||||
.bind("2022-11-29 22:05:04.121407300+00:00")
|
||||
.bind(None::<&str>) // 'null'.
|
||||
).await?;
|
||||
|
||||
let user = connection.load_user(1).await?.unwrap();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue