Move to SQLite (#13)

This commit is contained in:
Natsu Kagami 2021-06-19 22:36:17 +09:00 committed by GitHub
parent 750ddb7762
commit 1799b70bc1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 2122 additions and 394 deletions

View file

@ -0,0 +1,14 @@
[package]
name = "youmubot-db-sql"
version = "0.1.0"
authors = ["Natsu Kagami <nki@nkagami.me>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "sqlite", "chrono", "offline"] }
thiserror = "1"
chrono = "0.4"
futures-util = "0.3"
either = "1"

View file

@ -0,0 +1,12 @@
-- Add migration script here
CREATE TABLE osu_users (
user_id BIGINT NOT NULL PRIMARY KEY,
id BIGINT NOT NULL UNIQUE,
last_update DATETIME NOT NULL,
pp_std REAL NULL,
pp_taiko REAL NULL,
pp_mania REAL NULL,
pp_catch REAL NULL,
failures INT NOT NULL DEFAULT 0
);

View file

@ -0,0 +1,45 @@
-- Add migration script here
CREATE TABLE osu_last_beatmaps (
channel_id BIGINT NOT NULL PRIMARY KEY,
beatmap BLOB NOT NULL,
mode INT NOT NULL
);
CREATE TABLE osu_user_best_scores (
beatmap_id BIGINT NOT NULL,
mode INT NOT NULL,
user_id INT NOT NULL REFERENCES osu_users(user_id) ON DELETE CASCADE,
mods BIGINT NOT NULL,
cached_at DATETIME NOT NULL,
score BLOB NOT NULL,
PRIMARY KEY (beatmap_id, mode, user_id, mods)
);
CREATE TABLE osu_cached_beatmaps (
beatmap_id BIGINT NOT NULL,
mode INT NOT NULL,
cached_at DATETIME NOT NULL,
beatmap BLOB NOT NULL,
PRIMARY KEY (beatmap_id, mode)
);
CREATE TABLE osu_cached_beatmapsets (
beatmapset_id BIGINT NOT NULL,
beatmap_id BIGINT NOT NULL,
mode INT NOT NULL,
PRIMARY KEY (beatmapset_id, beatmap_id, mode),
FOREIGN KEY (beatmap_id, mode) REFERENCES osu_cached_beatmaps (beatmap_id, mode)
);
CREATE TABLE osu_cached_beatmap_contents (
beatmap_id BIGINT NOT NULL PRIMARY KEY,
cached_at DATETIME NOT NULL,
content BLOB NOT NULL
);

View file

@ -0,0 +1,491 @@
{
"db": "SQLite",
"1bf34dddbe994d6124c9382c75e70e1347329e945de2eefad4bfcab5f81b73ce": {
"query": "SELECT\n channel_id as \"channel_id: i64\",\n beatmap,\n mode as \"mode: u8\"\n FROM osu_last_beatmaps\n WHERE channel_id = ?",
"describe": {
"columns": [
{
"name": "channel_id: i64",
"ordinal": 0,
"type_info": "Int64"
},
{
"name": "beatmap",
"ordinal": 1,
"type_info": "Blob"
},
{
"name": "mode: u8",
"ordinal": 2,
"type_info": "Int64"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false
]
}
},
"235312a1aad1a58c2f7f2d817945bbac57c38ad2c51c1924683d13d045f21ad9": {
"query": "SELECT\n beatmap_id as \"beatmap_id: i64\",\n mode as \"mode: u8\",\n user_id as \"user_id: i64\",\n mods as \"mods: i64\",\n cached_at as \"cached_at: DateTime\",\n score as \"score: Vec<u8>\"\n FROM osu_user_best_scores\n WHERE\n beatmap_id = ?\n AND mode = ?\n AND user_id = ?",
"describe": {
"columns": [
{
"name": "beatmap_id: i64",
"ordinal": 0,
"type_info": "Int64"
},
{
"name": "mode: u8",
"ordinal": 1,
"type_info": "Int64"
},
{
"name": "user_id: i64",
"ordinal": 2,
"type_info": "Int64"
},
{
"name": "mods: i64",
"ordinal": 3,
"type_info": "Int64"
},
{
"name": "cached_at: DateTime",
"ordinal": 4,
"type_info": "Datetime"
},
{
"name": "score: Vec<u8>",
"ordinal": 5,
"type_info": "Blob"
}
],
"parameters": {
"Right": 3
},
"nullable": [
false,
false,
false,
false,
false,
false
]
}
},
"25077e7b2657eb918fa49acc16ceba14a004ed503c174073a1db184d902ee393": {
"query": "\n INSERT INTO\n osu_user_best_scores (beatmap_id, mode, user_id, mods, cached_at, score)\n VALUES\n (?, ?, ?, ?, ?, ?)\n ON CONFLICT (beatmap_id, mode, user_id, mods)\n DO UPDATE\n SET\n cached_at = excluded.cached_at,\n score = excluded.score\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 6
},
"nullable": []
}
},
"27edec2f76b1bc48e87b66e6d27e6784e0b0c17dec013feb05c4b7291b8b4a5f": {
"query": "SELECT\n user_id as \"user_id: i64\",\n id as \"id: i64\",\n last_update as \"last_update: DateTime\",\n pp_std, pp_taiko, pp_mania, pp_catch,\n failures as \"failures: u8\"\n FROM osu_users WHERE id = ?",
"describe": {
"columns": [
{
"name": "user_id: i64",
"ordinal": 0,
"type_info": "Int64"
},
{
"name": "id: i64",
"ordinal": 1,
"type_info": "Int64"
},
{
"name": "last_update: DateTime",
"ordinal": 2,
"type_info": "Datetime"
},
{
"name": "pp_std",
"ordinal": 3,
"type_info": "Float"
},
{
"name": "pp_taiko",
"ordinal": 4,
"type_info": "Float"
},
{
"name": "pp_mania",
"ordinal": 5,
"type_info": "Float"
},
{
"name": "pp_catch",
"ordinal": 6,
"type_info": "Float"
},
{
"name": "failures: u8",
"ordinal": 7,
"type_info": "Int64"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
true,
true,
true,
true,
false
]
}
},
"296c95c7ead4d747a4da007b4b6e28d3c6c1c4bb654c82cc40bf61390c3dad4b": {
"query": "SELECT\n beatmap_id as \"beatmap_id: i64\",\n cached_at as \"cached_at: DateTime\",\n content as \"content: Vec<u8>\"\n FROM osu_cached_beatmap_contents\n WHERE\n beatmap_id = ? ",
"describe": {
"columns": [
{
"name": "beatmap_id: i64",
"ordinal": 0,
"type_info": "Int64"
},
{
"name": "cached_at: DateTime",
"ordinal": 1,
"type_info": "Datetime"
},
{
"name": "content: Vec<u8>",
"ordinal": 2,
"type_info": "Blob"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false
]
}
},
"3c9d163aa2b752afc74e7b0909a9c1995cd019d9798a992eddc2e778f36f2d4c": {
"query": "SELECT\n user_id as \"user_id: i64\",\n id as \"id: i64\",\n last_update as \"last_update: DateTime\",\n pp_std, pp_taiko, pp_mania, pp_catch,\n failures as \"failures: u8\"\n FROM osu_users",
"describe": {
"columns": [
{
"name": "user_id: i64",
"ordinal": 0,
"type_info": "Int64"
},
{
"name": "id: i64",
"ordinal": 1,
"type_info": "Int64"
},
{
"name": "last_update: DateTime",
"ordinal": 2,
"type_info": "Datetime"
},
{
"name": "pp_std",
"ordinal": 3,
"type_info": "Float"
},
{
"name": "pp_taiko",
"ordinal": 4,
"type_info": "Float"
},
{
"name": "pp_mania",
"ordinal": 5,
"type_info": "Float"
},
{
"name": "pp_catch",
"ordinal": 6,
"type_info": "Float"
},
{
"name": "failures: u8",
"ordinal": 7,
"type_info": "Int64"
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
false,
false,
true,
true,
true,
true,
false
]
}
},
"4b033607229deba540f80e469753e2125b6d8134346144f462325dc025221044": {
"query": "SELECT\n beatmap.beatmap_id as \"beatmap_id: i64\",\n beatmap.mode as \"mode: u8\",\n beatmap.cached_at as \"cached_at: DateTime\",\n beatmap.beatmap as \"beatmap: Vec<u8>\"\n FROM osu_cached_beatmapsets\n INNER JOIN osu_cached_beatmaps AS beatmap\n ON osu_cached_beatmapsets.beatmap_id = beatmap.beatmap_id\n AND osu_cached_beatmapsets.mode = beatmap.mode\n WHERE\n beatmapset_id = ?\n ",
"describe": {
"columns": [
{
"name": "beatmap_id: i64",
"ordinal": 0,
"type_info": "Int64"
},
{
"name": "mode: u8",
"ordinal": 1,
"type_info": "Int64"
},
{
"name": "cached_at: DateTime",
"ordinal": 2,
"type_info": "Datetime"
},
{
"name": "beatmap: Vec<u8>",
"ordinal": 3,
"type_info": "Blob"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false
]
}
},
"5210e3e5610bb968b0b11411b99956d1bf358f2c1e35c4eb5730388ce0c2fe09": {
"query": "INSERT INTO\n osu_last_beatmaps (channel_id, beatmap, mode)\n VALUES\n (?, ?, ?)\n ON CONFLICT (channel_id) DO UPDATE\n SET\n beatmap = excluded.beatmap,\n mode = excluded.mode",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
}
},
"55fc5c2846680c32a2c9646e45cc578cff25cde57cca19f5fb53dceacc154954": {
"query": "SELECT\n user_id as \"user_id: i64\",\n id as \"id: i64\",\n last_update as \"last_update: DateTime\",\n pp_std, pp_taiko, pp_mania, pp_catch,\n failures as \"failures: u8\"\n FROM osu_users WHERE user_id = ?",
"describe": {
"columns": [
{
"name": "user_id: i64",
"ordinal": 0,
"type_info": "Int64"
},
{
"name": "id: i64",
"ordinal": 1,
"type_info": "Int64"
},
{
"name": "last_update: DateTime",
"ordinal": 2,
"type_info": "Datetime"
},
{
"name": "pp_std",
"ordinal": 3,
"type_info": "Float"
},
{
"name": "pp_taiko",
"ordinal": 4,
"type_info": "Float"
},
{
"name": "pp_mania",
"ordinal": 5,
"type_info": "Float"
},
{
"name": "pp_catch",
"ordinal": 6,
"type_info": "Float"
},
{
"name": "failures: u8",
"ordinal": 7,
"type_info": "Int64"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
true,
true,
true,
true,
false
]
}
},
"6125c1c187029c7ac6e1e9519445e49942ddf6068a16f000dd0750ab8a9d52c2": {
"query": "\n INSERT INTO\n osu_cached_beatmaps (beatmap_id, mode, cached_at, beatmap)\n VALUES\n (?, ?, ?, ?)\n ON CONFLICT (beatmap_id, mode)\n DO UPDATE\n SET\n cached_at = excluded.cached_at,\n beatmap = excluded.beatmap\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
}
},
"6bfd02cb36c9b74ed4c69eb694e936ba2ee8d3864e2a43b43db78afc32a47384": {
"query": "\n INSERT INTO\n osu_cached_beatmap_contents (beatmap_id, cached_at, content)\n VALUES\n (?, ?, ?)\n ON CONFLICT (beatmap_id)\n DO UPDATE\n SET\n cached_at = excluded.cached_at,\n content = excluded.content\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
}
},
"6c95dc522e1e8490358ce4c5fea08fe50300ab4092b33eef44aba85f4a43c818": {
"query": "INSERT\n INTO osu_users(user_id, id, last_update, pp_std, pp_taiko, pp_mania, pp_catch, failures)\n VALUES(?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (user_id) WHERE id = ? DO UPDATE\n SET\n last_update = excluded.last_update,\n pp_std = excluded.pp_std,\n pp_taiko = excluded.pp_taiko,\n pp_mania = excluded.pp_mania,\n pp_catch = excluded.pp_catch,\n failures = excluded.failures\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 9
},
"nullable": []
}
},
"8b9ad43442b7fa520f2eae498d2ee08264810e49c28bd8ddffaa9f444cada1b5": {
"query": "INSERT INTO osu_cached_beatmapsets(beatmapset_id, beatmap_id, mode)\n VALUES (?, ?, ?)\n ON CONFLICT DO NOTHING",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
}
},
"95541f737a8dfc7f440840617bed87ebde6dabdd70e2ba7b110ebec91e7feda7": {
"query": "SELECT\n beatmap_id as \"beatmap_id: i64\",\n mode as \"mode: u8\",\n user_id as \"user_id: i64\",\n mods as \"mods: i64\",\n cached_at as \"cached_at: DateTime\",\n score as \"score: Vec<u8>\"\n FROM osu_user_best_scores\n WHERE\n beatmap_id = ?\n AND mode = ?",
"describe": {
"columns": [
{
"name": "beatmap_id: i64",
"ordinal": 0,
"type_info": "Int64"
},
{
"name": "mode: u8",
"ordinal": 1,
"type_info": "Int64"
},
{
"name": "user_id: i64",
"ordinal": 2,
"type_info": "Int64"
},
{
"name": "mods: i64",
"ordinal": 3,
"type_info": "Int64"
},
{
"name": "cached_at: DateTime",
"ordinal": 4,
"type_info": "Datetime"
},
{
"name": "score: Vec<u8>",
"ordinal": 5,
"type_info": "Blob"
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false,
false,
false,
false,
false
]
}
},
"c83421661626cffd81d5590035ae5283a5b0e8a03696ae479b3d275b81b8af83": {
"query": "DELETE FROM osu_user_best_scores WHERE user_id = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
}
},
"d428568e88b653317cbe2c5336e6cdee0862df09faaa6c1fa09869d79438e427": {
"query": "DELETE FROM osu_users WHERE user_id = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
}
},
"d7c91077f904543740a12185fac7756346aa50a63b911414ee8f7a4a0d6dd1cc": {
"query": "SELECT\n beatmap_id as \"beatmap_id: i64\",\n mode as \"mode: u8\",\n cached_at as \"cached_at: DateTime\",\n beatmap as \"beatmap: Vec<u8>\"\n FROM osu_cached_beatmaps\n WHERE\n beatmap_id = ?\n AND mode = ?\n ",
"describe": {
"columns": [
{
"name": "beatmap_id: i64",
"ordinal": 0,
"type_info": "Int64"
},
{
"name": "mode: u8",
"ordinal": 1,
"type_info": "Int64"
},
{
"name": "cached_at: DateTime",
"ordinal": 2,
"type_info": "Datetime"
},
{
"name": "beatmap: Vec<u8>",
"ordinal": 3,
"type_info": "Blob"
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false,
false,
false
]
}
}
}

