diff --git a/apps/bot/src/main.rs b/apps/bot/src/main.rs index 8953447..b3b7787 100644 --- a/apps/bot/src/main.rs +++ b/apps/bot/src/main.rs @@ -1,6 +1,6 @@ use config::parse_local_config; use poise::serenity_prelude as serenity; -use tracing::{error, info}; +use tracing::{error, info, instrument}; pub mod config; pub mod dotenv; @@ -11,6 +11,7 @@ type Error = Box; type Context<'a> = poise::Context<'a, Data, Error>; /// Displays your or another user's account creation date +#[instrument(skip(ctx), level = "info", fields(channel_id = ctx.channel_id().get() , guild_id = ?ctx.guild_id(), user_id = ?ctx.author().id.get(), user_name = ctx.author().name))] #[poise::command(slash_command, prefix_command)] async fn age( ctx: Context<'_>, diff --git a/apps/bot/src/model/mod.rs b/apps/bot/src/model/mod.rs index c321b79..bb6098d 100644 --- a/apps/bot/src/model/mod.rs +++ b/apps/bot/src/model/mod.rs @@ -1,6 +1,9 @@ use std::{error::Error, sync::Arc}; pub mod trivial; +pub mod trivial_point; +pub mod trivial_question; +pub mod trivial_round; use clickhouse_pool::{ config::{ClickhouseConfig, DatalakeConfig, RetryConfig}, @@ -9,6 +12,9 @@ use clickhouse_pool::{ }; use tracing::{error, info, instrument}; use trivial::Trivial; +use trivial_point::TrivialPoint; +use trivial_question::TrivialQuestion; +use trivial_round::TrivialRound; use crate::config::PersistenceConfig; @@ -37,20 +43,51 @@ pub fn create_pool_manager(db_config: PersistenceConfig) -> Result, ) -> Result, Box> { - let manager = PoolManager::new(datalake_config, None).await; + let mut manager = PoolManager::new(datalake_config, None).await; - let _ = match manager - .execute_with_retry(Trivial::create_table_sql()) - .await - { - Ok(_) => { - info!("Table {} created successfully", Trivial::table_name()); - } + manager = match create_table::(manager).await { + Ok(manager) => manager, Err(e) => { - error!("Failed to create table {} : {}", Trivial::table_name(), e); - return Err(Box::new(e)); + return Err(e); } }; + manager = match create_table::(manager).await { + Ok(manager) => manager, + Err(e) => { + return Err(e); + } + }; + + manager = match create_table::(manager).await { + Ok(manager) => manager, + Err(e) => { + return Err(e); + } + }; + + manager = match create_table::(manager).await { + Ok(manager) => manager, + Err(e) => { + return Err(e); + } + }; + + info!("All tables created successfully"); + Ok(Arc::new(manager)) } + +#[instrument(skip(manager))] +pub async fn create_table(manager: PoolManager) -> Result> { + let _ = match manager.execute_with_retry(T::create_table_sql()).await { + Ok(_) => { + info!("Table {} created successfully", T::table_name()); + } + Err(e) => { + error!("Failed to create table {} : {}", T::table_name(), e); + return Err(Box::new(e)); + } + }; + Ok(manager) +} diff --git a/apps/bot/src/model/trivial_point.rs b/apps/bot/src/model/trivial_point.rs new file mode 100644 index 0000000..784034a --- /dev/null +++ b/apps/bot/src/model/trivial_point.rs @@ -0,0 +1,121 @@ +use chrono::{DateTime, Utc}; +use clickhouse::Row; +use clickhouse_pool::traits::Model; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug, Clone, PartialEq, PartialOrd, Row, Serialize, Deserialize)] +pub struct TrivialPoint { + #[serde(with = "clickhouse::serde::uuid")] + pub id: Uuid, + #[serde(with = "clickhouse::serde::uuid")] + pub trivial_id: Uuid, + + pub user_id: u64, + + pub points: i64, + + #[serde(with = "clickhouse::serde::chrono::datetime64::millis")] + pub created_at: DateTime, + #[serde(with = "clickhouse::serde::chrono::datetime64::millis")] + pub updated_at: DateTime, + + pub updater_id: u64, +} + +impl Model for TrivialPoint { + type T = TrivialPoint; + + fn table_name() -> &'static str { + "trivial_point" + } + + fn create_table_sql() -> &'static str { + r#" + CREATE TABLE IF NOT EXISTS trivial_point ( + id UUID PRIMARY KEY, + trivial_id UUID, + user_id UInt64, + points Int64, + created_at DateTime64(3), + updated_at DateTime64(3), + updater_id UInt64 + ) ENGINE = MergeTree() + ORDER BY (id, trivial_id) + "# + .trim() + } + + fn column_names() -> Vec<&'static str> { + vec![ + "id", + "trivial_id", + "user_id", + "points", + "created_at", + "updated_at", + "updater_id", + ] + } + + fn to_row(&self) -> (Vec<&'static str>, Vec) { + ( + Self::column_names(), + vec![ + self.id.to_string(), + self.trivial_id.to_string(), + self.user_id.to_string(), + self.points.to_string(), + self.created_at.to_rfc3339(), + self.updated_at.to_rfc3339(), + self.updater_id.to_string(), + ], + ) + } + + fn insert_query(&self) -> String { + let (columns, values) = self.to_row(); + let columns = columns.join(", "); + let values = values.join(", "); + format!( + "INSERT INTO {} ({}) VALUES ({})", + Self::table_name(), + columns, + values + ) + } + + fn batch_insert_query(items: &[Self::T]) -> String { + let mut queries = Vec::new(); + for item in items { + let (columns, values) = item.to_row(); + let columns = columns.join(", "); + let values = values.join(", "); + queries.push(format!( + "INSERT INTO {} ({}) VALUES ({})", + Self::table_name(), + columns, + values + )); + } + queries.join("; ") + } + + fn build_select_query( + where_clause: Option<&str>, + limit: Option, + offset: Option, + ) -> String { + let mut query = format!("SELECT * FROM {}", Self::table_name()); + if let Some(where_clause) = where_clause { + query.push_str(&format!(" WHERE {}", where_clause)); + } + if let Some(limit) = limit { + query.push_str(&format!(" LIMIT {}", limit)); + } + if let Some(offset) = offset { + query.push_str(&format!(" OFFSET {}", offset)); + } + query + } +} diff --git a/apps/bot/src/model/trivial_question.rs b/apps/bot/src/model/trivial_question.rs new file mode 100644 index 0000000..3f2f103 --- /dev/null +++ b/apps/bot/src/model/trivial_question.rs @@ -0,0 +1,119 @@ +use chrono::{DateTime, Utc}; +use clickhouse::Row; +use clickhouse_pool::traits::Model; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug, Row, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +pub struct TrivialQuestion { + #[serde(with = "clickhouse::serde::uuid")] + pub id: Uuid, + #[serde(with = "clickhouse::serde::uuid")] + pub trivial_id: Uuid, + + pub question: String, + pub answer: String, + + #[serde(with = "clickhouse::serde::chrono::datetime64::millis")] + pub created_at: DateTime, + #[serde(with = "clickhouse::serde::chrono::datetime64::millis")] + pub updated_at: DateTime, + + pub creator_id: u64, + pub updater_id: u64, +} + +impl Model for TrivialQuestion { + type T = TrivialQuestion; + + fn table_name() -> &'static str { + "trivial_question" + } + + fn create_table_sql() -> &'static str { + r#" + CREATE TABLE IF NOT EXISTS trivial_question ( + id UUID PRIMARY KEY, + trivial_id UUID, + question String, + answer String, + created_at DateTime64(3), + updated_at DateTime64(3), + creator_id UInt64, + updater_id UInt64 + ) ENGINE = MergeTree() + ORDER BY (id, trivial_id) + "# + .trim() + } + + fn column_names() -> Vec<&'static str> { + vec![ + "id", + "trivial_id", + "question", + "answer", + "created_at", + "updated_at", + "creator_id", + "updater_id", + ] + } + + fn to_row(&self) -> (Vec<&'static str>, Vec) { + ( + Self::column_names(), + vec![ + self.id.to_string(), + self.trivial_id.to_string(), + self.question.clone(), + self.answer.clone(), + self.created_at.to_string(), + self.updated_at.to_string(), + self.creator_id.to_string(), + self.updater_id.to_string(), + ], + ) + } + + fn insert_query(&self) -> String { + let (columns, values) = self.to_row(); + let columns = columns.join(", "); + let values = values.join(", "); + format!( + "INSERT INTO {} ({}) VALUES ({})", + Self::table_name(), + columns, + values + ) + } + + fn batch_insert_query(items: &[Self::T]) -> String { + let mut queries = Vec::new(); + for item in items { + let (columns, values) = item.to_row(); + let columns = columns.join(", "); + let values = values.join(", "); + queries.push(format!("({}, {})", columns, values)); + } + queries.join(", ") + } + + fn build_select_query( + where_clause: Option<&str>, + limit: Option, + offset: Option, + ) -> String { + let mut query = format!("SELECT * FROM {}", Self::table_name()); + if let Some(where_clause) = where_clause { + query.push_str(&format!(" WHERE {}", where_clause)); + } + if let Some(limit) = limit { + query.push_str(&format!(" LIMIT {}", limit)); + } + if let Some(offset) = offset { + query.push_str(&format!(" OFFSET {}", offset)); + } + query + } +} diff --git a/apps/bot/src/model/trivial_round.rs b/apps/bot/src/model/trivial_round.rs new file mode 100644 index 0000000..a30cd24 --- /dev/null +++ b/apps/bot/src/model/trivial_round.rs @@ -0,0 +1,116 @@ +use chrono::{DateTime, Utc}; +use clickhouse::Row; +use clickhouse_pool::traits::Model; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug, Clone, PartialEq, PartialOrd, Row, Serialize, Deserialize)] +pub struct TrivialRound { + #[serde(with = "clickhouse::serde::uuid")] + pub id: Uuid, + #[serde(with = "clickhouse::serde::uuid")] + pub trivial_id: Uuid, + #[serde(with = "clickhouse::serde::uuid")] + pub question_id: Uuid, + pub answer: Vec<(u64, String, DateTime)>, + + #[serde(with = "clickhouse::serde::chrono::datetime64::millis")] + pub created_at: DateTime, + #[serde(with = "clickhouse::serde::chrono::datetime64::millis")] + pub updated_at: DateTime, +} + +impl Model for TrivialRound { + type T = TrivialRound; + + fn table_name() -> &'static str { + "trivial_round" + } + + fn create_table_sql() -> &'static str { + r#" + CREATE TABLE IF NOT EXISTS trivial_round ( + id UUID PRIMARY KEY, + trivial_id UUID, + question_id UUID, + answer Array(Tuple(UInt64, String, DateTime64(3))), + created_at DateTime64(3), + updated_at DateTime64(3) + ) ENGINE = MergeTree() + ORDER BY (id, trivial_id) + "# + .trim() + } + + fn column_names() -> Vec<&'static str> { + vec![ + "id", + "trivial_id", + "question_id", + "answer", + "created_at", + "updated_at", + ] + } + + fn to_row(&self) -> (Vec<&'static str>, Vec) { + ( + Self::column_names(), + vec![ + self.id.to_string(), + self.trivial_id.to_string(), + self.question_id.to_string(), + format!("{:?}", self.answer), + self.created_at.to_string(), + self.updated_at.to_string(), + ], + ) + } + + fn insert_query(&self) -> String { + let (columns, values) = self.to_row(); + let columns_str = columns.join(", "); + let values_str = values.join(", "); + + format!( + "INSERT INTO {} ({}) VALUES ({})", + Self::table_name(), + columns_str, + values_str + ) + } + + fn batch_insert_query(items: &[Self::T]) -> String { + let mut queries = Vec::new(); + for item in items { + let (columns, values) = item.to_row(); + let columns_str = columns.join(", "); + let values_str = values.join(", "); + queries.push(format!( + "INSERT INTO {} ({}) VALUES ({})", + Self::table_name(), + columns_str, + values_str + )); + } + queries.join("; ") + } + + fn build_select_query( + where_clause: Option<&str>, + limit: Option, + offset: Option, + ) -> String { + let mut query = format!("SELECT * FROM {}", Self::table_name()); + if let Some(where_clause) = where_clause { + query.push_str(&format!(" WHERE {}", where_clause)); + } + if let Some(limit) = limit { + query.push_str(&format!(" LIMIT {}", limit)); + } + if let Some(offset) = offset { + query.push_str(&format!(" OFFSET {}", offset)); + } + query + } +} diff --git a/compose.yaml b/compose.yaml index 1900c06..0315aad 100644 --- a/compose.yaml +++ b/compose.yaml @@ -35,7 +35,7 @@ services: SERVER_click: database USER_click: default PASSWORD_click: password - PORT_click: 9000 + PORT_click: 8123 ENGINE_click: clickhouse@dbgate-plugin-clickhouse DATABASE_click: default depends_on: