1
Fork 0

Implement migrations, and switch from sqlx to rusqlite

This commit is contained in:
prescientmoon 2024-08-22 22:11:21 +02:00
parent 7cdc3a2755
commit fee7fe77f8
Signed by: prescientmoon
SSH key fingerprint: SHA256:UUF9JT2s8Xfyv76b8ZuVL7XrmimH4o49p4b+iexbVH4
17 changed files with 629 additions and 1044 deletions

View file

@ -1,10 +1,9 @@
use poise::serenity_prelude::{CreateAttachment, CreateEmbed, CreateMessage};
use sqlx::query;
use crate::{
arcaea::chart::Side,
arcaea::{chart::Side, play::Play},
context::{Context, Error},
get_user, play_from_db_record,
get_user,
recognition::fuzzy_song_name::guess_song_and_chart,
};
use std::io::Cursor;
@ -23,7 +22,7 @@ use poise::CreateReply;
use crate::{
arcaea::score::{Score, ScoringSystem},
user::discord_it_to_discord_user,
user::discord_id_to_discord_user,
};
// {{{ Top command
@ -55,16 +54,18 @@ async fn info(
None => None,
};
let play_count = query!(
"
let play_count: usize = ctx
.data()
.db
.get()?
.prepare_cached(
"
SELECT COUNT(*) as count
FROM plays
WHERE chart_id=?
",
chart.id
)
.fetch_one(&ctx.data().db)
.await?;
",
)?
.query_row([chart.id], |row| row.get(0))?;
let mut embed = CreateEmbed::default()
.title(format!(
@ -77,7 +78,7 @@ async fn info(
format!("{:.1}", chart.chart_constant as f32 / 100.0),
true,
)
.field("Total plays", format!("{}", play_count.count), true)
.field("Total plays", format!("{play_count}"), true)
.field("BPM", &song.bpm, true)
.field("Side", Side::SIDE_STRINGS[song.side.to_index()], true)
.field("Artist", &song.title, true);
@ -117,42 +118,40 @@ async fn best(
let user = get_user!(&ctx);
let (song, chart) = guess_song_and_chart(&ctx.data(), &name)?;
let play = query!(
"
SELECT
let play = ctx
.data()
.db
.get()?
.prepare_cached(
"
SELECT
p.id, p.chart_id, p.user_id, p.created_at,
p.max_recall, p.far_notes, s.score
FROM plays p
JOIN scores s ON s.play_id = p.id
WHERE s.scoring_system='standard'
AND p.user_id=?
AND p.chart_id=?
ORDER BY s.score DESC
LIMIT 1
",
user.id,
chart.id
)
.fetch_one(&ctx.data().db)
.await
.map_err(|_| {
format!(
"Could not find any scores for {} [{:?}]",
song.title, chart.difficulty
)
})?;
let play = play_from_db_record!(chart, play);
FROM plays p
JOIN scores s ON s.play_id = p.id
WHERE s.scoring_system='standard'
AND p.user_id=?
AND p.chart_id=?
ORDER BY s.score DESC
LIMIT 1
",
)?
.query_row((user.id, chart.id), |row| Play::from_sql(chart, row))
.map_err(|_| {
format!(
"Could not find any scores for {} [{:?}]",
song.title, chart.difficulty
)
})?;
let (embed, attachment) = play
.to_embed(
&ctx.data().db,
&user,
&song,
&chart,
0,
Some(&discord_it_to_discord_user(&ctx, &user.discord_id).await?),
)
.await?;
let (embed, attachment) = play.to_embed(
ctx.data(),
&user,
song,
chart,
0,
Some(&discord_id_to_discord_user(&ctx, &user.discord_id).await?),
)?;
ctx.channel_id()
.send_files(ctx.http(), attachment, CreateMessage::new().embed(embed))
@ -177,8 +176,12 @@ async fn plot(
let (song, chart) = guess_song_and_chart(&ctx.data(), &name)?;
// SAFETY: we limit the amount of plotted plays to 1000.
let plays = query!(
"
let plays = ctx
.data()
.db
.get()?
.prepare_cached(
"
SELECT
p.id, p.chart_id, p.user_id, p.created_at,
p.max_recall, p.far_notes, s.score
@ -190,11 +193,9 @@ async fn plot(
ORDER BY s.score DESC
LIMIT 1000
",
user.id,
chart.id
)
.fetch_all(&ctx.data().db)
.await?;
)?
.query_map((user.id, chart.id), |row| Play::from_sql(chart, row))?
.collect::<Result<Vec<_>, _>>()?;
if plays.len() == 0 {
ctx.reply(format!(
@ -209,7 +210,7 @@ async fn plot(
let max_time = plays.iter().map(|p| p.created_at).max().unwrap();
let mut min_score = plays
.iter()
.map(|p| play_from_db_record!(chart, p).score(scoring_system))
.map(|p| p.score(scoring_system))
.min()
.unwrap()
.0 as i64;
@ -266,7 +267,7 @@ async fn plot(
.map(|play| {
(
play.created_at.and_utc().timestamp_millis(),
play_from_db_record!(chart, play).score(scoring_system),
play.score(scoring_system),
)
})
.collect();

View file

@ -1,16 +1,15 @@
use std::time::Instant;
use crate::arcaea::play::CreatePlay;
use crate::arcaea::play::{CreatePlay, Play};
use crate::arcaea::score::Score;
use crate::context::{Context, Error};
use crate::recognition::recognize::{ImageAnalyzer, ScoreKind};
use crate::user::{discord_it_to_discord_user, User};
use crate::{edit_reply, get_user, play_from_db_record, timed};
use crate::user::{discord_id_to_discord_user, User};
use crate::{edit_reply, get_user, timed};
use image::DynamicImage;
use poise::serenity_prelude::futures::future::join_all;
use poise::serenity_prelude::CreateMessage;
use poise::{serenity_prelude as serenity, CreateReply};
use sqlx::query;
// {{{ Score
/// Score management
@ -121,15 +120,13 @@ pub async fn magic(
.with_attachment(file)
.with_fars(maybe_fars)
.with_max_recall(max_recall)
.save(&ctx.data(), &user, &chart)
.await?;
.save(&ctx.data(), &user, &chart)?;
// }}}
// }}}
// {{{ Deliver embed
let (embed, attachment) = timed!("to embed", {
play.to_embed(&ctx.data().db, &user, &song, &chart, i, None)
.await?
play.to_embed(ctx.data(), &user, &song, &chart, i, None)?
});
embeds.push(embed);
@ -183,11 +180,14 @@ pub async fn delete(
let mut count = 0;
for id in ids {
let res = query!("DELETE FROM plays WHERE id=? AND user_id=?", id, user.id)
.execute(&ctx.data().db)
.await?;
let res = ctx
.data()
.db
.get()?
.prepare_cached("DELETE FROM plays WHERE id=? AND user_id=?")?
.execute((id, user.id))?;
if res.rows_affected() == 0 {
if res == 0 {
ctx.reply(format!("No play with id {} found", id)).await?;
} else {
count += 1;
@ -216,36 +216,38 @@ pub async fn show(
let mut embeds = Vec::with_capacity(ids.len());
let mut attachments = Vec::with_capacity(ids.len());
let conn = ctx.data().db.get()?;
for (i, id) in ids.iter().enumerate() {
let res = query!(
"
SELECT
p.id, p.chart_id, p.user_id, p.created_at,
p.max_recall, p.far_notes, s.score,
u.discord_id
FROM plays p
JOIN scores s ON s.play_id = p.id
JOIN users u ON p.user_id = u.id
WHERE s.scoring_system='standard'
AND p.id=?
ORDER BY s.score DESC
LIMIT 1
",
id
)
.fetch_one(&ctx.data().db)
.await
.map_err(|_| format!("Could not find play with id {}", id))?;
let (song, chart, play, discord_id) = conn
.prepare_cached(
"
SELECT
p.id, p.chart_id, p.user_id, p.created_at,
p.max_recall, p.far_notes, s.score,
u.discord_id
FROM plays p
JOIN scores s ON s.play_id = p.id
JOIN users u ON p.user_id = u.id
WHERE s.scoring_system='standard'
AND p.id=?
ORDER BY s.score DESC
LIMIT 1
",
)?
.query_and_then([id], |row| -> Result<_, Error> {
let (song, chart) = ctx.data().song_cache.lookup_chart(row.get("chart_id")?)?;
let play = Play::from_sql(chart, row)?;
let discord_id = row.get::<_, String>("discord_id")?;
Ok((song, chart, play, discord_id))
})?
.next()
.ok_or_else(|| format!("Could not find play with id {}", id))??;
let (song, chart) = ctx.data().song_cache.lookup_chart(res.chart_id as u32)?;
let play = play_from_db_record!(chart, res);
let author = discord_id_to_discord_user(&ctx, &discord_id).await?;
let user = User::by_id(ctx.data(), play.user_id)?;
let author = discord_it_to_discord_user(&ctx, &res.discord_id).await?;
let user = User::by_id(&ctx.data().db, play.user_id).await?;
let (embed, attachment) = play
.to_embed(&ctx.data().db, &user, song, chart, i, Some(&author))
.await?;
let (embed, attachment) =
play.to_embed(ctx.data(), &user, song, chart, i, Some(&author))?;
embeds.push(embed);
attachments.extend(attachment);

View file

@ -5,7 +5,6 @@ use poise::{
serenity_prelude::{CreateAttachment, CreateEmbed},
CreateReply,
};
use sqlx::query;
use crate::{
arcaea::{
@ -26,7 +25,7 @@ use crate::{
context::{Context, Error},
get_user,
logs::debug_image_log,
reply_errors,
reply_errors, timed,
user::User,
};
@ -53,20 +52,20 @@ async fn best_plays(
let user_ctx = ctx.data();
let plays = reply_errors!(
ctx,
get_best_plays(
&user_ctx.db,
&user_ctx.song_cache,
user.id,
scoring_system,
if require_full {
grid_size.0 * grid_size.1
} else {
grid_size.0 * (grid_size.1.max(1) - 1) + 1
} as usize,
(grid_size.0 * grid_size.1) as usize,
None
)
.await?
timed!("get_best_plays", {
get_best_plays(
user_ctx,
user.id,
scoring_system,
if require_full {
grid_size.0 * grid_size.1
} else {
grid_size.0 * (grid_size.1.max(1) - 1) + 1
} as usize,
(grid_size.0 * grid_size.1) as usize,
None,
)?
})
);
// {{{ Layout
@ -463,48 +462,42 @@ pub async fn bany(
#[poise::command(prefix_command, slash_command, user_cooldown = 1)]
async fn meta(ctx: Context<'_>) -> Result<(), Error> {
let user = get_user!(&ctx);
let song_count = query!("SELECT count() as count FROM songs")
.fetch_one(&ctx.data().db)
.await?
.count;
let conn = ctx.data().db.get()?;
let song_count: usize = conn
.prepare_cached("SELECT count() as count FROM songs")?
.query_row((), |row| row.get(0))?;
let chart_count = query!("SELECT count() as count FROM charts")
.fetch_one(&ctx.data().db)
.await?
.count;
let chart_count: usize = conn
.prepare_cached("SELECT count() as count FROM charts")?
.query_row((), |row| row.get(0))?;
let users_count = query!("SELECT count() as count FROM users")
.fetch_one(&ctx.data().db)
.await?
.count;
let users_count: usize = conn
.prepare_cached("SELECT count() as count FROM users")?
.query_row((), |row| row.get(0))?;
let pookie_count = query!(
"
SELECT count() as count
FROM users
WHERE is_pookie=1
"
)
.fetch_one(&ctx.data().db)
.await?
.count;
let pookie_count: usize = conn
.prepare_cached(
"
SELECT count() as count
FROM users
WHERE is_pookie=1
",
)?
.query_row((), |row| row.get(0))?;
let play_count = query!("SELECT count() as count FROM plays")
.fetch_one(&ctx.data().db)
.await?
.count;
let play_count: usize = conn
.prepare_cached("SELECT count() as count FROM plays")?
.query_row((), |row| row.get(0))?;
let your_play_count = query!(
"
let your_play_count: usize = conn
.prepare_cached(
"
SELECT count() as count
FROM plays
WHERE user_id=?
",
user.id
)
.fetch_one(&ctx.data().db)
.await?
.count;
",
)?
.query_row([user.id], |row| row.get(0))?;
let embed = CreateEmbed::default()
.title("Bot statistics")

View file

@ -14,7 +14,7 @@ macro_rules! edit_reply {
#[macro_export]
macro_rules! get_user {
($ctx:expr) => {{
crate::reply_errors!($ctx, crate::user::User::from_context($ctx).await)
crate::reply_errors!($ctx, crate::user::User::from_context($ctx))
}};
}