Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

an incomplete attempt at sqlite support #18

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[workspace]
members = ["ormx-macros", "ormx", "example-postgres", "example-mysql"]
members = ["ormx-macros", "ormx", "example-postgres", "example-mysql", "example-sqlite"]
1 change: 1 addition & 0 deletions example-sqlite/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DATABASE_URL=sqlite:/tmp/example.db
31 changes: 31 additions & 0 deletions example-sqlite/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[package]
name = "example-sqlite"
version = "0.1.0"
authors = ["moritz"]
edition = "2018"
build = "src/build.rs"

[dependencies]
ormx = { path = "../ormx", features = ["sqlite"] }
tokio = { version = "1.1", features = ["full"] }
anyhow = "1"
dotenv = "0.15"
chrono = "0.4"
simple_logger = "1"
log = "0.4"
futures = "0.3.17"

[dependencies.sqlx]
version = "0.5"
default-features = false
features = ["macros", "sqlite", "runtime-tokio-rustls", "chrono", "offline", "migrate"]

[build-dependencies]
anyhow = "1"
dotenv = "0.15"
tokio = { version = "1.1", features = ["full"] }

[build-dependencies.sqlx]
version = "0.5"
default-features = false
features = ["macros", "sqlite", "runtime-tokio-rustls", "chrono", "offline", "migrate"]
11 changes: 11 additions & 0 deletions example-sqlite/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
## Sqlite ORMX Example

This example shows ormx querying a sqlite database.

To try this example, run:
```bash
cd ormx/example-sqlite
sqlx database setup
cargo run
```
By default, the database file is `/tmp/example.db`. To change it, edit the `.env` file in this directory.
15 changes: 15 additions & 0 deletions example-sqlite/migrations/20211108025529_create.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
CREATE TABLE users
(
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
role TEXT CHECK( role in ('user','admin') ) NOT NULL DEFAULT 'user',
disabled TEXT,
last_login INTEGER NOT NULL DEFAULT 0
);

CREATE TABLE test (
id INTEGER PRIMARY KEY AUTOINCREMENT,
rowdata TEXT[] NOT NULL
);
26 changes: 26 additions & 0 deletions example-sqlite/src/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use anyhow::Context;
use sqlx::prelude::*;
use sqlx::sqlite::SqliteConnectOptions;
use std::env;
use std::str::FromStr;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
println!("cargo:rerun-if-changed=.env");
println!("cargo:rerun-if-changed=example-sqlite/migrations/20211108025529_create.sql");
dotenv::dotenv().ok();
let url = env::var("DATABASE_URL").context("DATABASE_URL environment variable must be set")?;

let mut conn = SqliteConnectOptions::from_str(&url)?
.create_if_missing(true)
.connect()
.await
.context(format!("unable to open database connection: {}", url))?;

sqlx::migrate!("./migrations/")
.run(&mut conn)
.await
.context("unable to run database migration")?;

Ok(())
}
127 changes: 127 additions & 0 deletions example-sqlite/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
use chrono::Utc;
use ormx::exports::futures::StreamExt;
use ormx::{Db, Delete, Insert, Table};
use sqlx::SqlitePool;

// mod query2;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
dotenv::dotenv().ok();
simple_logger::SimpleLogger::new()
.with_level(log::LevelFilter::Info)
.init()?;

let db = SqlitePool::connect(&dotenv::var("DATABASE_URL")?).await?;
let mut conn = db.acquire().await?;

log::info!("insert a new row into the database");
let mut new = InsertUser {
id: 1,
first_name: "Moritz".to_owned(),
last_name: "Bischof".to_owned(),
email: "moritz.bischof1@gmail.com".to_owned(),
disabled: None,
role: Role::User,
}
.insert(&mut conn)
.await?;

new.reload(&mut conn).await?;
log::info!("reloaded user: {:?}", new);
new.reload(&db).await?;

