diff --git a/Cargo.lock b/Cargo.lock index 03687b5..6234307 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,6 +27,18 @@ dependencies = [ "pq-sys", ] +[[package]] +name = "diesel-derive-enum" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81c5131a2895ef64741dad1d483f358c2a229a3a2d1b256778cdc5e146db64d4" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "diesel_derives" version = "2.1.4" @@ -54,6 +66,12 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "itoa" version = "1.0.11" @@ -102,6 +120,7 @@ name = "schnabu-server" version = "0.1.0" dependencies = [ "diesel", + "diesel-derive-enum", "dotenvy", "postgis_diesel", ] diff --git a/Cargo.toml b/Cargo.toml index 38ba933..15b43fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,5 +7,6 @@ edition = "2021" [dependencies] diesel = { version = "2.1.6", features = ["postgres"] } +diesel-derive-enum = { version = "2.1.0", features = ["postgres"] } dotenvy = "0.15.7" -postgis_diesel = "2.3.1" +postgis_diesel = "2.3.1" diff --git a/diesel.toml b/diesel.toml index 0ead658..6ffb907 100644 --- a/diesel.toml +++ b/diesel.toml @@ -5,6 +5,7 @@ file = "src/schema.rs" custom_type_derives = ["diesel::query_builder::QueryId"] import_types = ["diesel::sql_types::*", "postgis_diesel::sql_types::*"] +patch_file = "patches/schema.patch" [migrations_directory] dir = "migrations" diff --git a/docs/database-model.drawio b/docs/database-model.drawio index d425c1d..f8006f2 100644 --- a/docs/database-model.drawio +++ b/docs/database-model.drawio @@ -1,434 +1,440 @@ - + - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - - + + - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - + + - - - - - - - - - - - - - - + - + - + - + - + - + - + - + - + - + - + - + - + - - + + - + - - + + - + - - + + - - - - - + + + + + - - + + - + - + - + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + - + + + + + + + + + + + diff --git a/migrations/00000000000000_diesel_initial_setup/down.sql b/migrations/00000000000000_diesel_initial_setup/down.sql new file mode 100644 index 0000000..a9f5260 --- /dev/null +++ b/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/migrations/00000000000000_diesel_initial_setup/up.sql b/migrations/00000000000000_diesel_initial_setup/up.sql new file mode 100644 index 0000000..d68895b --- /dev/null +++ b/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/migrations/2024-05-09-173338_initial_db_setup/down.sql b/migrations/2024-05-09-173338_initial_db_setup/down.sql new file mode 100644 index 0000000..95b54ab --- /dev/null +++ b/migrations/2024-05-09-173338_initial_db_setup/down.sql @@ -0,0 +1,13 @@ +ALTER TABLE meal_images DROP CONSTRAINT image_id_constraint; +ALTER TABLE meal_images DROP CONSTRAINT meal_id_constraint; + +DROP TABLE meals; +DROP TABLE restaurant_ratings; +DROP TABLE restaurants; +DROP TABLE nodes; +DROP TABLE users; +DROP TABLE images; +DROP TABLE meal_images; + +DROP TYPE node_type; +DROP EXTENSION postgis CASCADE; diff --git a/migrations/2024-05-09-173338_initial_db_setup/up.sql b/migrations/2024-05-09-173338_initial_db_setup/up.sql new file mode 100644 index 0000000..21a2b89 --- /dev/null +++ b/migrations/2024-05-09-173338_initial_db_setup/up.sql @@ -0,0 +1,58 @@ +CREATE TYPE node_type AS ENUM ('marker', 'restaurant'); +CREATE EXTENSION postgis; + +CREATE TABLE nodes( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + type NODE_TYPE NOT NULL, + coordinates GEOMETRY(POINT,4326) NOT NULL +); + +CREATE TABLE restaurants( + id SERIAL PRIMARY KEY, + node_id INTEGER REFERENCES nodes(id) NOT NULL, + address VARCHAR(30) NOT NULL, + price INTEGER CHECK ( price >= 0 AND price <= 10 ) NOT NULL +); + +CREATE TABLE images( + id SERIAL PRIMARY KEY, + url VARCHAR(50) NOT NULL +); + +CREATE TABLE users( + id SERIAL PRIMARY KEY, + name VARCHAR(20) NOT NULL, + uuid UUID NOT NULL, + avatar_id INTEGER REFERENCES images(id), + + UNIQUE(uuid) +); + +CREATE TABLE restaurant_ratings( + id SERIAL PRIMARY KEY, + restaurant_id INTEGER REFERENCES restaurants(id) NOT NULL, + user_id INTEGER REFERENCES users(id) NOT NULL, + note VARCHAR(100) NOT NULL DEFAULT '' +); + +CREATE TABLE meals( + id SERIAL PRIMARY KEY, + restaurant_rating_id INTEGER REFERENCES restaurant_ratings(id) NOT NULL, + user_id INTEGER REFERENCES users(id) NOT NULL, + name VARCHAR(30) NOT NULL, + rating INTEGER CHECK ( rating >= 0 AND rating <= 10 ) NOT NULL, + price DECIMAL(12,2) CHECK (price >= 0) NOT NULL, + price_currency VARCHAR(1) CHECK (TRIM(price_currency) <> '') NOT NULL, + note VARCHAR(100) NOT NULL DEFAULT '', + date DATE NOT NULL +); + +CREATE TABLE meal_images( + image_id INTEGER NOT NULL, + meal_id INTEGER NOT NULL, + CONSTRAINT image_id_constraint FOREIGN KEY (image_id) REFERENCES images(id), + CONSTRAINT meal_id_constraint FOREIGN KEY (meal_id) REFERENCES meals(id), + PRIMARY KEY(image_id,meal_id) +); + diff --git a/patches/schema.patch b/patches/schema.patch new file mode 100644 index 0000000..838943e --- /dev/null +++ b/patches/schema.patch @@ -0,0 +1,16 @@ +--- src/full_schema.rs 2024-05-10 11:34:46.376360145 +0200 ++++ src/schema.rs 2024-05-10 11:34:24.009031704 +0200 +@@ -52,13 +52,12 @@ + } + + diesel::table! { + use diesel::sql_types::*; + use postgis_diesel::sql_types::*; + use super::sql_types::NodeType; +- use super::sql_types::Geometry; + + nodes (id) { + id -> Int4, + #[max_length = 100] + name -> Varchar, + #[sql_name = "type"] diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..b802d02 --- /dev/null +++ b/src/db.rs @@ -0,0 +1,12 @@ +use diesel::pg::PgConnection; +use diesel::prelude::*; +use dotenvy::dotenv; +use std::env; + +pub fn establish_connection() -> PgConnection { + dotenv().ok(); + + let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); + PgConnection::establish(&database_url) + .unwrap_or_else(|_| panic!("Error connecting to {}", database_url)) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..2e43661 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,9 @@ +#[macro_use] +extern crate diesel; + +mod db; +mod models; +mod schema; +use diesel::prelude::*; + +fn main() {} diff --git a/src/models.rs b/src/models.rs new file mode 100644 index 0000000..2efc642 --- /dev/null +++ b/src/models.rs @@ -0,0 +1,18 @@ +use crate::schema::nodes; +use diesel::prelude::*; +use postgis_diesel::types::Point; + +#[derive(diesel_derive_enum::DbEnum, Debug)] +#[ExistingTypePath = "crate::schema::sql_types::NodeType"] +pub enum NodeType { + Marker, + Restaurant, +} + +#[derive(Insertable)] +#[diesel(table_name = nodes)] +pub struct NewNode<'a> { + pub name: &'a str, + pub type_: NodeType, + pub coordinates: Point, +} diff --git a/src/schema.rs b/src/schema.rs new file mode 100644 index 0000000..75f8a68 --- /dev/null +++ b/src/schema.rs @@ -0,0 +1,142 @@ +// @generated automatically by Diesel CLI. + +pub mod sql_types { + #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "geometry"))] + pub struct Geometry; + + #[derive(diesel::query_builder::QueryId, diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "node_type"))] + pub struct NodeType; +} + +diesel::table! { + use diesel::sql_types::*; + use postgis_diesel::sql_types::*; + + images (id) { + id -> Int4, + #[max_length = 50] + url -> Varchar, + } +} + +diesel::table! { + use diesel::sql_types::*; + use postgis_diesel::sql_types::*; + + meal_images (image_id, meal_id) { + image_id -> Int4, + meal_id -> Int4, + } +} + +diesel::table! { + use diesel::sql_types::*; + use postgis_diesel::sql_types::*; + + meals (id) { + id -> Int4, + restaurant_rating_id -> Int4, + user_id -> Int4, + #[max_length = 30] + name -> Varchar, + rating -> Int4, + price -> Numeric, + #[max_length = 1] + price_currency -> Varchar, + #[max_length = 100] + note -> Varchar, + date -> Date, + } +} + +diesel::table! { + use diesel::sql_types::*; + use postgis_diesel::sql_types::*; + use super::sql_types::NodeType; + + nodes (id) { + id -> Int4, + #[max_length = 100] + name -> Varchar, + #[sql_name = "type"] + type_ -> NodeType, + coordinates -> Geometry, + } +} + +diesel::table! { + use diesel::sql_types::*; + use postgis_diesel::sql_types::*; + + restaurant_ratings (id) { + id -> Int4, + restaurant_id -> Int4, + user_id -> Int4, + #[max_length = 100] + note -> Varchar, + } +} + +diesel::table! { + use diesel::sql_types::*; + use postgis_diesel::sql_types::*; + + restaurants (id) { + id -> Int4, + node_id -> Int4, + #[max_length = 30] + address -> Varchar, + price -> Int4, + } +} + +diesel::table! { + use diesel::sql_types::*; + use postgis_diesel::sql_types::*; + + spatial_ref_sys (srid) { + srid -> Int4, + #[max_length = 256] + auth_name -> Nullable, + auth_srid -> Nullable, + #[max_length = 2048] + srtext -> Nullable, + #[max_length = 2048] + proj4text -> Nullable, + } +} + +diesel::table! { + use diesel::sql_types::*; + use postgis_diesel::sql_types::*; + + users (id) { + id -> Int4, + #[max_length = 20] + name -> Varchar, + uuid -> Uuid, + avatar_id -> Nullable, + } +} + +diesel::joinable!(meal_images -> images (image_id)); +diesel::joinable!(meal_images -> meals (meal_id)); +diesel::joinable!(meals -> restaurant_ratings (restaurant_rating_id)); +diesel::joinable!(meals -> users (user_id)); +diesel::joinable!(restaurant_ratings -> restaurants (restaurant_id)); +diesel::joinable!(restaurant_ratings -> users (user_id)); +diesel::joinable!(restaurants -> nodes (node_id)); +diesel::joinable!(users -> images (avatar_id)); + +diesel::allow_tables_to_appear_in_same_query!( + images, + meal_images, + meals, + nodes, + restaurant_ratings, + restaurants, + spatial_ref_sys, + users, +);