You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

262 lines
6.3 KiB

use sqlx::FromRow;
use sqlx::PgPool;
use sqlx::types::chrono::{DateTime, Utc};
use chrono::serde::{ts_seconds, ts_seconds_option};
use serde::{Serialize, Deserialize};
pub trait TableName {
const TABLE_NAME: &'static str;
}
macro_rules! impl_table_name {
($ty:ident, $tname:expr) => {
impl TableName for $ty {
const TABLE_NAME: &'static str = $tname;
}
}
}
#[derive(FromRow, Serialize, Deserialize)]
pub struct League {
pub id: i32,
pub name: String,
#[serde(with = "ts_seconds")]
pub start_date: DateTime<Utc>,
#[serde(with = "ts_seconds_option")]
pub end_date: Option<DateTime<Utc>>,
}
#[derive(FromRow, Serialize, Deserialize)]
pub struct NewLeague {
pub name: String,
#[serde(with = "ts_seconds")]
pub start_date: DateTime<Utc>,
#[serde(with = "ts_seconds_option")]
pub end_date: Option<DateTime<Utc>>,
}
#[derive(FromRow, Serialize, Deserialize)]
pub struct Division {
pub id: i32,
pub name: String,
pub league: i32,
}
#[derive(FromRow, Serialize, Deserialize)]
pub struct NewDivision {
pub id: i32,
pub name: String,
pub league: i32,
}
#[derive(FromRow, Serialize, Deserialize)]
pub struct Team {
pub id: i32,
pub name: String,
pub division: i32,
}
#[derive(FromRow, Serialize, Deserialize)]
pub struct NewTeam {
pub name: String,
pub division: i32,
}
#[derive(FromRow, Serialize, Deserialize)]
pub struct Player {
pub id: i32,
pub name: String,
pub weight_kg: Option<i32>,
pub height_cm: Option<i32>,
}
#[derive(FromRow, Deserialize, Serialize)]
pub struct NewPlayer {
pub name: String,
pub weight_kg: Option<i32>,
pub height_cm: Option<i32>,
}
#[derive(FromRow, Deserialize, Serialize)]
pub struct Shot {
pub id: i32,
pub shooter_team: i32,
pub goalie: i32,
pub assistant: Option<i32>,
pub game: i32,
pub period: i32,
pub period_time: i32,
pub video_timestamp: Option<i32>,
}
#[derive(FromRow, Deserialize, Serialize)]
pub struct TeamPlayer {
pub id: i32,
pub team: i32,
pub player: i32,
pub position: i32,
}
#[derive(FromRow, Deserialize, Serialize, Debug)]
pub struct Notification {
pub scorer_name: String,
pub scorer_number: i32,
pub position: String,
pub scorer_team_name: String,
pub period_name: String,
pub period_time_left: i32,
}
#[derive(FromRow, Deserialize, Serialize, Debug)]
pub struct PlayerStats {
pub player_name: String,
pub goals: i64,
pub assists: i64,
pub points: i64,
}
async fn get_player_stats_overview(pool: PgPool) -> Result<Vec<PlayerStats>, sqlx::Error> {
let query = r#"
SELECT
(
SELECT COUNT(id)
FROM shots
WHERE shooter=players.id
AND goal=true
) AS goals,
(
SELECT COUNT(id)
FROM shots
WHERE assistant=players.id
AND goal=true
) AS assists,
(
SELECT COUNT(id)
FROM shots
WHERE assistant=players.id
OR shooter=players.id
) AS points,
players.name AS player_name
FROM players
ORDER BY
points DESC,
players.name;
"#;
let result = sqlx::query_as::<_, PlayerStats>(query)
.fetch_all(&pool)
.await;
result
}
impl_table_name!(TeamPlayer, "team_players");
impl_table_name!(Player, "players");
impl_table_name!(League, "leagues");
impl_table_name!(Division, "divisions");
impl_table_name!(Team, "teams");
impl_table_name!(Shot, "shots");
#[cfg(test)]
mod tests {
use std::env;
use crate::model::{
TeamPlayer,
Player,
League,
Division,
Team,
Shot,
TableName,
Notification,
get_player_stats_overview,
};
#[test]
fn check_player_overall_stats() {
tokio_test::block_on(async move {
let pool = db_connect().await;
let players_stats = get_player_stats_overview(pool).await.unwrap();
for player_stats in players_stats {
println!("{player_stats:?}");
}
})
}
#[test]
fn check_notification_query() {
tokio_test::block_on(async move {
let pool = db_connect().await;
let query = r#"
SELECT
teams.name AS scorer_team_name,
players.name AS scorer_name,
positions.name AS position,
team_players.player_number AS scorer_number,
shots.period_time AS period_time_left,
periods.name AS period_name
FROM
shots
JOIN teams ON teams.id=shots.shooter_team
JOIN players ON players.id=shots.shooter
JOIN team_players ON team_players.player=players.id AND team_players.team=teams.id
JOIN periods ON periods.id=shots.period
JOIN positions ON positions.id=team_players.position;
"#;
let result = sqlx::query_as::<_, Notification>(query)
.fetch_one(&pool)
.await
.unwrap();
let minutes = result.period_time_left / 60;
let seconds = result.period_time_left % 60;
println!("{0} {1} player #{3} {2} has scored! Time of the goal: {4}:{5} in the {6}",
result.scorer_team_name,
result.position,
result.scorer_name,
result.scorer_number,
minutes,
seconds,
result.period_name
);
});
}
/// A simple function to connect to the database.
async fn db_connect() -> sqlx::PgPool {
let db_url = env::var("DATABASE_URL").expect("DATABASE_URL environment variable must be set to run tests.");
sqlx::postgres::PgPoolOptions::new()
.max_connections(1)
.connect(&db_url)
.await
.expect("Active database connection must be made")
}
/// This macro generates a test that will `SELECT` all records for a table.
/// Then, it checks that
/// 1. The table rows gets deserialized correctly.
/// 2. There is at least one row.
macro_rules! generate_select_test {
($ret_type:ident, $func_name:ident) => {
#[test]
fn $func_name() {
tokio_test::block_on(async move {
let pool = db_connect().await;
let results = sqlx::query_as::<_, $ret_type>(
&format!(
"SELECT * FROM {};",
<$ret_type as TableName>::TABLE_NAME
)
)
.fetch_all(&pool)
.await
.unwrap();
// check that there is at least one result item
assert!(results.len() > 0, "There must be at least one result in the table.");
});
}
}
}
generate_select_test!(TeamPlayer, select_team_player);
generate_select_test!(Player, select_player);
generate_select_test!(League, select_league);
generate_select_test!(Division, select_division);
generate_select_test!(Team, select_team);
generate_select_test!(Shot, select_shot);
}