diff --git a/src/leetcode/0511.game-play-analysis-i/.env b/src/leetcode/0511.game-play-analysis-i/.env new file mode 100644 index 00000000..453c39f8 --- /dev/null +++ b/src/leetcode/0511.game-play-analysis-i/.env @@ -0,0 +1 @@ +DATABASE_URL=postgres://leetcode:leetcode-password@localhost/leetcode diff --git a/src/leetcode/0511.game-play-analysis-i/.gitignore b/src/leetcode/0511.game-play-analysis-i/.gitignore new file mode 100644 index 00000000..c2de89b2 --- /dev/null +++ b/src/leetcode/0511.game-play-analysis-i/.gitignore @@ -0,0 +1 @@ +/db diff --git a/src/leetcode/0511.game-play-analysis-i/Cargo.toml b/src/leetcode/0511.game-play-analysis-i/Cargo.toml new file mode 100644 index 00000000..a868b43c --- /dev/null +++ b/src/leetcode/0511.game-play-analysis-i/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "lc-0511-game-play-analysis-i" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +diesel = { version = "2.2.0", default-features = false, features = [ "chrono", "postgres", "r2d2" ] } +dotenvy = "0.15.7" +env_logger = "0.11.3" +log = "0.4.21" +r2d2 = "0.8.10" diff --git a/src/leetcode/0511.game-play-analysis-i/diesel.toml b/src/leetcode/0511.game-play-analysis-i/diesel.toml new file mode 100644 index 00000000..da5ad1ab --- /dev/null +++ b/src/leetcode/0511.game-play-analysis-i/diesel.toml @@ -0,0 +1,9 @@ +# For documentation on how to configure this file, +# see https://diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "src/schema.rs" +custom_type_derives = ["diesel::query_builder::QueryId", "Clone"] + +[migrations_directory] +dir = "/home/shaohua/dev/rust/TheAlgorithms/src/leetcode/0511.game-play-analysis-i/migrations" diff --git a/src/leetcode/0511.game-play-analysis-i/docker-compose.yml b/src/leetcode/0511.game-play-analysis-i/docker-compose.yml new file mode 100644 index 00000000..afd296b1 --- /dev/null +++ b/src/leetcode/0511.game-play-analysis-i/docker-compose.yml @@ -0,0 +1,13 @@ +version: "3.0" +services: + leetcode_db: + image: postgres:15.3 + restart: always + ports: + - 127.0.0.1:5432:5432 + environment: + POSTGRES_PASSWORD: leetcode-password + POSTGRES_USER: leetcode + POSTGRES_DB: leetcode + volumes: + - ./db:/var/lib/postgresql diff --git a/src/leetcode/0511.game-play-analysis-i/index.md b/src/leetcode/0511.game-play-analysis-i/index.md new file mode 100644 index 00000000..77a618ba --- /dev/null +++ b/src/leetcode/0511.game-play-analysis-i/index.md @@ -0,0 +1,4 @@ + +# + +[问题描述](../problems/) diff --git a/src/leetcode/0511.game-play-analysis-i/migrations/.keep b/src/leetcode/0511.game-play-analysis-i/migrations/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/leetcode/0511.game-play-analysis-i/migrations/00000000000000_diesel_initial_setup/down.sql b/src/leetcode/0511.game-play-analysis-i/migrations/00000000000000_diesel_initial_setup/down.sql new file mode 100644 index 00000000..a9f52609 --- /dev/null +++ b/src/leetcode/0511.game-play-analysis-i/migrations/00000000000000_diesel_initial_setup/down.sql @@ -0,0 +1,6 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + +DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); +DROP FUNCTION IF EXISTS diesel_set_updated_at(); diff --git a/src/leetcode/0511.game-play-analysis-i/migrations/00000000000000_diesel_initial_setup/up.sql b/src/leetcode/0511.game-play-analysis-i/migrations/00000000000000_diesel_initial_setup/up.sql new file mode 100644 index 00000000..d68895b1 --- /dev/null +++ b/src/leetcode/0511.game-play-analysis-i/migrations/00000000000000_diesel_initial_setup/up.sql @@ -0,0 +1,36 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + + + + +-- Sets up a trigger for the given table to automatically set a column called +-- `updated_at` whenever the row is modified (unless `updated_at` was included +-- in the modified columns) +-- +-- # Example +-- +-- ```sql +-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); +-- +-- SELECT diesel_manage_updated_at('users'); +-- ``` +CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ +BEGIN + EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s + FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ +BEGIN + IF ( + NEW IS DISTINCT FROM OLD AND + NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at + ) THEN + NEW.updated_at := current_timestamp; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; diff --git a/src/leetcode/0511.game-play-analysis-i/migrations/2024-06-01-053932_activity/down.sql b/src/leetcode/0511.game-play-analysis-i/migrations/2024-06-01-053932_activity/down.sql new file mode 100644 index 00000000..5be5184d --- /dev/null +++ b/src/leetcode/0511.game-play-analysis-i/migrations/2024-06-01-053932_activity/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE activity; diff --git a/src/leetcode/0511.game-play-analysis-i/migrations/2024-06-01-053932_activity/up.sql b/src/leetcode/0511.game-play-analysis-i/migrations/2024-06-01-053932_activity/up.sql new file mode 100644 index 00000000..2c1864e0 --- /dev/null +++ b/src/leetcode/0511.game-play-analysis-i/migrations/2024-06-01-053932_activity/up.sql @@ -0,0 +1,16 @@ +-- Your SQL goes here + +CREATE TABLE IF NOT EXISTS activity +( + player_id INTEGER NOT NULL, + device_id INTEGER NOT NULL, + event_date DATE NOT NULL DEFAULT CURRENT_DATE, + games_played INTEGER NOT NULL, + PRIMARY KEY (player_id, event_date) +); + +INSERT INTO activity (player_id, device_id, event_date, games_played) VALUES ('1', '2', '2016-03-01', '5'); +INSERT INTO activity (player_id, device_id, event_date, games_played) VALUES ('1', '2', '2016-05-02', '6'); +INSERT INTO activity (player_id, device_id, event_date, games_played) VALUES ('2', '3', '2017-06-25', '1'); +INSERT INTO activity (player_id, device_id, event_date, games_played) VALUES ('3', '1', '2016-03-02', '0'); +INSERT INTO activity (player_id, device_id, event_date, games_played) VALUES ('3', '4', '2018-07-03', '5'); diff --git a/src/leetcode/0511.game-play-analysis-i/query.sql b/src/leetcode/0511.game-play-analysis-i/query.sql new file mode 100644 index 00000000..3fe32cd4 --- /dev/null +++ b/src/leetcode/0511.game-play-analysis-i/query.sql @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2024 Xu Shaohua . All rights reserved. + * Use of this source is governed by General Public License that can be found + * in the LICENSE file. + */ + +SELECT player_id, MIN(event_date) AS first_login +FROM activity +GROUP BY player_id; \ No newline at end of file diff --git a/src/leetcode/0511.game-play-analysis-i/src/db.rs b/src/leetcode/0511.game-play-analysis-i/src/db.rs new file mode 100644 index 00000000..c8d1e3be --- /dev/null +++ b/src/leetcode/0511.game-play-analysis-i/src/db.rs @@ -0,0 +1,37 @@ +// Copyright (c) 2024 Xu Shaohua . All rights reserved. +// Use of this source is governed by General Public License that can be found +// in the LICENSE file. + +use diesel::pg::PgConnection; +use diesel::r2d2::{ConnectionManager, Pool}; + +use crate::error::{Error, ErrorKind}; + +pub type DbPool = Pool>; + +/// Create postgres database connection pool. +/// +/// # Errors +/// +/// Returns error if: +/// - No `DATABASE_URL` is set in current environment. +/// - Failed to connect to database. +pub fn get_connection_pool() -> Result { + let url = std::env::var("DATABASE_URL").map_err(|err| { + Error::from_string( + ErrorKind::ConfigError, + format!("DATABASE_URL is not set in environment, err: {err:?}"), + ) + })?; + let manager = ConnectionManager::::new(&url); + + Pool::builder() + .test_on_check_out(true) + .build(manager) + .map_err(|err| { + Error::from_string( + ErrorKind::DbConnError, + format!("Failed to create connection pool, url: {url}, err: {err:?}"), + ) + }) +} diff --git a/src/leetcode/0511.game-play-analysis-i/src/error.rs b/src/leetcode/0511.game-play-analysis-i/src/error.rs new file mode 100644 index 00000000..c9baa518 --- /dev/null +++ b/src/leetcode/0511.game-play-analysis-i/src/error.rs @@ -0,0 +1,117 @@ +// Copyright (c) 2022 Xu Shaohua . All rights reserved. +// Use of this source is governed by GNU General Public License +// that can be found in the LICENSE file. + +#![allow(clippy::enum_variant_names)] + +use diesel::result::DatabaseErrorKind; +use std::fmt::{Display, Formatter}; +use std::io; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ErrorKind { + ConfigError, + + DbConnError, + DbGeneralError, + DbUniqueViolationError, + DbForeignKeyViolationError, + DbNotFoundError, + + IoError, +} + +unsafe impl Send for ErrorKind {} + +#[derive(Debug, Clone)] +pub struct Error { + kind: ErrorKind, + message: String, +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}: {}", self.kind, self.message) + } +} + +impl std::error::Error for Error {} + +#[allow(dead_code)] +impl Error { + #[must_use] + pub fn new(kind: ErrorKind, message: &str) -> Self { + Self { + kind, + message: message.to_owned(), + } + } + + #[must_use] + pub const fn from_string(kind: ErrorKind, message: String) -> Self { + Self { kind, message } + } + + #[must_use] + pub const fn kind(&self) -> ErrorKind { + self.kind + } + + #[must_use] + pub fn message(&self) -> &str { + &self.message + } +} + +impl From for Error { + fn from(err: io::Error) -> Self { + Self::from_string(ErrorKind::IoError, err.to_string()) + } +} + +impl From for Error { + fn from(err: r2d2::Error) -> Self { + Self::from_string(ErrorKind::DbConnError, format!("r2d2 err: {err}")) + } +} + +impl From for Error { + fn from(err: diesel::result::Error) -> Self { + match &err { + diesel::result::Error::DatabaseError(kind, _info) => match kind { + DatabaseErrorKind::UniqueViolation => { + Self::from_string(ErrorKind::DbUniqueViolationError, err.to_string()) + } + DatabaseErrorKind::ForeignKeyViolation => { + Self::from_string(ErrorKind::DbForeignKeyViolationError, err.to_string()) + } + _ => Self::from_string(ErrorKind::DbGeneralError, err.to_string()), + }, + diesel::result::Error::NotFound => { + Self::from_string(ErrorKind::DbNotFoundError, err.to_string()) + } + _ => Self::from_string(ErrorKind::DbGeneralError, err.to_string()), + } + } +} + +impl From for Error { + fn from(err: std::num::ParseIntError) -> Self { + Self::from_string(ErrorKind::ConfigError, err.to_string()) + } +} + +impl From for Error { + fn from(err: std::ffi::OsString) -> Self { + Self::from_string( + ErrorKind::ConfigError, + format!("OsString to String err: {err:?}"), + ) + } +} + +impl From for Error { + fn from(err: dotenvy::Error) -> Self { + Self::from_string(ErrorKind::ConfigError, format!("dotenv err: {err:?}")) + } +} diff --git a/src/leetcode/0511.game-play-analysis-i/src/main.rs b/src/leetcode/0511.game-play-analysis-i/src/main.rs new file mode 100644 index 00000000..8ad13477 --- /dev/null +++ b/src/leetcode/0511.game-play-analysis-i/src/main.rs @@ -0,0 +1,25 @@ +// Copyright (c) 2024 Xu Shaohua . All rights reserved. +// Use of this source is governed by General Public License that can be found +// in the LICENSE file. + +use diesel::connection::SimpleConnection; +use std::env; +use std::fs; + +mod db; +mod error; + +use error::Error; + +fn main() -> Result<(), Error> { + dotenvy::dotenv()?; + env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); + + let pool = db::get_connection_pool()?; + let mut conn = pool.get()?; + for arg in env::args().skip(1) { + let sql_content: String = fs::read_to_string(arg)?; + conn.batch_execute(&sql_content)?; + } + Ok(()) +} diff --git a/src/leetcode/0511.game-play-analysis-i/src/schema.rs b/src/leetcode/0511.game-play-analysis-i/src/schema.rs new file mode 100644 index 00000000..5dabb71a --- /dev/null +++ b/src/leetcode/0511.game-play-analysis-i/src/schema.rs @@ -0,0 +1,10 @@ +// @generated automatically by Diesel CLI. + +diesel::table! { + activity (player_id, event_date) { + player_id -> Int4, + device_id -> Int4, + event_date -> Date, + games_played -> Int4, + } +} diff --git a/src/leetcode/template-pgsql/Cargo.toml b/src/leetcode/template-pgsql/Cargo.toml index 06d23952..b6ec28fd 100644 --- a/src/leetcode/template-pgsql/Cargo.toml +++ b/src/leetcode/template-pgsql/Cargo.toml @@ -5,3 +5,8 @@ edition = "2021" publish = false [dependencies] +diesel = { version = "2.2.0", default-features = false, features = [ "chrono", "postgres", "r2d2" ] } +dotenvy = "0.15.7" +env_logger = "0.11.3" +log = "0.4.21" +r2d2 = "0.8.10" diff --git a/src/leetcode/template-pgsql/src/db.rs b/src/leetcode/template-pgsql/src/db.rs new file mode 100644 index 00000000..c8d1e3be --- /dev/null +++ b/src/leetcode/template-pgsql/src/db.rs @@ -0,0 +1,37 @@ +// Copyright (c) 2024 Xu Shaohua . All rights reserved. +// Use of this source is governed by General Public License that can be found +// in the LICENSE file. + +use diesel::pg::PgConnection; +use diesel::r2d2::{ConnectionManager, Pool}; + +use crate::error::{Error, ErrorKind}; + +pub type DbPool = Pool>; + +/// Create postgres database connection pool. +/// +/// # Errors +/// +/// Returns error if: +/// - No `DATABASE_URL` is set in current environment. +/// - Failed to connect to database. +pub fn get_connection_pool() -> Result { + let url = std::env::var("DATABASE_URL").map_err(|err| { + Error::from_string( + ErrorKind::ConfigError, + format!("DATABASE_URL is not set in environment, err: {err:?}"), + ) + })?; + let manager = ConnectionManager::::new(&url); + + Pool::builder() + .test_on_check_out(true) + .build(manager) + .map_err(|err| { + Error::from_string( + ErrorKind::DbConnError, + format!("Failed to create connection pool, url: {url}, err: {err:?}"), + ) + }) +} diff --git a/src/leetcode/template-pgsql/src/error.rs b/src/leetcode/template-pgsql/src/error.rs new file mode 100644 index 00000000..c9baa518 --- /dev/null +++ b/src/leetcode/template-pgsql/src/error.rs @@ -0,0 +1,117 @@ +// Copyright (c) 2022 Xu Shaohua . All rights reserved. +// Use of this source is governed by GNU General Public License +// that can be found in the LICENSE file. + +#![allow(clippy::enum_variant_names)] + +use diesel::result::DatabaseErrorKind; +use std::fmt::{Display, Formatter}; +use std::io; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ErrorKind { + ConfigError, + + DbConnError, + DbGeneralError, + DbUniqueViolationError, + DbForeignKeyViolationError, + DbNotFoundError, + + IoError, +} + +unsafe impl Send for ErrorKind {} + +#[derive(Debug, Clone)] +pub struct Error { + kind: ErrorKind, + message: String, +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}: {}", self.kind, self.message) + } +} + +impl std::error::Error for Error {} + +#[allow(dead_code)] +impl Error { + #[must_use] + pub fn new(kind: ErrorKind, message: &str) -> Self { + Self { + kind, + message: message.to_owned(), + } + } + + #[must_use] + pub const fn from_string(kind: ErrorKind, message: String) -> Self { + Self { kind, message } + } + + #[must_use] + pub const fn kind(&self) -> ErrorKind { + self.kind + } + + #[must_use] + pub fn message(&self) -> &str { + &self.message + } +} + +impl From for Error { + fn from(err: io::Error) -> Self { + Self::from_string(ErrorKind::IoError, err.to_string()) + } +} + +impl From for Error { + fn from(err: r2d2::Error) -> Self { + Self::from_string(ErrorKind::DbConnError, format!("r2d2 err: {err}")) + } +} + +impl From for Error { + fn from(err: diesel::result::Error) -> Self { + match &err { + diesel::result::Error::DatabaseError(kind, _info) => match kind { + DatabaseErrorKind::UniqueViolation => { + Self::from_string(ErrorKind::DbUniqueViolationError, err.to_string()) + } + DatabaseErrorKind::ForeignKeyViolation => { + Self::from_string(ErrorKind::DbForeignKeyViolationError, err.to_string()) + } + _ => Self::from_string(ErrorKind::DbGeneralError, err.to_string()), + }, + diesel::result::Error::NotFound => { + Self::from_string(ErrorKind::DbNotFoundError, err.to_string()) + } + _ => Self::from_string(ErrorKind::DbGeneralError, err.to_string()), + } + } +} + +impl From for Error { + fn from(err: std::num::ParseIntError) -> Self { + Self::from_string(ErrorKind::ConfigError, err.to_string()) + } +} + +impl From for Error { + fn from(err: std::ffi::OsString) -> Self { + Self::from_string( + ErrorKind::ConfigError, + format!("OsString to String err: {err:?}"), + ) + } +} + +impl From for Error { + fn from(err: dotenvy::Error) -> Self { + Self::from_string(ErrorKind::ConfigError, format!("dotenv err: {err:?}")) + } +} diff --git a/src/leetcode/template-pgsql/src/main.rs b/src/leetcode/template-pgsql/src/main.rs index 23d089cc..8ad13477 100644 --- a/src/leetcode/template-pgsql/src/main.rs +++ b/src/leetcode/template-pgsql/src/main.rs @@ -2,4 +2,24 @@ // Use of this source is governed by General Public License that can be found // in the LICENSE file. -fn main() {} +use diesel::connection::SimpleConnection; +use std::env; +use std::fs; + +mod db; +mod error; + +use error::Error; + +fn main() -> Result<(), Error> { + dotenvy::dotenv()?; + env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); + + let pool = db::get_connection_pool()?; + let mut conn = pool.get()?; + for arg in env::args().skip(1) { + let sql_content: String = fs::read_to_string(arg)?; + conn.batch_execute(&sql_content)?; + } + Ok(()) +} diff --git a/src/leetcode/template-pgsql/src/schema.rs b/src/leetcode/template-pgsql/src/schema.rs new file mode 100644 index 00000000..5dabb71a --- /dev/null +++ b/src/leetcode/template-pgsql/src/schema.rs @@ -0,0 +1,10 @@ +// @generated automatically by Diesel CLI. + +diesel::table! { + activity (player_id, event_date) { + player_id -> Int4, + device_id -> Int4, + event_date -> Date, + games_played -> Int4, + } +}