View file

@ -0,0 +1,48 @@
use sqlx::sqlite;
use std::path::Path;
pub use errors::*;
/// The DB constructs that will be used in the package.
pub use sqlite::{SqliteConnection as Connection, SqliteError, SqlitePool as Pool};
pub use sqlx::Sqlite as Database;
/// Models defined in the database.
pub mod models;
/// Create a new pool of sqlite connections to the given database path,
/// run migrations on it and return the result.
pub async fn connect(path: impl AsRef<Path>) -> Result<Pool> {
let pool = Pool::connect_with(
sqlite::SqliteConnectOptions::new()
.filename(path)
.foreign_keys(true)
.create_if_missing(true)
.journal_mode(sqlite::SqliteJournalMode::Wal),
)
.await?;
// Run migration before we return.
migration::MIGRATOR.run(&pool).await?;
Ok(pool)
}
pub mod errors {
/// Default `Result` type used in this package.
pub type Result<T, E = Error> = std::result::Result<T, E>;
/// Possible errors in the package.
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("sqlx error: {:?}", .0)]
SQLx(#[from] sqlx::Error),
#[error("sqlx migration error: {:?}", .0)]
Migration(#[from] sqlx::migrate::MigrateError),
}
}
mod migration {
use sqlx::migrate::Migrator;
pub(crate) static MIGRATOR: Migrator = sqlx::migrate!("./migrations");
}

View file

@ -0,0 +1,22 @@
use crate::*;
use futures_util::stream::{Stream, StreamExt};
use sqlx::{query, query_as, Executor};
/// The DateTime used in the package.
pub type DateTime = chrono::DateTime<chrono::Utc>;
pub mod osu;
pub mod osu_user;
/// Map a `fetch_many` result to a normal result.
pub(crate) async fn map_many_result<T, E, W>(
item: Result<either::Either<W, T>, E>,
) -> Option<Result<T>>
where
E: Into<Error>,
{
match item {
Ok(v) => v.right().map(Ok),
Err(e) => Some(Err(e.into())),
}
}

View file

@ -0,0 +1,319 @@
use crate::models::*;
pub struct LastBeatmap {
pub channel_id: i64,
pub beatmap: Vec<u8>,
pub mode: u8,
}
impl LastBeatmap {
/// Get a [`LastBeatmap`] by the channel id.
pub async fn by_channel_id(
id: i64,
conn: impl Executor<'_, Database = Database>,
) -> Result<Option<LastBeatmap>> {
let m = query_as!(
LastBeatmap,
r#"SELECT
channel_id as "channel_id: i64",
beatmap,
mode as "mode: u8"
FROM osu_last_beatmaps
WHERE channel_id = ?"#,
id
)
.fetch_optional(conn)
.await?;
Ok(m)
}
}
impl LastBeatmap {
/// Store the value.
pub async fn store(&self, conn: impl Executor<'_, Database = Database>) -> Result<()> {
query!(
r#"INSERT INTO
osu_last_beatmaps (channel_id, beatmap, mode)
VALUES
(?, ?, ?)
ON CONFLICT (channel_id) DO UPDATE
SET
beatmap = excluded.beatmap,
mode = excluded.mode"#,
self.channel_id,
self.beatmap,
self.mode,
)
.execute(conn)
.await?;
Ok(())
}
}
pub struct UserBestScore {
pub beatmap_id: i64,
pub mode: u8,
pub user_id: i64,
pub mods: i64,
pub cached_at: DateTime,
/// To be deserialized by `bincode`
pub score: Vec<u8>,
}
impl UserBestScore {
/// Get a list of scores by the given map and user.
pub async fn by_map_and_user(
beatmap: i64,
mode: u8,
user: i64,
conn: impl Executor<'_, Database = Database>,
) -> Result<Vec<Self>> {
query_as!(
UserBestScore,
r#"SELECT
beatmap_id as "beatmap_id: i64",
mode as "mode: u8",
user_id as "user_id: i64",
mods as "mods: i64",
cached_at as "cached_at: DateTime",
score as "score: Vec<u8>"
FROM osu_user_best_scores
WHERE
beatmap_id = ?
AND mode = ?
AND user_id = ?"#,
beatmap,
mode,
user
)
.fetch_all(conn)
.await
.map_err(Error::from)
}
/// Get a list of scores by the given map.
pub async fn by_map(
beatmap: i64,
mode: u8,
conn: impl Executor<'_, Database = Database>,
) -> Result<Vec<Self>> {
query_as!(
UserBestScore,
r#"SELECT
beatmap_id as "beatmap_id: i64",
mode as "mode: u8",
user_id as "user_id: i64",
mods as "mods: i64",
cached_at as "cached_at: DateTime",
score as "score: Vec<u8>"
FROM osu_user_best_scores
WHERE
beatmap_id = ?
AND mode = ?"#,
beatmap,
mode
)
.fetch_all(conn)
.await
.map_err(Error::from)
}
}
impl UserBestScore {
pub async fn store(&mut self, conn: impl Executor<'_, Database = Database>) -> Result<()> {
self.cached_at = chrono::Utc::now();
query!(
r#"
INSERT INTO
osu_user_best_scores (beatmap_id, mode, user_id, mods, cached_at, score)
VALUES
(?, ?, ?, ?, ?, ?)
ON CONFLICT (beatmap_id, mode, user_id, mods)
DO UPDATE
SET
cached_at = excluded.cached_at,
score = excluded.score
"#,
self.beatmap_id,
self.mode,
self.user_id,
self.mods,
self.cached_at,
self.score
)
.execute(conn)
.await?;
Ok(())
}
pub async fn clear_user(
user_id: i64,
conn: impl Executor<'_, Database = Database>,
) -> Result<()> {
query!(
"DELETE FROM osu_user_best_scores WHERE user_id = ?",
user_id
)
.execute(conn)
.await?;
Ok(())
}
}
pub struct CachedBeatmap {
pub beatmap_id: i64,
pub mode: u8,
pub cached_at: DateTime,
pub beatmap: Vec<u8>,
}
impl CachedBeatmap {
/// Get a cached beatmap by its id.
pub async fn by_id(
id: i64,
mode: u8,
conn: impl Executor<'_, Database = Database>,
) -> Result<Option<Self>> {
query_as!(
Self,
r#"SELECT
beatmap_id as "beatmap_id: i64",
mode as "mode: u8",
cached_at as "cached_at: DateTime",
beatmap as "beatmap: Vec<u8>"
FROM osu_cached_beatmaps
WHERE
beatmap_id = ?
AND mode = ?
"#,
id,
mode
)
.fetch_optional(conn)
.await
.map_err(Error::from)
}
pub async fn by_beatmapset(
beatmapset: i64,
conn: impl Executor<'_, Database = Database>,
) -> Result<Vec<Self>> {
query_as!(
Self,
r#"SELECT
beatmap.beatmap_id as "beatmap_id: i64",
beatmap.mode as "mode: u8",
beatmap.cached_at as "cached_at: DateTime",
beatmap.beatmap as "beatmap: Vec<u8>"
FROM osu_cached_beatmapsets
INNER JOIN osu_cached_beatmaps AS beatmap
ON osu_cached_beatmapsets.beatmap_id = beatmap.beatmap_id
AND osu_cached_beatmapsets.mode = beatmap.mode
WHERE
beatmapset_id = ?
"#,
beatmapset
)
.fetch_all(conn)
.await
.map_err(Error::from)
}
}
impl CachedBeatmap {
pub async fn store(&mut self, conn: impl Executor<'_, Database = Database>) -> Result<()> {
self.cached_at = chrono::Utc::now();
query!(
r#"
INSERT INTO
osu_cached_beatmaps (beatmap_id, mode, cached_at, beatmap)
VALUES
(?, ?, ?, ?)
ON CONFLICT (beatmap_id, mode)
DO UPDATE
SET
cached_at = excluded.cached_at,
beatmap = excluded.beatmap
"#,
self.beatmap_id,
self.mode,
self.cached_at,
self.beatmap
)
.execute(conn)
.await?;
Ok(())
}
pub async fn link_beatmapset(
&self,
beatmapset_id: i64,
conn: impl Executor<'_, Database = Database>,
) -> Result<()> {
query!(
r#"INSERT INTO osu_cached_beatmapsets(beatmapset_id, beatmap_id, mode)
VALUES (?, ?, ?)
ON CONFLICT DO NOTHING"#,
beatmapset_id,
self.beatmap_id,
self.mode,
)
.execute(conn)
.await?;
Ok(())
}
}
pub struct CachedBeatmapContent {
pub beatmap_id: i64,
pub cached_at: DateTime,
pub content: Vec<u8>,
}
impl CachedBeatmapContent {
/// Get a cached beatmap by its id.
pub async fn by_id(
id: i64,
conn: impl Executor<'_, Database = Database>,
) -> Result<Option<Self>> {
query_as!(
Self,
r#"SELECT
beatmap_id as "beatmap_id: i64",
cached_at as "cached_at: DateTime",
content as "content: Vec<u8>"
FROM osu_cached_beatmap_contents
WHERE
beatmap_id = ? "#,
id,
)
.fetch_optional(conn)
.await
.map_err(Error::from)
}
}
impl CachedBeatmapContent {
pub async fn store(&mut self, conn: impl Executor<'_, Database = Database>) -> Result<()> {
self.cached_at = chrono::Utc::now();
query!(
r#"
INSERT INTO
osu_cached_beatmap_contents (beatmap_id, cached_at, content)
VALUES
(?, ?, ?)
ON CONFLICT (beatmap_id)
DO UPDATE
SET
cached_at = excluded.cached_at,
content = excluded.content
"#,
self.beatmap_id,
self.cached_at,
self.content
)
.execute(conn)
.await?;
Ok(())
}
}

