From 9c2b43ec3c14db5eef2a332f784d67fb85da9c40 Mon Sep 17 00:00:00 2001
From: Philippe Loctaux
Date: Mon, 27 Feb 2023 16:07:18 +0100
Subject: [PATCH] added database crate, "settings" with migrations and queries,
running migrations on web startup
---
crates/database/Cargo.toml | 11 +++
crates/database/build.rs | 5 ++
crates/database/justfile | 48 ++++++++++++
.../20230227133744_settings.down.sql | 1 +
.../migrations/20230227133744_settings.up.sql | 20 +++++
crates/database/queries/settings/get.sql | 9 +++
crates/database/queries/settings/init.sql | 2 +
.../queries/settings/set_business_logo.sql | 5 ++
.../queries/settings/set_business_name.sql | 5 ++
crates/database/readme.md | 59 +++++++++++++++
crates/database/sqlx-data.json | 75 +++++++++++++++++++
crates/database/src/error.rs | 45 +++++++++++
crates/database/src/lib.rs | 11 +++
crates/database/src/tables/mod.rs | 3 +
crates/database/src/tables/settings.rs | 57 ++++++++++++++
crates/database_pool/src/migrations.rs | 2 +-
16 files changed, 357 insertions(+), 1 deletion(-)
create mode 100644 crates/database/Cargo.toml
create mode 100644 crates/database/build.rs
create mode 100755 crates/database/justfile
create mode 100644 crates/database/migrations/20230227133744_settings.down.sql
create mode 100644 crates/database/migrations/20230227133744_settings.up.sql
create mode 100644 crates/database/queries/settings/get.sql
create mode 100644 crates/database/queries/settings/init.sql
create mode 100644 crates/database/queries/settings/set_business_logo.sql
create mode 100644 crates/database/queries/settings/set_business_name.sql
create mode 100644 crates/database/readme.md
create mode 100644 crates/database/sqlx-data.json
create mode 100644 crates/database/src/error.rs
create mode 100644 crates/database/src/lib.rs
create mode 100644 crates/database/src/tables/mod.rs
create mode 100644 crates/database/src/tables/settings.rs
diff --git a/crates/database/Cargo.toml b/crates/database/Cargo.toml
new file mode 100644
index 0000000..8f5dcfc
--- /dev/null
+++ b/crates/database/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "database"
+version = "0.0.0"
+edition = "2021"
+
+[dependencies]
+thiserror = { workspace = true }
+
+[dependencies.sqlx]
+workspace = true
+features = ["sqlite", "macros", "migrate", "chrono", "offline", "runtime-tokio-rustls"]
diff --git a/crates/database/build.rs b/crates/database/build.rs
new file mode 100644
index 0000000..7609593
--- /dev/null
+++ b/crates/database/build.rs
@@ -0,0 +1,5 @@
+// generated by `sqlx migrate build-script`
+fn main() {
+ // trigger recompilation when a new migration is added
+ println!("cargo:rerun-if-changed=migrations");
+}
\ No newline at end of file
diff --git a/crates/database/justfile b/crates/database/justfile
new file mode 100755
index 0000000..714fef6
--- /dev/null
+++ b/crates/database/justfile
@@ -0,0 +1,48 @@
+#!/usr/bin/env just --justfile
+
+database_dir := justfile_directory() + "/../../database"
+database_file := "ezidam.sqlite"
+database_path := database_dir / database_file
+
+database_url := "sqlite://" + absolute_path(database_path)
+sqlx_database := "--database-url " + database_url
+
+cargo := "cargo"
+
+# list recipes
+default:
+ just --list
+
+# prepare sql queries for offline usage
+offline:
+ {{cargo}} sqlx prepare {{sqlx_database}}
+
+# verify offline data is up to date
+offline_check:
+ {{cargo}} sqlx prepare --check {{sqlx_database}}
+
+# run pending migrations
+run:
+ sqlx migrate run {{sqlx_database}}
+
+# add new migration
+new name:
+ sqlx migrate add -r {{name}}
+
+# revert latest migration
+revert_last:
+ sqlx migrate revert {{sqlx_database}}
+
+# create database
+create:
+ mkdir -p {{database_dir}}
+ sqlx database create {{sqlx_database}}
+
+# reset database and apply migrations
+reset:
+ mkdir -p {{database_dir}}
+ sqlx database reset {{sqlx_database}}
+
+# create a build script to trigger recompilation when a new migration is added
+build_script:
+ sqlx migrate build-script
diff --git a/crates/database/migrations/20230227133744_settings.down.sql b/crates/database/migrations/20230227133744_settings.down.sql
new file mode 100644
index 0000000..54ca3d7
--- /dev/null
+++ b/crates/database/migrations/20230227133744_settings.down.sql
@@ -0,0 +1 @@
+drop table if exists settings;
diff --git a/crates/database/migrations/20230227133744_settings.up.sql b/crates/database/migrations/20230227133744_settings.up.sql
new file mode 100644
index 0000000..51a1531
--- /dev/null
+++ b/crates/database/migrations/20230227133744_settings.up.sql
@@ -0,0 +1,20 @@
+create table if not exists settings
+(
+ id INTEGER not null primary key check ( id = 0 ),
+ created_at TEXT not null default CURRENT_TIMESTAMP,
+ updated_at TEXT not null default CURRENT_TIMESTAMP,
+ business_name TEXT,
+ business_logo BLOB
+);
+
+-- update "updated_at"
+create trigger if not exists settings_updated_at
+ after
+ update
+ on settings
+ for each row
+begin
+ update settings
+ set updated_at = CURRENT_TIMESTAMP
+ where id is NEW.id;
+end;
diff --git a/crates/database/queries/settings/get.sql b/crates/database/queries/settings/get.sql
new file mode 100644
index 0000000..313b33b
--- /dev/null
+++ b/crates/database/queries/settings/get.sql
@@ -0,0 +1,9 @@
+select id,
+ created_at as "created_at: DateTime",
+ updated_at as "updated_at: DateTime",
+ business_name,
+ business_logo
+
+from settings
+
+where id is 0
diff --git a/crates/database/queries/settings/init.sql b/crates/database/queries/settings/init.sql
new file mode 100644
index 0000000..dd4764f
--- /dev/null
+++ b/crates/database/queries/settings/init.sql
@@ -0,0 +1,2 @@
+insert or ignore into settings(id)
+values (0);
\ No newline at end of file
diff --git a/crates/database/queries/settings/set_business_logo.sql b/crates/database/queries/settings/set_business_logo.sql
new file mode 100644
index 0000000..8238312
--- /dev/null
+++ b/crates/database/queries/settings/set_business_logo.sql
@@ -0,0 +1,5 @@
+update settings
+
+set business_logo = ?
+
+where id is 0
diff --git a/crates/database/queries/settings/set_business_name.sql b/crates/database/queries/settings/set_business_name.sql
new file mode 100644
index 0000000..66c4df3
--- /dev/null
+++ b/crates/database/queries/settings/set_business_name.sql
@@ -0,0 +1,5 @@
+update settings
+
+set business_name = ?
+
+where id is 0
diff --git a/crates/database/readme.md b/crates/database/readme.md
new file mode 100644
index 0000000..1e03a8d
--- /dev/null
+++ b/crates/database/readme.md
@@ -0,0 +1,59 @@
+# sql
+
+sql interactions for ezidam
+
+the purpose of this crate is **only** to interact with the database
+
+## initial setup
+
+- install https://github.com/casey/just (it's like `make` but simpler)
+- install https://github.com/launchbadge/sqlx/tree/main/sqlx-cli (it's the tool to interact with the database)
+- install sqlite (if it is not already installed)
+- run `just create` to create an empty database
+
+## tools
+
+- `just run` to run pending migrations (in case they are not already applied)
+- `just reset` to trash the database, create a new one and apply migrations
+
+## offline
+
+### save
+
+the command `just offline` is used to verify if all sql queries are valid.
+the result will be placed in `sqlx-data.json` and is used to compile the crate. if this command fails the crate **will
+not compile**.
+
+since this command will have be executed frequently, find a way to run this command when any files are modified inside
+this crate.
+
+- vscode: TODO
+- clion: https://www.jetbrains.com/help/clion/using-file-watchers.html
+
+### check
+
+the command `just offline_check` is here to verify if offline data is up-to-date. it will return a nonzero exit
+status if data is not up-to-date.
+
+it is mainly used in CI, but you can also use it to determinate if `just offline` is required to run.
+
+## migrations
+
+### new
+
+use `just new ` to create a new SQL migration. the parameter `` should indicate what the migration is doing.
+
+### fix mistakes
+
+if there is a mistake in the latest migration, but it has already been applied, use `just revert_last` to revert it.
+make your modifications, and run `just run` to apply it again.
+
+**note**: this works only for the most recent migration, if there is a mistake in an earlier migration, you will have to
+reset the whole database with `just reset`.
+
+consider making a new migration to fix the broken migration, and document it.
+
+## miscellaneous
+
+the command `just build_script` will create a rust build script `build.rs` to trigger recompilation when a new migration
+is added. this action can be done only at the beginning of the project.
diff --git a/crates/database/sqlx-data.json b/crates/database/sqlx-data.json
new file mode 100644
index 0000000..859442a
--- /dev/null
+++ b/crates/database/sqlx-data.json
@@ -0,0 +1,75 @@
+{
+ "db": "SQLite",
+ "06cfa74715f3725e99e63aa206f1be5d26cb26924d53dc5a68ee4ea48d6bbbfd": {
+ "describe": {
+ "columns": [],
+ "nullable": [],
+ "parameters": {
+ "Right": 1
+ }
+ },
+ "query": "update settings\n\nset business_logo = ?\n\nwhere id is 0\n"
+ },
+ "0b60c7829e95dde4145b7f207b64df7006c1fde2faaca0f7952a009d6cda90a3": {
+ "describe": {
+ "columns": [],
+ "nullable": [],
+ "parameters": {
+ "Right": 1
+ }
+ },
+ "query": "update settings\n\nset business_name = ?\n\nwhere id is 0\n"
+ },
+ "62c75412f673f6a293b0d188d79c50676ec21cf94e2e50e18f9279c91e6b85c8": {
+ "describe": {
+ "columns": [],
+ "nullable": [],
+ "parameters": {
+ "Right": 0
+ }
+ },
+ "query": "insert or ignore into settings(id)\nvalues (0);"
+ },
+ "cc69514c4d9457462e634eb58cbfc82b454197c5cb7f4a451954eb5a421afc3b": {
+ "describe": {
+ "columns": [
+ {
+ "name": "id",
+ "ordinal": 0,
+ "type_info": "Int64"
+ },
+ {
+ "name": "created_at: DateTime",
+ "ordinal": 1,
+ "type_info": "Text"
+ },
+ {
+ "name": "updated_at: DateTime",
+ "ordinal": 2,
+ "type_info": "Text"
+ },
+ {
+ "name": "business_name",
+ "ordinal": 3,
+ "type_info": "Text"
+ },
+ {
+ "name": "business_logo",
+ "ordinal": 4,
+ "type_info": "Blob"
+ }
+ ],
+ "nullable": [
+ false,
+ false,
+ false,
+ true,
+ true
+ ],
+ "parameters": {
+ "Right": 0
+ }
+ },
+ "query": "select id,\n created_at as \"created_at: DateTime\",\n updated_at as \"updated_at: DateTime\",\n business_name,\n business_logo\n\nfrom settings\n\nwhere id is 0\n"
+ }
+}
\ No newline at end of file
diff --git a/crates/database/src/error.rs b/crates/database/src/error.rs
new file mode 100644
index 0000000..b04070a
--- /dev/null
+++ b/crates/database/src/error.rs
@@ -0,0 +1,45 @@
+// error
+#[derive(thiserror::Error)]
+// the rest
+#[derive(Debug)]
+pub enum Error {
+ #[error("Unique constraint on primary key")]
+ UniqueConstraintPrimaryKey,
+
+ #[error("Unique constraint on key {0}")]
+ UniqueConstraint(String),
+
+ #[error("{0}")]
+ Database(#[from] sqlx::Error),
+}
+
+fn parse_unique_constraint(msg: &str) -> Option {
+ // Format will be "table.column"
+ let mut constraint = msg.strip_prefix("UNIQUE constraint failed: ")?.split('.');
+
+ // Extract "column" (which is the second value)
+ let _table = constraint.next()?;
+ let column = constraint.next()?;
+
+ Some(column.to_string())
+}
+
+pub fn handle_error(e: sqlx::Error) -> Error {
+ match e.as_database_error() {
+ Some(database_error) => match database_error.code() {
+ // List of all codes: https://www.sqlite.org/rescode.html
+ Some(code) => match code.as_ref() {
+ "1555" => Error::UniqueConstraintPrimaryKey,
+
+ // Attempt to extract unique constraint column
+ "2067" => match parse_unique_constraint(database_error.message()) {
+ Some(column) => Error::UniqueConstraint(column),
+ None => Error::Database(e),
+ },
+ _ => Error::Database(e),
+ },
+ None => Error::Database(e),
+ },
+ None => Error::Database(e),
+ }
+}
diff --git a/crates/database/src/lib.rs b/crates/database/src/lib.rs
new file mode 100644
index 0000000..9c5d078
--- /dev/null
+++ b/crates/database/src/lib.rs
@@ -0,0 +1,11 @@
+pub(crate) mod error;
+mod tables;
+
+/// Re-export sqlx
+pub use sqlx;
+
+/// Error
+pub use self::error::Error;
+
+/// Export tables
+pub use self::tables::*;
diff --git a/crates/database/src/tables/mod.rs b/crates/database/src/tables/mod.rs
new file mode 100644
index 0000000..a6fdf4c
--- /dev/null
+++ b/crates/database/src/tables/mod.rs
@@ -0,0 +1,3 @@
+mod settings;
+
+pub use settings::Settings;
diff --git a/crates/database/src/tables/settings.rs b/crates/database/src/tables/settings.rs
new file mode 100644
index 0000000..a7a5709
--- /dev/null
+++ b/crates/database/src/tables/settings.rs
@@ -0,0 +1,57 @@
+use crate::error::{handle_error, Error};
+use sqlx::sqlite::SqliteQueryResult;
+use sqlx::types::chrono::{DateTime, Utc};
+use sqlx::{FromRow, SqliteExecutor};
+
+#[derive(FromRow)]
+pub struct Settings {
+ pub id: i64,
+ pub created_at: DateTime,
+ pub updated_at: DateTime,
+ pub business_name: Option,
+ pub business_logo: Option>,
+}
+
+impl Settings {
+ pub async fn init(conn: impl SqliteExecutor<'_>) -> Result