Figure out database shenanigans
This commit is contained in:
parent
8823120942
commit
5ca754e972
14 changed files with 310 additions and 39 deletions
20
.idea/runConfigurations/Backend.xml
Normal file
20
.idea/runConfigurations/Backend.xml
Normal 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>
|
|
@ -8,7 +8,7 @@
|
|||
<node-interpreter value="project" />
|
||||
<envs />
|
||||
<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>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
|
|
56
holycow_backend/Cargo.lock
generated
56
holycow_backend/Cargo.lock
generated
|
@ -26,6 +26,21 @@ dependencies = [
|
|||
"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]]
|
||||
name = "anyhow"
|
||||
version = "1.0.93"
|
||||
|
@ -202,7 +217,12 @@ version = "0.4.38"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"wasm-bindgen",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -318,6 +338,7 @@ checksum = "cbf9649c05e0a9dbd6d0b0b8301db5182b972d0fd02f0a7c6736cf632d7c0fd5"
|
|||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"byteorder",
|
||||
"chrono",
|
||||
"diesel_derives",
|
||||
"itoa",
|
||||
"pq-sys",
|
||||
|
@ -630,11 +651,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
|
||||
|
||||
[[package]]
|
||||
name = "holycow_api"
|
||||
name = "holycow_backend"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
"chrono",
|
||||
"diesel",
|
||||
"diesel_migrations",
|
||||
"log",
|
||||
|
@ -792,6 +814,29 @@ dependencies = [
|
|||
"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]]
|
||||
name = "icu_collections"
|
||||
version = "1.5.0"
|
||||
|
@ -2211,6 +2256,15 @@ dependencies = [
|
|||
"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]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
[package]
|
||||
name = "holycow_api"
|
||||
name = "holycow_backend"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.93"
|
||||
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"] }
|
||||
log = "0.4.22"
|
||||
micronfig = "0.3.0"
|
||||
|
@ -15,3 +15,4 @@ skillratings = "0.27.1"
|
|||
teloxide = { version = "0.13.0", features = ["webhooks-axum"] }
|
||||
tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread", "net"] }
|
||||
serde = { version = "1.0.215", features = ["derive"] }
|
||||
chrono = "0.4.38"
|
||||
|
|
|
@ -6,4 +6,4 @@ file = "src/schema.rs"
|
|||
custom_type_derives = ["diesel::query_builder::QueryId", "Clone"]
|
||||
|
||||
[migrations_directory]
|
||||
dir = "/mnt/work/steffo/holycow_api/migrations"
|
||||
dir = "./migrations"
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
DROP TABLE IF EXISTS games;
|
|
@ -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')
|
||||
);
|
|
@ -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;
|
|
@ -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)
|
||||
);
|
|
@ -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
|
||||
;
|
|
@ -1,13 +1,13 @@
|
|||
use std::convert::Infallible;
|
||||
use std::process::exit;
|
||||
use std::sync::Arc;
|
||||
use anyhow::Context;
|
||||
use axum::http::StatusCode;
|
||||
use diesel::{Connection, PgConnection};
|
||||
use diesel_migrations::MigrationHarness;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
mod config;
|
||||
mod schema;
|
||||
mod types;
|
||||
|
||||
pub const MIGRATIONS: diesel_migrations::EmbeddedMigrations = diesel_migrations::embed_migrations!();
|
||||
|
||||
|
@ -55,13 +55,8 @@ async fn main() -> anyhow::Result<Infallible> {
|
|||
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!()
|
||||
}
|
||||
|
|
|
@ -1,10 +1,46 @@
|
|||
// @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! {
|
||||
games (id) {
|
||||
use diesel::sql_types::*;
|
||||
use super::sql_types::WenglinT;
|
||||
use super::sql_types::OutcomeT;
|
||||
|
||||
matches (id) {
|
||||
id -> Int4,
|
||||
p1 -> Nullable<Bpchar>,
|
||||
p2 -> Nullable<Bpchar>,
|
||||
p1result -> Nullable<Bpchar>,
|
||||
instant -> Timestamptz,
|
||||
name -> Nullable<Varchar>,
|
||||
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,
|
||||
);
|
||||
|
|
137
holycow_backend/src/types.rs
Normal file
137
holycow_backend/src/types.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "holycow",
|
||||
"name": "holycow_frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
|
Loading…
Reference in a new issue