diff --git a/.idea/runConfigurations/Backend.xml b/.idea/runConfigurations/Backend.xml
new file mode 100644
index 0000000..adc1c75
--- /dev/null
+++ b/.idea/runConfigurations/Backend.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/Frontend.xml b/.idea/runConfigurations/Frontend.xml
index f783027..bfc0be5 100644
--- a/.idea/runConfigurations/Frontend.xml
+++ b/.idea/runConfigurations/Frontend.xml
@@ -8,7 +8,7 @@
-
+
diff --git a/holycow_backend/Cargo.lock b/holycow_backend/Cargo.lock
index fb20135..c4d430c 100644
--- a/holycow_backend/Cargo.lock
+++ b/holycow_backend/Cargo.lock
@@ -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"
diff --git a/holycow_backend/Cargo.toml b/holycow_backend/Cargo.toml
index 052fd0e..f71addf 100644
--- a/holycow_backend/Cargo.toml
+++ b/holycow_backend/Cargo.toml
@@ -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"
diff --git a/holycow_backend/diesel.toml b/holycow_backend/diesel.toml
index 9624cfd..83d15a9 100644
--- a/holycow_backend/diesel.toml
+++ b/holycow_backend/diesel.toml
@@ -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"
diff --git a/holycow_backend/migrations/2024-11-27-175800_first/down.sql b/holycow_backend/migrations/2024-11-27-175800_first/down.sql
deleted file mode 100644
index df130f5..0000000
--- a/holycow_backend/migrations/2024-11-27-175800_first/down.sql
+++ /dev/null
@@ -1 +0,0 @@
-DROP TABLE IF EXISTS games;
diff --git a/holycow_backend/migrations/2024-11-27-175800_first/up.sql b/holycow_backend/migrations/2024-11-27-175800_first/up.sql
deleted file mode 100644
index 601536b..0000000
--- a/holycow_backend/migrations/2024-11-27-175800_first/up.sql
+++ /dev/null
@@ -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')
-);
diff --git a/holycow_backend/migrations/2024-11-28-084240_create players/down.sql b/holycow_backend/migrations/2024-11-28-084240_create players/down.sql
new file mode 100644
index 0000000..c9ab14a
--- /dev/null
+++ b/holycow_backend/migrations/2024-11-28-084240_create players/down.sql
@@ -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;
diff --git a/holycow_backend/migrations/2024-11-28-084240_create players/up.sql b/holycow_backend/migrations/2024-11-28-084240_create players/up.sql
new file mode 100644
index 0000000..17a930c
--- /dev/null
+++ b/holycow_backend/migrations/2024-11-28-084240_create players/up.sql
@@ -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)
+);
diff --git a/holycow_backend/src/get_results.sql b/holycow_backend/src/get_results.sql
deleted file mode 100644
index 3682072..0000000
--- a/holycow_backend/src/get_results.sql
+++ /dev/null
@@ -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
-;
diff --git a/holycow_backend/src/main.rs b/holycow_backend/src/main.rs
index d2b95e2..5df52ce 100644
--- a/holycow_backend/src/main.rs
+++ b/holycow_backend/src/main.rs
@@ -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 {
exit(1)
}
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-struct HolyCowResults {
- played: u16,
- wins: u16,
- rating: u16,
-}
-
-fn results_handler() -> Result {
+#[axum::debug_handler]
+async fn results_handler() -> Result {
+ todo!()
}
diff --git a/holycow_backend/src/schema.rs b/holycow_backend/src/schema.rs
index 2ef243f..f40ae01 100644
--- a/holycow_backend/src/schema.rs
+++ b/holycow_backend/src/schema.rs
@@ -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,
- p2 -> Nullable,
- p1result -> Nullable,
+ instant -> Timestamptz,
+ name -> Nullable,
+ 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,
+ }
+}
+
+diesel::allow_tables_to_appear_in_same_query!(
+ matches,
+ players,
+);
diff --git a/holycow_backend/src/types.rs b/holycow_backend/src/types.rs
new file mode 100644
index 0000000..3b80b99
--- /dev/null
+++ b/holycow_backend/src/types.rs
@@ -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,
+}
+
+#[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,
+ pub name: Option,
+ 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,
+ pub name: Option,
+ 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 for TelegramId {
+ fn from_sql(bytes: ::RawValue<'_>) -> diesel::deserialize::Result {
+ let s = >::from_sql(bytes)?;
+
+ Ok(Self(teloxide::types::ChatId(s)))
+ }
+}
+
+impl FromSql for WengLinRating {
+ fn from_sql(bytes: ::RawValue<'_>) -> diesel::deserialize::Result {
+ let rating = >::from_sql(bytes)?;
+ let uncertainty = >::from_sql(bytes)?;
+
+ let rating = skillratings::weng_lin::WengLinRating::from((rating, uncertainty));
+ Ok(Self(rating))
+ }
+}
+
+impl FromSql for Outcome {
+ fn from_sql(bytes: ::RawValue<'_>) -> diesel::deserialize::Result {
+ 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 for TelegramId {
+ fn to_sql<'b>(&'b self, out: &mut diesel::serialize::Output<'b, '_, Pg>) -> diesel::serialize::Result {
+ >::to_sql(&self.0.0, out)
+ }
+}
+
+impl ToSql for WengLinRating {
+ fn to_sql<'b>(&'b self, out: &mut DieselOutput<'b, '_, Pg>) -> diesel::serialize::Result {
+ >::to_sql(&self.0.rating, out)?;
+ >::to_sql(&self.0.uncertainty, out)?;
+
+ Ok(diesel::serialize::IsNull::No)
+ }
+}
+
+impl ToSql 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)
+ }
+}
\ No newline at end of file
diff --git a/holycow_frontend/package.json b/holycow_frontend/package.json
index a7c89aa..a08a9f2 100644
--- a/holycow_frontend/package.json
+++ b/holycow_frontend/package.json
@@ -1,5 +1,5 @@
{
- "name": "holycow",
+ "name": "holycow_frontend",
"version": "0.1.0",
"private": true,
"scripts": {