Skip to content

Commit

Permalink
explicit (though not pretty) mysql support
Browse files Browse the repository at this point in the history
  • Loading branch information
NyxCode committed Sep 8, 2024
1 parent a088991 commit b41f8af
Show file tree
Hide file tree
Showing 17 changed files with 418 additions and 77 deletions.
37 changes: 36 additions & 1 deletion .github/workflows/examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,41 @@ on:
- '*-dev'

jobs:
mysql:
name: MySQL Example
runs-on: ubuntu-latest

services:
mysql:
image: mysql:latest
env:
MYSQL_ROOT_PASSWORD: password
ports:
- 3306:3306

steps:
- uses: actions/checkout@v4

- uses: Swatinem/rust-cache@v2
with:
key: mysql-example

- name: Install sqlx-cli
run: cargo install sqlx-cli

- name: Setup
working-directory: example-mysql
env:
DATABASE_URL: mysql://root:password@localhost:3306/ormx?ssl-mode=disabled
run: sqlx db setup

- name: Run
working-directory: example-mysql
env:
DATABASE_URL: mysql://root:password@localhost:3306/ormx?ssl-mode=disabled
run: cargo run


mariadb:
name: MariaDB Example
runs-on: ubuntu-latest
Expand All @@ -25,7 +60,7 @@ jobs:

- uses: Swatinem/rust-cache@v2
with:
key: mariadb-examples
key: mariadb-example

- name: Install sqlx-cli
run: cargo install sqlx-cli
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[workspace]
members = ["ormx-macros", "ormx", "example-postgres", "example-mariadb"]
members = ["ormx-macros", "ormx", "example-postgres", "example-mariadb", "example-mysql"]
resolver = "2"
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ version = "0.8"
default-features = false
features = ["macros", "mysql", "runtime-tokio-rustls"]
```
Right now, ormx supports mysql/mariadb and postgres.
Right now, ormx supports mysql, mariadb and postgres.

## what does it do?
ormx provides macros for generating commonly used sql queries at compile time.
ormx is meant to be used together with sqlx. Everything it generates uses `sqlx::query!` under the hood, so every generated query will be checked against your database at compile time.
Expand All @@ -57,8 +58,9 @@ is not what you are looking for.
if you encounter an issue or have questions, feel free to ask in [`#ormx` on the sqlx discord](https://discord.gg/mrZz4Wv8r2).
The documentation currently is not what it should be, so don't be afraid to ask for help.

## [mysql example](https://github.com/NyxCode/ormx/tree/master/example-mysql/src/main.rs)
## [postgres example](https://github.com/NyxCode/ormx/tree/master/example-postgres/src/main.rs)
## [mariadb example](https://github.com/NyxCode/ormx/tree/master/example-mariadb/src/main.rs)
## [mysql example](https://github.com/NyxCode/ormx/tree/master/example-mysql/src/main.rs)
## features
- `mysql` - enable support for mysql/mariadb
- `postgres` - enable support for postgres
Expand Down
4 changes: 2 additions & 2 deletions example-mariadb/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
[package]
name = "example-mysql"
name = "example-mariadb"
version = "0.1.0"
authors = ["moritz"]
edition = "2021"

