1
Fork 0

Figure out database shenanigans

This commit is contained in:
Steffo 2024-11-28 12:24:15 +01:00
parent 8823120942
commit 5ca754e972
Signed by: steffo
GPG key ID: 5ADA3868646C3FC0
14 changed files with 310 additions and 39 deletions

View file

@ -0,0 +1,20 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Backend" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="buildProfileId" value="dev" />
<option name="command" value="run" />
<option name="workingDirectory" value="file://$PROJECT_DIR$/holycow_backend" />
<envs />
<option name="emulateTerminal" value="true" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>

View file

@ -8,7 +8,7 @@
<node-interpreter value="project" /> <node-interpreter value="project" />
<envs /> <envs />
<EXTENSION ID="com.intellij.lang.javascript.buildTools.npm.rc.StartBrowserRunConfigurationExtension"> <EXTENSION ID="com.intellij.lang.javascript.buildTools.npm.rc.StartBrowserRunConfigurationExtension">
<browser with-js-debugger="true" url="http://localhost:3000" /> <browser url="http://localhost:3000" />
</EXTENSION> </EXTENSION>
<method v="2" /> <method v="2" />
</configuration> </configuration>

View file

@ -26,6 +26,21 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.93" version = "1.0.93"
@ -202,7 +217,12 @@ version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [ dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits", "num-traits",
"wasm-bindgen",
"windows-targets 0.52.6",
] ]
[[package]] [[package]]
@ -318,6 +338,7 @@ checksum = "cbf9649c05e0a9dbd6d0b0b8301db5182b972d0fd02f0a7c6736cf632d7c0fd5"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"byteorder", "byteorder",
"chrono",
"diesel_derives", "diesel_derives",
"itoa", "itoa",
"pq-sys", "pq-sys",
@ -630,11 +651,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
[[package]] [[package]]
name = "holycow_api" name = "holycow_backend"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum", "axum",
"chrono",
"diesel", "diesel",
"diesel_migrations", "diesel_migrations",
"log", "log",
@ -792,6 +814,29 @@ dependencies = [
"tower-service", "tower-service",
] ]
[[package]]
name = "iana-time-zone"
version = "0.1.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "icu_collections" name = "icu_collections"
version = "1.5.0" version = "1.5.0"
@ -2211,6 +2256,15 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.48.0" version = "0.48.0"

View file

@ -1,12 +1,12 @@
[package] [package]
name = "holycow_api" name = "holycow_backend"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
anyhow = "1.0.93" anyhow = "1.0.93"
axum = { version = "0.7.9", features = ["macros"] } axum = { version = "0.7.9", features = ["macros"] }
diesel = { version = "2.2.5", features = ["postgres"] } diesel = { version = "2.2.5", features = ["chrono", "postgres"] }
diesel_migrations = { version = "2.2.0", features = ["postgres"] } diesel_migrations = { version = "2.2.0", features = ["postgres"] }
log = "0.4.22" log = "0.4.22"
micronfig = "0.3.0" micronfig = "0.3.0"
@ -15,3 +15,4 @@ skillratings = "0.27.1"
teloxide = { version = "0.13.0", features = ["webhooks-axum"] } teloxide = { version = "0.13.0", features = ["webhooks-axum"] }
tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread", "net"] } tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread", "net"] }
serde = { version = "1.0.215", features = ["derive"] } serde = { version = "1.0.215", features = ["derive"] }
chrono = "0.4.38"

View file

@ -6,4 +6,4 @@ file = "src/schema.rs"
custom_type_derives = ["diesel::query_builder::QueryId", "Clone"] custom_type_derives = ["diesel::query_builder::QueryId", "Clone"]
[migrations_directory] [migrations_directory]
dir = "/mnt/work/steffo/holycow_api/migrations" dir = "./migrations"

View file

@ -1 +0,0 @@
DROP TABLE IF EXISTS games;

View file

@ -1,11 +0,0 @@
CREATE TABLE games (
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
p1 BPCHAR,
p2 BPCHAR,
p1result BPCHAR,
CONSTRAINT not_self CHECK (p1 != p2),
CONSTRAINT acceptable_result CHECK (p1result = 'W' OR p1result = 'T' OR p1result = 'L')
);

View file

@ -0,0 +1,4 @@
DROP TABLE IF EXISTS matches;
DROP TYPE IF EXISTS outcome_t;
DROP TABLE IF EXISTS players;
DROP TYPE IF EXISTS wenglin_t;

View file