log::info!("update a single field");
new.set_last_login(&db, Utc::now().naive_utc().timestamp())
.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");
new.patch(
&db,
UpdateUser {
first_name: "NewFirstName".to_owned(),
last_name: "NewLastName".to_owned(),
disabled: Some("Reason".to_owned()),
role: Role::Admin,
},
)
.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);
let mut stream = User::stream_all(&db);
while let Some(x) = stream.next().await {
log::info!("{:?}", x);
}

new.reload(&db).await?;

log::info!("update a single field");
new.set_last_login(&db, Utc::now().naive_utc().timestamp())
.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");
new.patch(
&db,
UpdateUser {
first_name: "NewFirstName".to_owned(),
last_name: "NewLastName".to_owned(),
disabled: Some("Reason".to_owned()),
role: Role::Admin,
},
)
.await?;

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

Ok(())
}

#[derive(Debug, ormx::Table)]
#[ormx(table = "users", id = id, insertable, deletable)]
struct User {
#[ormx(column = "id")]
#[ormx(get_one = get_by_id)]
id: i64,
first_name: String,
last_name: String,
// generate `User::by_email(&str) -> Result<Option<Self>>`
#[ormx(get_optional(&str))]
email: String,
#[ormx(custom_type)]
role: Role,
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: i64,
}

// 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 UpdateUser {
first_name: String,
last_name: String,
disabled: Option<String>,
#[ormx(custom_type)]
role: Role,
}

#[derive(Debug, Copy, Clone, sqlx::Type)]
#[sqlx(type_name = "user_role")]
#[sqlx(rename_all = "lowercase")]
enum Role {
User,
Admin,
}
27 changes: 27 additions & 0 deletions example-sqlite/src/query2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use sqlx::SqlitePool;

use crate::User;

pub(crate) async fn query_users(
db: &SqlitePool,
filter: Option<&str>,
limit: Option<i64>,
) -> anyhow::Result<Vec<User>> {
let result = ormx::conditional_query_as!(
User,
r#"SELECT id AS user_id, first_name, last_name, email, disabled, role AS "role: _", 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)
}
)
.fetch_all(db)
.await?;