View file

@ -0,0 +1,118 @@
use super::*;
use sqlx::{query, query_as, Executor};
/// An osu user, as represented in the SQL.
#[derive(Debug, Clone)]
pub struct OsuUser {
pub user_id: i64,
pub id: i64,
pub last_update: DateTime,
pub pp_std: Option<f32>,
pub pp_taiko: Option<f32>,
pub pp_mania: Option<f32>,
pub pp_catch: Option<f32>,
/// Number of consecutive update failures
pub failures: u8,
}
impl OsuUser {
/// Query an user by their user id.
pub async fn by_user_id<'a, E>(user_id: i64, conn: &'a mut E) -> Result<Option<Self>>
where
&'a mut E: Executor<'a, Database = Database>,
{
let u = query_as!(
Self,
r#"SELECT
user_id as "user_id: i64",
id as "id: i64",
last_update as "last_update: DateTime",
pp_std, pp_taiko, pp_mania, pp_catch,
failures as "failures: u8"
FROM osu_users WHERE user_id = ?"#,
user_id
)
.fetch_optional(conn)
.await?;
Ok(u)
}
/// Query an user by their osu id.
pub async fn by_osu_id<'a, E>(osu_id: i64, conn: &'a mut E) -> Result<Option<Self>>
where
&'a mut E: Executor<'a, Database = Database>,
{
let u = query_as!(
Self,
r#"SELECT
user_id as "user_id: i64",
id as "id: i64",
last_update as "last_update: DateTime",
pp_std, pp_taiko, pp_mania, pp_catch,
failures as "failures: u8"
FROM osu_users WHERE id = ?"#,
osu_id
)
.fetch_optional(conn)
.await?;
Ok(u)
}
/// Query all users.
pub fn all<'a, E>(conn: &'a mut E) -> impl Stream<Item = Result<Self>> + 'a
where
&'a mut E: Executor<'a, Database = Database>,
{
query_as!(
Self,
r#"SELECT
user_id as "user_id: i64",
id as "id: i64",
last_update as "last_update: DateTime",
pp_std, pp_taiko, pp_mania, pp_catch,
failures as "failures: u8"
FROM osu_users"#,
)
.fetch_many(conn)
.filter_map(map_many_result)
}
}
impl OsuUser {
/// Stores the user.
pub async fn store<'a, E>(&self, conn: &'a mut E) -> Result<()>
where
&'a mut E: Executor<'a, Database = Database>,
{
query!(
r#"INSERT
INTO osu_users(user_id, id, last_update, pp_std, pp_taiko, pp_mania, pp_catch, failures)
VALUES(?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (user_id) WHERE id = ? DO UPDATE
SET
last_update = excluded.last_update,
pp_std = excluded.pp_std,
pp_taiko = excluded.pp_taiko,
pp_mania = excluded.pp_mania,
pp_catch = excluded.pp_catch,
failures = excluded.failures
"#,
self.user_id,
self.id,
self.last_update,
self.pp_std,
self.pp_taiko,
self.pp_mania,
self.pp_catch,
self.failures,
self.user_id).execute(conn).await?;
Ok(())
}
pub async fn delete(user_id: i64, conn: impl Executor<'_, Database = Database>) -> Result<()> {
query!("DELETE FROM osu_users WHERE user_id = ?", user_id)
.execute(conn)
.await?;
Ok(())
}
}