[dependencies]
ormx = { path = "../ormx", features = ["mysql"] }
ormx = { path = "../ormx", features = ["mariadb"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1"
dotenv = "0.15"
Expand Down
19 changes: 19 additions & 0 deletions example-mysql/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "example-mysql"
version = "0.1.0"
authors = ["moritz"]
edition = "2021"

[dependencies]
ormx = { path = "../ormx", features = ["mysql"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1"
dotenv = "0.15"
chrono = "0.4"
env_logger = "0.11.5"
log = "0.4"

[dependencies.sqlx]
version = "0.8"
default-features = false
features = ["macros", "mysql", "runtime-tokio-rustls", "chrono"]
File renamed without changes.
91 changes: 91 additions & 0 deletions example-mysql/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
use chrono::{NaiveDateTime, Utc};
use log::LevelFilter;
use ormx::{Delete, Insert, Table};
use sqlx::MySqlPool;

// To run this example, first run `/scripts/mariadb.sh` to start mariadb in a docker container and
// write the database URL to `.env`. Then, source `.env` (`. .env`) and run `cargo run`

mod query2;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
dotenv::dotenv().ok();
env_logger::builder()
.filter_level(LevelFilter::Debug)
.init();

let db = MySqlPool::connect(&dotenv::var("DATABASE_URL")?).await?;

log::info!("insert a new row into the database");
let mut new = InsertUser {
first_name: "Moritz".to_owned(),
last_name: "Bischof".to_owned(),
email: "[email protected]".to_owned(),
disabled: None,
}
.insert(&mut *db.acquire().await?)
.await?;

log::info!("update a single field");
new.set_last_login(&db, Some(Utc::now().naive_utc()))
.await?;

log::info!("update all fields at once");
new.email = "asdf".to_owned();
new.update(&db).await?;

log::info!("apply a patch to the user, updating its first and last name");
new.patch(
&db,
UpdateName {
first_name: "NewFirstName".to_owned(),
last_name: "NewLastName".to_owned(),
disabled: Some("Reason".to_owned()),
},
)
.await?;

log::info!("reload the user, in case it has been modified");
new.reload(&db).await?;

log::info!("use the improved query macro for searching users");
let search_result = query2::query_users(&db, Some("NewFirstName"), None).await?;
println!("{:?}", search_result);

log::info!("delete the user from the database");
new.delete(&db).await?;

Ok(())
}

#[derive(Debug, ormx::Table)]
#[ormx(table = "users", id = user_id, insertable, deletable)]
struct User {
// map this field to the column "id"
#[ormx(column = "id")]
// generate `User::get_by_user_id(u32) -> Result<Self>`
#[ormx(get_one = get_by_user_id(u32))]
// this column is database-generated.
#[ormx(default)]
user_id: u32,
first_name: String,
last_name: String,
// generate `User::by_email(&str) -> Result<Option<Self>>`
#[ormx(get_optional(&str))]
email: String,
disabled: Option<String>,
// don't include this field into `InsertUser` since it has a default value
// generate `User::set_last_login(Option<NaiveDateTime>) -> Result<()>`
#[ormx(default, set)]
last_login: Option<NaiveDateTime>,
}

// Patches can be used to update multiple fields at once (in diesel, they're called "ChangeSets").
#[derive(ormx::Patch)]
#[ormx(table_name = "users", table = crate::User, id = "id")]
struct UpdateName {
first_name: String,
last_name: String,
disabled: Option<String>,
}
27 changes: 27 additions & 0 deletions example-mysql/src/query2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use sqlx::MySqlPool;

use crate::User;

pub(crate) async fn query_users(
db: &MySqlPool,
filter: Option<&str>,
limit: Option<usize>,
) -> anyhow::Result<Vec<User>> {
let result = ormx::conditional_query_as!(
User,
r#"SELECT id AS user_id, first_name, last_name, email, disabled, last_login"#
"FROM users"
Some(f) = filter => {
"WHERE first_name LIKE" ?(f)
"OR last_name LIKE" ?(f)
}
"ORDER BY first_name DESC"
Some(l) = limit => {
"LIMIT" ?(l as i64)
}
)
.fetch_all(db)
.await?;

Ok(result)
}
1 change: 1 addition & 0 deletions ormx-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ proc-macro = true
[features]
sqlite = []
mysql = []
mariadb = []
postgres = []

[dependencies]
Expand Down
84 changes: 84 additions & 0 deletions ormx-macros/src/backend/mariadb/insert.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use itertools::Itertools;
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::Ident;

use crate::{
backend::mariadb::{MariaBackend, MariaBindings},
table::{Table, TableField},
};

pub fn impl_insert(table: &Table<MariaBackend>) -> TokenStream {
let insert_ident = match &table.insertable {
Some(i) => &i.ident,
None => return quote!(),
};

let insert_fields: Vec<&TableField<MariaBackend>> = table.insertable_fields().collect();
let default_fields: Vec<&TableField<MariaBackend>> = table.default_fields().collect();

let table_ident = &table.ident;
let insert_field_idents = insert_fields.iter().map(|field| &field.field);
let default_field_idents = default_fields.iter().map(|field| &field.field);
let default_field_ordinals = (0usize..).take(default_fields.len());

let insert_sql = insert_sql(table, &insert_fields);

let insert_field_exprs = insert_fields.iter().map(|f| f.fmt_as_argument());

let fetch_fn = if default_fields.is_empty() {
Ident::new("execute", Span::call_site())
} else {
Ident::new("fetch_one", Span::call_site())
};

quote! {
impl ormx::Insert for #insert_ident {
type Table = #table_ident;

async fn insert<'a, 'c: 'a>(
self,
db: impl sqlx::Executor<'c, Database = ormx::Db> + 'a,
) -> sqlx::Result<Self::Table> {
use sqlx::Row;

let _generated = sqlx::query!(#insert_sql, #( #insert_field_exprs, )*)
.#fetch_fn(db)
.await?;

Ok(Self::Table {
#( #insert_field_idents: self.#insert_field_idents, )*
#( #default_field_idents: _generated.get(#default_field_ordinals), )*
})
}
}
}
}

fn insert_sql(table: &Table<MariaBackend>, insert_fields: &[&TableField<MariaBackend>]) -> String {
let columns = insert_fields.iter().map(|field| field.column()).join(", ");
let fields = MariaBindings::default()
.take(insert_fields.len())
.join(", ");
let returning_fields = table
.default_fields()
.map(TableField::fmt_for_select)
.join(", ");

if returning_fields.is_empty() {
format!(
"INSERT INTO {} ({}) VALUES ({})",
table.name(),
columns,
fields
)
} else {
format!(
"INSERT INTO {} ({}) VALUES ({}) RETURNING {}",
table.name(),
columns,
fields,
returning_fields
)
}
}
37 changes: 37 additions & 0 deletions ormx-macros/src/backend/mariadb/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use std::borrow::Cow;

use proc_macro2::TokenStream;
use quote::quote;

use crate::{backend::Backend, table::Table};

mod insert;

#[derive(Clone)]
pub struct MariaBackend;

impl Backend for MariaBackend {
const QUOTE: char = '`';
const IS_MYSQL: bool = true;

type Bindings = MariaBindings;

fn query_result() -> TokenStream {
quote!(sqlx::mysql::MySqlQueryResult)
}

fn impl_insert(table: &Table<Self>) -> TokenStream {
insert::impl_insert(table)
}
}

#[derive(Default)]
pub struct MariaBindings;

impl Iterator for MariaBindings {
type Item = Cow<'static, str>;

fn next(&mut self) -> Option<Self::Item> {
Some(Cow::Borrowed("?"))
}
}
4 changes: 4 additions & 0 deletions ormx-macros/src/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@ use proc_macro2::TokenStream;
use crate::{patch::Patch, table::Table};

mod common;
#[cfg(feature = "mariadb")]
mod mariadb;
#[cfg(feature = "mysql")]
mod mysql;
#[cfg(feature = "postgres")]
mod postgres;

#[cfg(feature = "mariadb")]
pub type Implementation = mariadb::MariaBackend;
#[cfg(feature = "mysql")]
pub type Implementation = mysql::MySqlBackend;
#[cfg(feature = "postgres")]
Expand Down
Loading

0 comments on commit b41f8af

Please sign in to comment.