Ok(result)
}
2 changes: 1 addition & 1 deletion ormx-macros/src/attrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,4 +176,4 @@ impl syn::parse::Parse for AnyAttribute {
fn parse(input: ParseStream) -> Result<Self> {
input.call(Attribute::parse_outer).map(Self)
}
}
}
7 changes: 4 additions & 3 deletions ormx-macros/src/backend/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ pub fn get_optional(vis: &Visibility, ident: &Ident, by_ty: &Type, sql: &str) ->
db: impl sqlx::Executor<'_, Database = ormx::Db>,
by: #by_ty,
) -> sqlx::Result<Option<Self>> {
sqlx::query_as!(Self, #sql, by)
sqlx::query_as!(Self, #sql, by)
.fetch_optional(db)
.await
}
Expand All @@ -84,7 +84,7 @@ pub fn get_many(vis: &Visibility, ident: &Ident, by_ty: &Type, sql: &str) -> Tok
db: impl sqlx::Executor<'_, Database = ormx::Db>,
by: #by_ty,
) -> sqlx::Result<Vec<Self>> {
sqlx::query_as!(Self, #sql, by)
sqlx::query_as!(Self, #sql, by)
.fetch_all(db)
.await
}
Expand Down Expand Up @@ -123,7 +123,8 @@ pub fn setters<B: Backend>(table: &Table<B>) -> TokenStream {
db: impl sqlx::Executor<'_, Database = ormx::Db>,
value: #field_ty
) -> sqlx::Result<()> {
sqlx::query!(#sql, #value, <Self as ormx::Table>::id(self))
let id = <Self as ormx::Table>::id(self);
sqlx::query!(#sql, #value, id)
.execute(db)
.await?;
self.#field_ident = value;
Expand Down
27 changes: 23 additions & 4 deletions ormx-macros/src/backend/common/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ fn get<B: Backend>(table: &Table<B>, column_list: &str) -> TokenStream {
db: impl sqlx::Executor<'c, Database = ormx::Db> + 'a,
id: Self::Id,
) -> #box_future<'a, sqlx::Result<Self>> {
Box::pin(async move {
Box::pin(async move {
sqlx::query_as!(Self, #get_sql, id)
.fetch_one(db)
.await
Expand Down Expand Up @@ -95,7 +95,7 @@ fn update<B: Backend>(table: &Table<B>) -> TokenStream {
&'a self,
db: impl sqlx::Executor<'c, Database = ormx::Db> + 'a,
) -> #box_future<'a, sqlx::Result<()>> {
Box::pin(async move {
Box::pin(async move {
sqlx::query!(#update_sql, #( #other_arguments, )* self.#id_argument)
.execute(db)
.await?;
Expand All @@ -113,7 +113,7 @@ fn stream_all<B: Backend>(table: &Table<B>, column_list: &str) -> TokenStream {
fn stream_all<'a, 'c: 'a>(
db: impl sqlx::Executor<'c, Database = ormx::Db> + 'a,
) -> #box_stream<'a, sqlx::Result<Self>> {
sqlx::query_as!(Self, #all_sql)
sqlx::query_as!(Self, #all_sql)
.fetch(db)
}
}
Expand All @@ -130,6 +130,7 @@ fn stream_all_paginated<B: Backend>(table: &Table<B>, column_list: &str) -> Toke
bindings.next().unwrap()
);

#[cfg(not(feature = "sqlite"))]
quote! {
fn stream_all_paginated<'a, 'c: 'a>(
db: impl sqlx::Executor<'c, Database = ormx::Db> + 'a,
Expand All @@ -140,6 +141,24 @@ fn stream_all_paginated<B: Backend>(table: &Table<B>, column_list: &str) -> Toke
.fetch(db)
}
}
#[cfg(feature = "sqlite")]
quote! {
fn stream_all_paginated<'a>(
db: &'a sqlx::Pool<Db>,
offset: i64,
limit: i64,
) -> #box_stream<'a, sqlx::Result<Self>> {
Box::pin(
ormx::SelfRefStream::build(
(db.clone(), offset, limit),
move |(db, offset, limit)| {
sqlx::query_as!(Self, #all_sql, *limit, *offset)
.fetch(db)
},
)
)
}
}
}

fn delete<B: Backend>(table: &Table<B>) -> TokenStream {
Expand All @@ -163,7 +182,7 @@ fn delete<B: Backend>(table: &Table<B>) -> TokenStream {
db: impl sqlx::Executor<'c, Database = ormx::Db> + 'a,
id: #id_ty
) -> #box_future<'a, sqlx::Result<()>> {
use #result_import;
use #result_import;

Box::pin(async move {
let result = sqlx::query!(#delete_sql, id)
Expand Down
4 changes: 3 additions & 1 deletion ormx-macros/src/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ mod common;
mod mysql;
#[cfg(feature = "postgres")]
mod postgres;
#[cfg(feature = "sqlite")]
mod sqlite;

#[cfg(feature = "mysql")]
pub type Implementation = mysql::MySqlBackend;
#[cfg(feature = "postgres")]
pub type Implementation = postgres::PgBackend;
#[cfg(feature = "sqlite")]
compile_error!("sqlite is currently not supported");
pub type Implementation = sqlite::SqliteBackend;

pub trait Backend: Sized + Clone {
const QUOTE: char;
Expand Down
7 changes: 7 additions & 0 deletions ormx-macros/src/patch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,19 @@ pub struct PatchField {
impl PatchField {
pub fn fmt_as_argument(&self) -> TokenStream {
let ident = &self.ident;
#[cfg(not(feature = "sqlite"))]
let ty = &self.ty;

let mut out = quote!(#ident);
#[cfg(not(feature = "sqlite"))]
if self.custom_type {
out = quote!(#out as #ty);
}
#[cfg(feature = "sqlite")]
if self.custom_type {
out = quote!(#out);
}

if self.by_ref {
out = quote!(&(#out));
}
Expand Down
Loading