@ -0,0 +1,43 @@
CREATE TYPE wenglin_t AS (
rating float8,
uncertainty float8
);
CREATE TABLE players (
id INTEGER GENERATED ALWAYS AS IDENTITY,
wenglin wenglin_t NOT NULL DEFAULT ROW(25.0, 25.0 / 3),
telegram_id BIGINT,
CONSTRAINT telegram_ids_are_unique UNIQUE (telegram_id),
PRIMARY KEY (id)
);
CREATE TYPE outcome_t AS ENUM (
'AWins',
'BWins',
'Tie'
);
CREATE TABLE matches (
id INTEGER GENERATED ALWAYS AS IDENTITY,
instant TIMESTAMPTZ NOT NULL DEFAULT now(),
name VARCHAR,
player_a_id BIGINT NOT NULL,
player_a_wenglin_before wenglin_t NOT NULL,
player_a_wenglin_after wenglin_t NOT NULL,
player_b_id BIGINT NOT NULL,
player_b_wenglin_before wenglin_t NOT NULL,
player_b_wenglin_after wenglin_t NOT NULL,
outcome outcome_t NOT NULL,
CONSTRAINT match_unique_name UNIQUE (name),
CONSTRAINT not_same_player CHECK (player_a_id != player_b_id),
FOREIGN KEY (player_a_id) REFERENCES players (id),
FOREIGN KEY (player_b_id) REFERENCES players (id),
PRIMARY KEY (id)
);

View file

@ -1,7 +0,0 @@
SELECT
played,
wins
FROM
(SELECT COUNT(*) AS played FROM games WHERE :p = p1 OR :p = p2) AS pt,
(SELECT COUNT(*) AS wins FROM games WHERE (:p = p1 AND p1result = 'W') OR (:p = p2 AND p1result = 'L')) AS wt
;

View file

@ -1,13 +1,13 @@
use std::convert::Infallible; use std::convert::Infallible;
use std::process::exit; use std::process::exit;
use std::sync::Arc;
use anyhow::Context; use anyhow::Context;
use axum::http::StatusCode; use axum::http::StatusCode;
use diesel::{Connection, PgConnection}; use diesel::{Connection, PgConnection};
use diesel_migrations::MigrationHarness; use diesel_migrations::MigrationHarness;
use serde::{Deserialize, Serialize};
mod config; mod config;
mod schema;
mod types;
pub const MIGRATIONS: diesel_migrations::EmbeddedMigrations = diesel_migrations::embed_migrations!(); pub const MIGRATIONS: diesel_migrations::EmbeddedMigrations = diesel_migrations::embed_migrations!();
@ -55,13 +55,8 @@ async fn main() -> anyhow::Result<Infallible> {
exit(1) exit(1)
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct HolyCowResults {
played: u16,
wins: u16,
rating: u16,
}
fn results_handler() -> Result<HolyCowResults, StatusCode> {
#[axum::debug_handler]
async fn results_handler() -> Result<String, StatusCode> {
todo!()
} }

View file

@ -1,10 +1,46 @@
// @generated automatically by Diesel CLI. // @generated automatically by Diesel CLI.
pub mod sql_types {
#[derive(diesel::query_builder::QueryId, Clone, diesel::sql_types::SqlType)]
#[diesel(postgres_type(name = "outcome_t"))]
pub struct OutcomeT;
#[derive(diesel::query_builder::QueryId, Clone, diesel::sql_types::SqlType)]
#[diesel(postgres_type(name = "wenglin_t"))]
pub struct WenglinT;
}
diesel::table! { diesel::table! {
games (id) { use diesel::sql_types::*;
use super::sql_types::WenglinT;
use super::sql_types::OutcomeT;
matches (id) {
id -> Int4, id -> Int4,
p1 -> Nullable<Bpchar>, instant -> Timestamptz,
p2 -> Nullable<Bpchar>, name -> Nullable<Varchar>,
p1result -> Nullable<Bpchar>, player_a_id -> Int8,
player_a_wenglin_before -> WenglinT,
player_a_wenglin_after -> WenglinT,
player_b_id -> Int8,
player_b_wenglin_before -> WenglinT,
player_b_wenglin_after -> WenglinT,
outcome -> OutcomeT,
} }
} }
diesel::table! {
use diesel::sql_types::*;
use super::sql_types::WenglinT;
players (id) {
id -> Int4,
wenglin -> WenglinT,
telegram_id -> Nullable<Int8>,
}
}
diesel::allow_tables_to_appear_in_same_query!(
matches,
players,
);

View file

@ -0,0 +1,137 @@
use std::io::Write;
use chrono::{DateTime, Utc};
use diesel::{AsExpression, FromSqlRow, Identifiable, Insertable, Queryable, QueryableByName, Selectable};
use diesel::backend::Backend;
use diesel::deserialize::FromSql;
use diesel::pg::Pg;
use diesel::serialize::ToSql;
use diesel::sql_types as sql;
use diesel::serialize::Output as DieselOutput;
use crate::schema;
#[derive(Debug, Clone, FromSqlRow, AsExpression)]
#[diesel(sql_type = sql::BigInt)]
#[diesel(check_for_backend(Pg))]
pub struct TelegramId(pub teloxide::types::ChatId);
#[derive(Debug, Clone, FromSqlRow, AsExpression)]
#[diesel(sql_type = schema::sql_types::WenglinT)]
#[diesel(check_for_backend(Pg))]
pub struct WengLinRating(pub skillratings::weng_lin::WengLinRating);
#[derive(Debug, Clone, Queryable, QueryableByName, Identifiable, Selectable)]
#[diesel(table_name = schema::players)]
#[diesel(check_for_backend(Pg))]
pub struct Player {
pub id: i32,
pub wenglin: WengLinRating,
pub telegram_id: Option<TelegramId>,
}
#[derive(Debug, Clone, Insertable)]
#[diesel(table_name = schema::players)]
#[diesel(check_for_backend(Pg))]
pub struct PlayerI {
pub wenglin: WengLinRating,
pub telegram_id: TelegramId,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, FromSqlRow, AsExpression)]
#[diesel(sql_type = schema::sql_types::OutcomeT)]
pub enum Outcome {
AWins,
BWins,
Tie,
}
#[derive(Debug, Clone, Queryable, QueryableByName, Identifiable, Selectable)]
#[diesel(table_name = schema::matches)]
#[diesel(check_for_backend(Pg))]
pub struct Match {
pub id: i32,
pub instant: DateTime<Utc>,
pub name: Option<String>,
pub player_a_id: TelegramId,
pub player_a_wenglin_before: WengLinRating,
pub player_a_wenglin_after: WengLinRating,
pub player_b_id: TelegramId,
pub player_b_wenglin_before: WengLinRating,
pub player_b_wenglin_after: WengLinRating,
pub outcome: Outcome,
}
#[derive(Debug, Clone, Insertable)]
#[diesel(table_name = schema::matches)]
#[diesel(check_for_backend(Pg))]
pub struct MatchI {
pub instant: DateTime<Utc>,
pub name: Option<String>,
pub player_a_id: TelegramId,
pub player_a_wenglin_before: WengLinRating,
pub player_a_wenglin_after: WengLinRating,
pub player_b_id: TelegramId,
pub player_b_wenglin_before: WengLinRating,
pub player_b_wenglin_after: WengLinRating,
pub outcome: Outcome,
}
impl FromSql<sql::BigInt, Pg> for TelegramId {
fn from_sql(bytes: <Pg as Backend>::RawValue<'_>) -> diesel::deserialize::Result<Self> {
let s = <i64 as FromSql<sql::BigInt, Pg>>::from_sql(bytes)?;
Ok(Self(teloxide::types::ChatId(s)))
}
}
impl FromSql<schema::sql_types::WenglinT, Pg> for WengLinRating {
fn from_sql(bytes: <Pg as Backend>::RawValue<'_>) -> diesel::deserialize::Result<Self> {
let rating = <f64 as FromSql<sql::Double, Pg>>::from_sql(bytes)?;
let uncertainty = <f64 as FromSql<sql::Double, Pg>>::from_sql(bytes)?;
let rating = skillratings::weng_lin::WengLinRating::from((rating, uncertainty));
Ok(Self(rating))
}
}
impl FromSql<schema::sql_types::OutcomeT, Pg> for Outcome {
fn from_sql(bytes: <Pg as Backend>::RawValue<'_>) -> diesel::deserialize::Result<Self> {
let o = match bytes.as_bytes() {
b"AWins" => Self::AWins,
b"BWins" => Self::BWins,
b"Tie" => Self::Tie,
_ => Err(anyhow::Error::msg("unknown enum variant"))?
};
Ok(o)
}
}
impl ToSql<sql::BigInt, Pg> for TelegramId {
fn to_sql<'b>(&'b self, out: &mut diesel::serialize::Output<'b, '_, Pg>) -> diesel::serialize::Result {
<i64 as ToSql<sql::BigInt, Pg>>::to_sql(&self.0.0, out)
}
}
impl ToSql<schema::sql_types::WenglinT, Pg> for WengLinRating {
fn to_sql<'b>(&'b self, out: &mut DieselOutput<'b, '_, Pg>) -> diesel::serialize::Result {
<f64 as ToSql<sql::Double, Pg>>::to_sql(&self.0.rating, out)?;
<f64 as ToSql<sql::Double, Pg>>::to_sql(&self.0.uncertainty, out)?;
Ok(diesel::serialize::IsNull::No)
}
}
impl ToSql<schema::sql_types::OutcomeT, Pg> for Outcome {
fn to_sql<'b>(&'b self, out: &mut DieselOutput<'b, '_, Pg>) -> diesel::serialize::Result {
out.write_all(
match self {
Outcome::AWins => b"AWins",
Outcome::BWins => b"BWins",
Outcome::Tie => b"Tie",
}
)?;
Ok(diesel::serialize::IsNull::No)
}
}

View file

@ -1,5 +1,5 @@
{ {
"name": "holycow", "name": "holycow_frontend",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {