From 7cf2aa98f38bd94463d21eb79b5fe96fcaaca08e Mon Sep 17 00:00:00 2001 From: Michael de Silva Date: Sat, 2 Nov 2024 20:22:41 +0530 Subject: [PATCH] Add example to use rustqlite to persist the example into a local file This includes very simple error logging, prompting the user to replace the DB file if it exists on concecutive runs. --- .gitignore | 1 + Cargo.toml | 1 + examples/hello-toasty-rustqlite/Cargo.toml | 11 + examples/hello-toasty-rustqlite/schema.toasty | 28 + examples/hello-toasty-rustqlite/src/db/mod.rs | 7 + .../hello-toasty-rustqlite/src/db/todo.rs | 552 ++++++++++++++ .../hello-toasty-rustqlite/src/db/user.rs | 715 ++++++++++++++++++ examples/hello-toasty-rustqlite/src/main.rs | 157 ++++ 8 files changed, 1472 insertions(+) create mode 100644 examples/hello-toasty-rustqlite/Cargo.toml create mode 100644 examples/hello-toasty-rustqlite/schema.toasty create mode 100644 examples/hello-toasty-rustqlite/src/db/mod.rs create mode 100644 examples/hello-toasty-rustqlite/src/db/todo.rs create mode 100644 examples/hello-toasty-rustqlite/src/db/user.rs create mode 100644 examples/hello-toasty-rustqlite/src/main.rs diff --git a/.gitignore b/.gitignore index 4fffb2f..ad6514c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target /Cargo.lock +/examples/hello-toasty-rustqlite/*.sql diff --git a/Cargo.toml b/Cargo.toml index 49e33c7..fd8b21b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ members = [ # Examples "examples/composite-key", "examples/hello-toasty", + "examples/hello-toasty-rustqlite", "examples/cratehub", "examples/user-has-one-profile", diff --git a/examples/hello-toasty-rustqlite/Cargo.toml b/examples/hello-toasty-rustqlite/Cargo.toml new file mode 100644 index 0000000..6b7282e --- /dev/null +++ b/examples/hello-toasty-rustqlite/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "example-hello-toasty-rustqlite" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +toasty = { path = "../../src/toasty" } +toasty-sqlite = { path = "../../src/db/sqlite" } + +tokio = { version = "1.18", features = ["full"] } diff --git a/examples/hello-toasty-rustqlite/schema.toasty b/examples/hello-toasty-rustqlite/schema.toasty new file mode 100644 index 0000000..56ba783 --- /dev/null +++ b/examples/hello-toasty-rustqlite/schema.toasty @@ -0,0 +1,28 @@ +model User { + #[key] + #[auto] + id: Id, + + name: String, + + #[unique] + email: String, + + todos: [Todo], + + moto: Option, +} + +model Todo { + #[key] + #[auto] + id: Id, + + #[index] + user_id: Id, + + #[relation(key = user_id, references = id)] + user: User, + + title: String, +} diff --git a/examples/hello-toasty-rustqlite/src/db/mod.rs b/examples/hello-toasty-rustqlite/src/db/mod.rs new file mode 100644 index 0000000..07411b0 --- /dev/null +++ b/examples/hello-toasty-rustqlite/src/db/mod.rs @@ -0,0 +1,7 @@ +#![allow(non_upper_case_globals, dead_code, warnings)] + +mod user; +pub use user::User; + +mod todo; +pub use todo::Todo; diff --git a/examples/hello-toasty-rustqlite/src/db/todo.rs b/examples/hello-toasty-rustqlite/src/db/todo.rs new file mode 100644 index 0000000..d823341 --- /dev/null +++ b/examples/hello-toasty-rustqlite/src/db/todo.rs @@ -0,0 +1,552 @@ +use toasty::codegen_support::*; +#[derive(Debug)] +pub struct Todo { + pub id: Id, + pub user_id: Id, + user: BelongsTo, + pub title: String, +} +impl Todo { + pub const ID: Path> = Path::from_field_index::(0); + pub const USER_ID: Path> = Path::from_field_index::(1); + pub const USER: self::fields::User = + self::fields::User::from_path(Path::from_field_index::(2)); + pub const TITLE: Path = Path::from_field_index::(3); + pub fn create<'a>() -> CreateTodo<'a> { + CreateTodo::default() + } + pub fn create_many<'a>() -> CreateMany<'a, Todo> { + CreateMany::default() + } + pub fn filter<'a>(expr: stmt::Expr<'a, bool>) -> Query<'a> { + Query::from_stmt(stmt::Select::from_expr(expr)) + } + pub fn update<'a>(&'a mut self) -> UpdateTodo<'a> { + UpdateTodo { + model: self, + query: UpdateQuery { + stmt: stmt::Update::default(), + }, + } + } + pub async fn delete(self, db: &Db) -> Result<()> { + let stmt = self.into_select().delete(); + db.exec(stmt).await?; + Ok(()) + } +} +impl Model for Todo { + const ID: ModelId = ModelId(1); + const FIELD_COUNT: usize = 4; + type Key = Id; + fn load(mut record: Record<'_>) -> Result { + Ok(Todo { + id: Id::from_untyped(record[0].take().to_id()?), + user_id: Id::from_untyped(record[1].take().to_id()?), + user: BelongsTo::load(record[2].take())?, + title: record[3].take().to_string()?, + }) + } +} +impl<'a> stmt::IntoSelect<'a> for &'a Todo { + type Model = Todo; + fn into_select(self) -> stmt::Select<'a, Self::Model> { + Todo::find_by_id(&self.id).into_select() + } +} +impl stmt::AsSelect for Todo { + type Model = Todo; + fn as_select(&self) -> stmt::Select<'_, Self::Model> { + Todo::find_by_id(&self.id).into_select() + } +} +impl stmt::IntoSelect<'static> for Todo { + type Model = Todo; + fn into_select(self) -> stmt::Select<'static, Self::Model> { + Todo::find_by_id(self.id).into_select() + } +} +impl<'a> stmt::IntoExpr<'a, Todo> for &'a Todo { + fn into_expr(self) -> stmt::Expr<'a, Todo> { + stmt::Key::from_expr(&self.id).into() + } +} +impl<'a> stmt::IntoExpr<'a, [Todo]> for &'a Todo { + fn into_expr(self) -> stmt::Expr<'a, [Todo]> { + stmt::Key::from_expr(&self.id).into() + } +} +#[derive(Debug)] +pub struct Query<'a> { + stmt: stmt::Select<'a, Todo>, +} +impl<'a> Query<'a> { + pub const fn from_stmt(stmt: stmt::Select<'a, Todo>) -> Query<'a> { + Query { stmt } + } + pub async fn all(self, db: &'a Db) -> Result> { + db.all(self.stmt).await + } + pub async fn first(self, db: &Db) -> Result> { + db.first(self.stmt).await + } + pub async fn get(self, db: &Db) -> Result { + db.get(self.stmt).await + } + pub fn update(self) -> UpdateQuery<'a> { + UpdateQuery::from(self) + } + pub async fn delete(self, db: &Db) -> Result<()> { + db.exec(self.stmt.delete()).await?; + Ok(()) + } + pub async fn collect(self, db: &'a Db) -> Result + where + A: FromCursor, + { + self.all(db).await?.collect().await + } + pub fn filter(self, expr: stmt::Expr<'a, bool>) -> Query<'a> { + Query { + stmt: self.stmt.and(expr), + } + } + pub fn user(mut self) -> super::user::Query<'a> { + todo!() + } +} +impl<'a> stmt::IntoSelect<'a> for Query<'a> { + type Model = Todo; + fn into_select(self) -> stmt::Select<'a, Todo> { + self.stmt + } +} +impl<'a> stmt::IntoSelect<'a> for &Query<'a> { + type Model = Todo; + fn into_select(self) -> stmt::Select<'a, Todo> { + self.stmt.clone() + } +} +impl Default for Query<'static> { + fn default() -> Query<'static> { + Query { + stmt: stmt::Select::all(), + } + } +} +#[derive(Debug)] +pub struct CreateTodo<'a> { + pub(super) stmt: stmt::Insert<'a, Todo>, +} +impl<'a> CreateTodo<'a> { + pub fn id(mut self, id: impl Into>) -> Self { + self.stmt.set_value(0, id.into()); + self + } + pub fn user_id(mut self, user_id: impl Into>) -> Self { + self.stmt.set_value(1, user_id.into()); + self + } + pub fn user<'b>(mut self, user: impl IntoExpr<'a, self::relation::User<'b>>) -> Self { + self.stmt.set_expr(2, user.into_expr()); + self + } + pub fn title(mut self, title: impl Into) -> Self { + self.stmt.set_value(3, title.into()); + self + } + pub async fn exec(self, db: &'a Db) -> Result { + db.exec_insert_one::(self.stmt).await + } +} +impl<'a> IntoInsert<'a> for CreateTodo<'a> { + type Model = Todo; + fn into_insert(self) -> stmt::Insert<'a, Todo> { + self.stmt + } +} +impl<'a> IntoExpr<'a, Todo> for CreateTodo<'a> { + fn into_expr(self) -> stmt::Expr<'a, Todo> { + self.stmt.into() + } +} +impl<'a> IntoExpr<'a, [Todo]> for CreateTodo<'a> { + fn into_expr(self) -> stmt::Expr<'a, [Todo]> { + self.stmt.into_list_expr() + } +} +impl<'a> Default for CreateTodo<'a> { + fn default() -> CreateTodo<'a> { + CreateTodo { + stmt: stmt::Insert::blank(), + } + } +} +#[derive(Debug)] +pub struct UpdateTodo<'a> { + model: &'a mut Todo, + query: UpdateQuery<'a>, +} +#[derive(Debug)] +pub struct UpdateQuery<'a> { + stmt: stmt::Update<'a, Todo>, +} +impl<'a> UpdateTodo<'a> { + pub fn id(mut self, id: impl Into>) -> Self { + self.query.set_id(id); + self + } + pub fn user_id(mut self, user_id: impl Into>) -> Self { + self.query.set_user_id(user_id); + self + } + pub fn user<'b>(mut self, user: impl IntoExpr<'a, self::relation::User<'b>>) -> Self { + self.query.set_user(user); + self + } + pub fn title(mut self, title: impl Into) -> Self { + self.query.set_title(title); + self + } + pub async fn exec(self, db: &Db) -> Result<()> { + let fields; + let mut into_iter; + { + let mut stmt = self.query.stmt; + fields = stmt.fields().clone(); + stmt.set_selection(&*self.model); + let mut records = db.exec::(stmt.into()).await?; + into_iter = records + .next() + .await + .unwrap()? + .into_record() + .into_owned() + .into_iter(); + } + for field in fields.iter() { + match field.into_usize() { + 0 => self.model.id = stmt::Id::from_untyped(into_iter.next().unwrap().to_id()?), + 1 => { + self.model.user_id = stmt::Id::from_untyped(into_iter.next().unwrap().to_id()?) + } + 2 => { + self.model.user_id = stmt::Id::from_untyped(into_iter.next().unwrap().to_id()?) + } + 3 => self.model.title = into_iter.next().unwrap().to_string()?, + _ => todo!("handle unknown field id in reload after update"), + } + } + Ok(()) + } +} +impl<'a> UpdateQuery<'a> { + pub fn id(mut self, id: impl Into>) -> Self { + self.set_id(id); + self + } + pub fn set_id(&mut self, id: impl Into>) -> &mut Self { + self.stmt.set_expr(0, id.into()); + self + } + pub fn user_id(mut self, user_id: impl Into>) -> Self { + self.set_user_id(user_id); + self + } + pub fn set_user_id(&mut self, user_id: impl Into>) -> &mut Self { + self.stmt.set_expr(1, user_id.into()); + self + } + pub fn user<'b>(mut self, user: impl IntoExpr<'a, self::relation::User<'b>>) -> Self { + self.set_user(user); + self + } + pub fn set_user<'b>(&mut self, user: impl IntoExpr<'a, self::relation::User<'b>>) -> &mut Self { + self.stmt.set_expr(2, user.into_expr()); + self + } + pub fn title(mut self, title: impl Into) -> Self { + self.set_title(title); + self + } + pub fn set_title(&mut self, title: impl Into) -> &mut Self { + self.stmt.set_expr(3, title.into()); + self + } + pub async fn exec(self, db: &Db) -> Result<()> { + let stmt = self.stmt; + let mut cursor = db.exec(stmt.into()).await?; + Ok(()) + } +} +impl<'a> From> for UpdateQuery<'a> { + fn from(value: Query<'a>) -> UpdateQuery<'a> { + UpdateQuery { + stmt: stmt::Update::new(value), + } + } +} +impl<'a> From> for UpdateQuery<'a> { + fn from(src: stmt::Select<'a, Todo>) -> UpdateQuery<'a> { + UpdateQuery { + stmt: stmt::Update::new(src), + } + } +} +pub mod fields { + use super::*; + pub struct User { + pub(super) path: Path, + } + impl User { + pub const fn from_path(path: Path) -> User { + User { path } + } + pub fn id(mut self) -> Path> { + self.path.chain(super::super::user::User::ID) + } + pub fn name(mut self) -> Path { + self.path.chain(super::super::user::User::NAME) + } + pub fn email(mut self) -> Path { + self.path.chain(super::super::user::User::EMAIL) + } + pub fn todos(mut self) -> super::super::user::fields::Todos { + let path = self.path.chain(super::super::user::User::TODOS); + super::super::user::fields::Todos::from_path(path) + } + pub fn moto(mut self) -> Path { + self.path.chain(super::super::user::User::MOTO) + } + pub fn eq<'a, 'b, T>(self, rhs: T) -> stmt::Expr<'a, bool> + where + T: toasty::stmt::IntoExpr<'a, super::relation::user::User<'b>>, + { + self.path.eq(rhs.into_expr().cast()) + } + pub fn in_query<'a, Q>(self, rhs: Q) -> toasty::stmt::Expr<'a, bool> + where + Q: stmt::IntoSelect<'a, Model = super::super::user::User>, + { + self.path.in_query(rhs) + } + } + impl From for Path { + fn from(val: User) -> Path { + val.path + } + } + impl<'stmt> stmt::IntoExpr<'stmt, super::relation::user::User<'stmt>> for User { + fn into_expr(self) -> stmt::Expr<'stmt, super::relation::user::User<'stmt>> { + todo!("into_expr for {} (field path struct)", stringify!(User)); + } + } +} +pub mod relation { + use super::*; + use toasty::Cursor; + pub mod user { + use super::*; + #[derive(Debug)] + pub struct User<'a> { + scope: &'a Todo, + } + impl super::Todo { + pub fn user(&self) -> User<'_> { + User { scope: self } + } + } + impl<'a> User<'a> { + pub fn get(&self) -> &super::super::super::user::User { + self.scope.user.get() + } + } + impl<'a> stmt::IntoSelect<'a> for &'a User<'_> { + type Model = super::super::super::user::User; + fn into_select(self) -> stmt::Select<'a, Self::Model> { + super::super::super::user::User::find_by_id(&self.scope.user_id).into_select() + } + } + impl<'stmt, 'a> stmt::IntoExpr<'stmt, User<'a>> for User<'a> { + fn into_expr(self) -> stmt::Expr<'stmt, User<'a>> { + todo!( + "stmt::IntoExpr for {} (belongs_to Fk struct) - self = {:#?}", + stringify!(User), + self + ); + } + } + impl<'stmt, 'a> stmt::IntoExpr<'stmt, User<'a>> for &'stmt User<'a> { + fn into_expr(self) -> stmt::Expr<'stmt, User<'a>> { + todo!( + "stmt::IntoExpr for &'a {} (belongs_to Fk struct) - self = {:#?}", + stringify!(User), + self + ); + } + } + impl<'stmt, 'a> stmt::IntoExpr<'stmt, User<'a>> for &'stmt super::super::super::user::User { + fn into_expr(self) -> stmt::Expr<'stmt, User<'a>> { + stmt::Expr::from_untyped(&self.id) + } + } + impl<'stmt, 'a> stmt::IntoExpr<'stmt, User<'a>> for super::super::super::user::CreateUser<'stmt> { + fn into_expr(self) -> stmt::Expr<'stmt, User<'a>> { + let expr: stmt::Expr<'stmt, super::super::super::user::User> = self.stmt.into(); + expr.cast() + } + } + impl<'a> User<'a> { + pub async fn find(&self, db: &Db) -> Result { + db.get(self.into_select()).await + } + } + } + pub use user::User; +} +pub mod queries { + use super::*; + impl super::Todo { + pub fn find_by_id<'a>(id: impl stmt::IntoExpr<'a, Id>) -> FindById<'a> { + FindById { + query: Query::from_stmt(stmt::Select::from_expr(Todo::ID.eq(id))), + } + } + } + pub struct FindById<'a> { + query: Query<'a>, + } + impl<'a> FindById<'a> { + pub async fn all(self, db: &'a Db) -> Result> { + self.query.all(db).await + } + pub async fn first(self, db: &Db) -> Result> { + self.query.first(db).await + } + pub async fn get(self, db: &Db) -> Result { + self.query.get(db).await + } + pub fn update(self) -> super::UpdateQuery<'a> { + super::UpdateQuery::from(self.query) + } + pub async fn delete(self, db: &Db) -> Result<()> { + self.query.delete(db).await + } + pub fn include(mut self, path: impl Into>) -> FindById<'a> { + let path = path.into(); + self.query.stmt.include(path); + self + } + pub fn filter(self, filter: stmt::Expr<'a, bool>) -> Query<'a> { + let stmt = self.into_select(); + Query::from_stmt(stmt.and(filter)) + } + pub async fn collect(self, db: &'a Db) -> Result + where + A: FromCursor, + { + self.all(db).await?.collect().await + } + } + impl<'a> stmt::IntoSelect<'a> for FindById<'a> { + type Model = super::Todo; + fn into_select(self) -> stmt::Select<'a, Self::Model> { + self.query.into_select() + } + } + impl super::Todo { + pub fn find_many_by_id<'a>() -> FindManyById<'a> { + FindManyById { items: vec![] } + } + } + pub struct FindManyById<'a> { + items: Vec>>, + } + impl<'a> FindManyById<'a> { + pub fn item(mut self, id: impl stmt::IntoExpr<'a, Id>) -> Self { + self.items.push(id.into_expr()); + self + } + pub async fn all(self, db: &'a Db) -> Result> { + db.all(self.into_select()).await + } + pub async fn first(self, db: &Db) -> Result> { + db.first(self.into_select()).await + } + pub async fn get(self, db: &Db) -> Result { + db.get(self.into_select()).await + } + pub fn update(self) -> super::UpdateQuery<'a> { + super::UpdateQuery::from(self.into_select()) + } + pub async fn delete(self, db: &Db) -> Result<()> { + db.delete(self.into_select()).await + } + pub fn filter(self, filter: stmt::Expr<'a, bool>) -> Query<'a> { + let stmt = self.into_select(); + Query::from_stmt(stmt.and(filter)) + } + pub async fn collect(self, db: &'a Db) -> Result + where + A: FromCursor, + { + self.all(db).await?.collect().await + } + } + impl<'a> stmt::IntoSelect<'a> for FindManyById<'a> { + type Model = super::Todo; + fn into_select(self) -> stmt::Select<'a, Self::Model> { + stmt::Select::from_expr(stmt::in_set(Todo::ID, self.items)) + } + } + impl super::Todo { + pub fn find_by_user_id<'a>( + user_id: impl stmt::IntoExpr<'a, Id>, + ) -> FindByUserId<'a> { + FindByUserId { + query: Query::from_stmt(stmt::Select::from_expr(Todo::USER_ID.eq(user_id))), + } + } + } + pub struct FindByUserId<'a> { + query: Query<'a>, + } + impl<'a> FindByUserId<'a> { + pub async fn all(self, db: &'a Db) -> Result> { + self.query.all(db).await + } + pub async fn first(self, db: &Db) -> Result> { + self.query.first(db).await + } + pub async fn get(self, db: &Db) -> Result { + self.query.get(db).await + } + pub fn update(self) -> super::UpdateQuery<'a> { + super::UpdateQuery::from(self.query) + } + pub async fn delete(self, db: &Db) -> Result<()> { + self.query.delete(db).await + } + pub fn include(mut self, path: impl Into>) -> FindByUserId<'a> { + let path = path.into(); + self.query.stmt.include(path); + self + } + pub fn filter(self, filter: stmt::Expr<'a, bool>) -> Query<'a> { + let stmt = self.into_select(); + Query::from_stmt(stmt.and(filter)) + } + pub async fn collect(self, db: &'a Db) -> Result + where + A: FromCursor, + { + self.all(db).await?.collect().await + } + } + impl<'a> stmt::IntoSelect<'a> for FindByUserId<'a> { + type Model = super::Todo; + fn into_select(self) -> stmt::Select<'a, Self::Model> { + self.query.into_select() + } + } +} diff --git a/examples/hello-toasty-rustqlite/src/db/user.rs b/examples/hello-toasty-rustqlite/src/db/user.rs new file mode 100644 index 0000000..f85b994 --- /dev/null +++ b/examples/hello-toasty-rustqlite/src/db/user.rs @@ -0,0 +1,715 @@ +use toasty::codegen_support::*; +#[derive(Debug)] +pub struct User { + pub id: Id, + pub name: String, + pub email: String, + todos: HasMany, + pub moto: Option, +} +impl User { + pub const ID: Path> = Path::from_field_index::(0); + pub const NAME: Path = Path::from_field_index::(1); + pub const EMAIL: Path = Path::from_field_index::(2); + pub const TODOS: self::fields::Todos = + self::fields::Todos::from_path(Path::from_field_index::(3)); + pub const MOTO: Path = Path::from_field_index::(4); + pub fn create<'a>() -> CreateUser<'a> { + CreateUser::default() + } + pub fn create_many<'a>() -> CreateMany<'a, User> { + CreateMany::default() + } + pub fn filter<'a>(expr: stmt::Expr<'a, bool>) -> Query<'a> { + Query::from_stmt(stmt::Select::from_expr(expr)) + } + pub fn update<'a>(&'a mut self) -> UpdateUser<'a> { + UpdateUser { + model: self, + query: UpdateQuery { + stmt: stmt::Update::default(), + }, + } + } + pub async fn delete(self, db: &Db) -> Result<()> { + let stmt = self.into_select().delete(); + db.exec(stmt).await?; + Ok(()) + } +} +impl Model for User { + const ID: ModelId = ModelId(0); + const FIELD_COUNT: usize = 5; + type Key = Id; + fn load(mut record: Record<'_>) -> Result { + Ok(User { + id: Id::from_untyped(record[0].take().to_id()?), + name: record[1].take().to_string()?, + email: record[2].take().to_string()?, + todos: HasMany::load(record[3].take())?, + moto: record[4].take().to_option_string()?, + }) + } +} +impl<'a> stmt::IntoSelect<'a> for &'a User { + type Model = User; + fn into_select(self) -> stmt::Select<'a, Self::Model> { + User::find_by_id(&self.id).into_select() + } +} +impl stmt::AsSelect for User { + type Model = User; + fn as_select(&self) -> stmt::Select<'_, Self::Model> { + User::find_by_id(&self.id).into_select() + } +} +impl stmt::IntoSelect<'static> for User { + type Model = User; + fn into_select(self) -> stmt::Select<'static, Self::Model> { + User::find_by_id(self.id).into_select() + } +} +impl<'a> stmt::IntoExpr<'a, User> for &'a User { + fn into_expr(self) -> stmt::Expr<'a, User> { + stmt::Key::from_expr(&self.id).into() + } +} +impl<'a> stmt::IntoExpr<'a, [User]> for &'a User { + fn into_expr(self) -> stmt::Expr<'a, [User]> { + stmt::Key::from_expr(&self.id).into() + } +} +#[derive(Debug)] +pub struct Query<'a> { + stmt: stmt::Select<'a, User>, +} +impl<'a> Query<'a> { + pub const fn from_stmt(stmt: stmt::Select<'a, User>) -> Query<'a> { + Query { stmt } + } + pub async fn all(self, db: &'a Db) -> Result> { + db.all(self.stmt).await + } + pub async fn first(self, db: &Db) -> Result> { + db.first(self.stmt).await + } + pub async fn get(self, db: &Db) -> Result { + db.get(self.stmt).await + } + pub fn update(self) -> UpdateQuery<'a> { + UpdateQuery::from(self) + } + pub async fn delete(self, db: &Db) -> Result<()> { + db.exec(self.stmt.delete()).await?; + Ok(()) + } + pub async fn collect(self, db: &'a Db) -> Result + where + A: FromCursor, + { + self.all(db).await?.collect().await + } + pub fn filter(self, expr: stmt::Expr<'a, bool>) -> Query<'a> { + Query { + stmt: self.stmt.and(expr), + } + } +} +impl<'a> stmt::IntoSelect<'a> for Query<'a> { + type Model = User; + fn into_select(self) -> stmt::Select<'a, User> { + self.stmt + } +} +impl<'a> stmt::IntoSelect<'a> for &Query<'a> { + type Model = User; + fn into_select(self) -> stmt::Select<'a, User> { + self.stmt.clone() + } +} +impl Default for Query<'static> { + fn default() -> Query<'static> { + Query { + stmt: stmt::Select::all(), + } + } +} +#[derive(Debug)] +pub struct CreateUser<'a> { + pub(super) stmt: stmt::Insert<'a, User>, +} +impl<'a> CreateUser<'a> { + pub fn id(mut self, id: impl Into>) -> Self { + self.stmt.set_value(0, id.into()); + self + } + pub fn name(mut self, name: impl Into) -> Self { + self.stmt.set_value(1, name.into()); + self + } + pub fn email(mut self, email: impl Into) -> Self { + self.stmt.set_value(2, email.into()); + self + } + pub fn todo(mut self, todo: impl IntoExpr<'a, super::todo::Todo>) -> Self { + self.stmt.push_expr(3, todo.into_expr()); + self + } + pub fn moto(mut self, moto: impl Into) -> Self { + self.stmt.set_value(4, moto.into()); + self + } + pub async fn exec(self, db: &'a Db) -> Result { + db.exec_insert_one::(self.stmt).await + } +} +impl<'a> IntoInsert<'a> for CreateUser<'a> { + type Model = User; + fn into_insert(self) -> stmt::Insert<'a, User> { + self.stmt + } +} +impl<'a> IntoExpr<'a, User> for CreateUser<'a> { + fn into_expr(self) -> stmt::Expr<'a, User> { + self.stmt.into() + } +} +impl<'a> IntoExpr<'a, [User]> for CreateUser<'a> { + fn into_expr(self) -> stmt::Expr<'a, [User]> { + self.stmt.into_list_expr() + } +} +impl<'a> Default for CreateUser<'a> { + fn default() -> CreateUser<'a> { + CreateUser { + stmt: stmt::Insert::blank(), + } + } +} +#[derive(Debug)] +pub struct UpdateUser<'a> { + model: &'a mut User, + query: UpdateQuery<'a>, +} +#[derive(Debug)] +pub struct UpdateQuery<'a> { + stmt: stmt::Update<'a, User>, +} +impl<'a> UpdateUser<'a> { + pub fn id(mut self, id: impl Into>) -> Self { + self.query.set_id(id); + self + } + pub fn name(mut self, name: impl Into) -> Self { + self.query.set_name(name); + self + } + pub fn email(mut self, email: impl Into) -> Self { + self.query.set_email(email); + self + } + pub fn todo(mut self, todo: impl IntoExpr<'a, super::todo::Todo>) -> Self { + self.query.add_todo(todo); + self + } + pub fn moto(mut self, moto: impl Into) -> Self { + self.query.set_moto(moto); + self + } + pub fn unset_moto(&mut self) -> &mut Self { + self.query.unset_moto(); + self + } + pub async fn exec(self, db: &Db) -> Result<()> { + let fields; + let mut into_iter; + { + let mut stmt = self.query.stmt; + fields = stmt.fields().clone(); + stmt.set_selection(&*self.model); + let mut records = db.exec::(stmt.into()).await?; + into_iter = records + .next() + .await + .unwrap()? + .into_record() + .into_owned() + .into_iter(); + } + for field in fields.iter() { + match field.into_usize() { + 0 => self.model.id = stmt::Id::from_untyped(into_iter.next().unwrap().to_id()?), + 1 => self.model.name = into_iter.next().unwrap().to_string()?, + 2 => self.model.email = into_iter.next().unwrap().to_string()?, + 3 => {} + 4 => self.model.moto = into_iter.next().unwrap().to_option_string()?, + _ => todo!("handle unknown field id in reload after update"), + } + } + Ok(()) + } +} +impl<'a> UpdateQuery<'a> { + pub fn id(mut self, id: impl Into>) -> Self { + self.set_id(id); + self + } + pub fn set_id(&mut self, id: impl Into>) -> &mut Self { + self.stmt.set_expr(0, id.into()); + self + } + pub fn name(mut self, name: impl Into) -> Self { + self.set_name(name); + self + } + pub fn set_name(&mut self, name: impl Into) -> &mut Self { + self.stmt.set_expr(1, name.into()); + self + } + pub fn email(mut self, email: impl Into) -> Self { + self.set_email(email); + self + } + pub fn set_email(&mut self, email: impl Into) -> &mut Self { + self.stmt.set_expr(2, email.into()); + self + } + pub fn todo(mut self, todo: impl IntoExpr<'a, super::todo::Todo>) -> Self { + self.add_todo(todo); + self + } + pub fn add_todo(&mut self, todo: impl IntoExpr<'a, super::todo::Todo>) -> &mut Self { + self.stmt.push_expr(3, todo.into_expr()); + self + } + pub fn moto(mut self, moto: impl Into) -> Self { + self.set_moto(moto); + self + } + pub fn set_moto(&mut self, moto: impl Into) -> &mut Self { + self.stmt.set_expr(4, moto.into()); + self + } + pub fn unset_moto(&mut self) -> &mut Self { + self.stmt.set(4, Value::Null); + self + } + pub async fn exec(self, db: &Db) -> Result<()> { + let stmt = self.stmt; + let mut cursor = db.exec(stmt.into()).await?; + Ok(()) + } +} +impl<'a> From> for UpdateQuery<'a> { + fn from(value: Query<'a>) -> UpdateQuery<'a> { + UpdateQuery { + stmt: stmt::Update::new(value), + } + } +} +impl<'a> From> for UpdateQuery<'a> { + fn from(src: stmt::Select<'a, User>) -> UpdateQuery<'a> { + UpdateQuery { + stmt: stmt::Update::new(src), + } + } +} +pub mod fields { + use super::*; + pub struct Todos { + pub(super) path: Path<[super::super::todo::Todo]>, + } + impl Todos { + pub const fn from_path(path: Path<[super::super::todo::Todo]>) -> Todos { + Todos { path } + } + pub fn id(mut self) -> Path> { + self.path.chain(super::super::todo::Todo::ID) + } + pub fn user_id(mut self) -> Path> { + self.path.chain(super::super::todo::Todo::USER_ID) + } + pub fn user(mut self) -> super::super::todo::fields::User { + let path = self.path.chain(super::super::todo::Todo::USER); + super::super::todo::fields::User::from_path(path) + } + pub fn title(mut self) -> Path { + self.path.chain(super::super::todo::Todo::TITLE) + } + } + impl From for Path<[super::super::todo::Todo]> { + fn from(val: Todos) -> Path<[super::super::todo::Todo]> { + val.path + } + } + impl<'stmt> stmt::IntoExpr<'stmt, super::relation::todos::Todos<'stmt>> for Todos { + fn into_expr(self) -> stmt::Expr<'stmt, super::relation::todos::Todos<'stmt>> { + todo!("into_expr for {} (field path struct)", stringify!(Todos)); + } + } +} +pub mod relation { + use super::*; + use toasty::Cursor; + pub mod todos { + use super::*; + #[derive(Debug)] + pub struct Todos<'a> { + scope: &'a User, + } + #[derive(Debug)] + pub struct Query<'a> { + pub(super) scope: super::Query<'a>, + } + #[derive(Debug)] + pub struct Remove<'a> { + stmt: stmt::Unlink<'a, super::User>, + } + #[derive(Debug)] + pub struct Add<'a> { + stmt: stmt::Link<'a, super::User>, + } + impl super::User { + pub fn todos(&self) -> Todos<'_> { + Todos { scope: self } + } + } + impl<'a> super::Query<'a> { + pub fn todos(self) -> Query<'a> { + Query::with_scope(self) + } + } + impl<'a> Todos<'a> { + pub fn get(&self) -> &[super::super::super::todo::Todo] { + self.scope.todos.get() + } + #[doc = r" Iterate all entries in the relation"] + pub async fn all( + self, + db: &'a Db, + ) -> Result> { + db.all(self.into_select()).await + } + pub async fn collect(self, db: &'a Db) -> Result + where + A: FromCursor, + { + self.all(db).await?.collect().await + } + #[doc = r" Create a new associated record"] + pub fn create(self) -> super::super::super::todo::CreateTodo<'a> { + let mut builder = super::super::super::todo::CreateTodo::default(); + builder.stmt.set_scope(self); + builder + } + pub fn query( + self, + filter: stmt::Expr<'a, bool>, + ) -> super::super::super::todo::Query<'a> { + let query = self.into_select(); + super::super::super::todo::Query::from_stmt(query.and(filter)) + } + #[doc = r" Add an item to the association"] + pub fn add( + self, + todos: impl IntoSelect<'a, Model = super::super::super::todo::Todo>, + ) -> Add<'a> { + Add { + stmt: stmt::Link::new(self.scope, super::User::TODOS, todos), + } + } + #[doc = r" Remove items from the association"] + pub fn remove( + self, + todos: impl IntoSelect<'a, Model = super::super::super::todo::Todo>, + ) -> Remove<'a> { + Remove { + stmt: stmt::Unlink::new(self.scope, super::User::TODOS, todos), + } + } + pub fn find_by_id( + self, + id: impl stmt::IntoExpr<'a, Id>, + ) -> FindByUserAndId<'a> { + FindByUserAndId { + stmt: stmt::Select::from_expr( + super::super::super::todo::Todo::USER + .in_query(self.scope) + .and(super::super::super::todo::Todo::ID.eq(id)), + ), + } + } + } + impl<'a> stmt::IntoSelect<'a> for Todos<'a> { + type Model = super::super::super::todo::Todo; + fn into_select(self) -> stmt::Select<'a, super::super::super::todo::Todo> { + super::super::super::todo::Todo::filter( + super::super::super::todo::Todo::USER.in_query(self.scope), + ) + .into_select() + } + } + impl<'a> Query<'a> { + pub fn with_scope(scope: S) -> Query<'a> + where + S: IntoSelect<'a, Model = User>, + { + Query { + scope: super::Query::from_stmt(scope.into_select()), + } + } + pub fn find_by_id( + self, + id: impl stmt::IntoExpr<'a, Id>, + ) -> FindByUserAndId<'a> { + FindByUserAndId { + stmt: stmt::Select::from_expr( + super::super::super::todo::Todo::USER + .in_query(self.scope) + .and(super::super::super::todo::Todo::ID.eq(id)), + ), + } + } + } + impl<'a> Add<'a> { + pub async fn exec(self, db: &'a Db) -> Result<()> { + let mut cursor = db.exec(self.stmt.into()).await?; + assert!(cursor.next().await.is_none()); + Ok(()) + } + } + impl<'a> Remove<'a> { + pub async fn exec(self, db: &'a Db) -> Result<()> { + let mut cursor = db.exec(self.stmt.into()).await?; + assert!(cursor.next().await.is_none()); + Ok(()) + } + } + pub struct FindByUserAndId<'a> { + stmt: stmt::Select<'a, super::super::super::todo::Todo>, + } + impl<'a> FindByUserAndId<'a> { + pub async fn all( + self, + db: &'a Db, + ) -> Result> { + db.all(self.stmt).await + } + pub async fn first(self, db: &Db) -> Result> { + db.first(self.stmt).await + } + pub async fn get(self, db: &Db) -> Result { + db.get(self.stmt).await + } + pub fn update(self) -> super::super::super::todo::UpdateQuery<'a> { + super::super::super::todo::UpdateQuery::from(self.stmt) + } + pub async fn delete(self, db: &Db) -> Result<()> { + db.exec(self.stmt.delete()).await?; + Ok(()) + } + } + impl<'a> stmt::IntoSelect<'a> for FindByUserAndId<'a> { + type Model = super::super::super::todo::Todo; + fn into_select(self) -> stmt::Select<'a, Self::Model> { + self.stmt + } + } + } + pub use todos::Todos; +} +pub mod queries { + use super::*; + impl super::User { + pub fn find_by_id<'a>(id: impl stmt::IntoExpr<'a, Id>) -> FindById<'a> { + FindById { + query: Query::from_stmt(stmt::Select::from_expr(User::ID.eq(id))), + } + } + } + pub struct FindById<'a> { + query: Query<'a>, + } + impl<'a> FindById<'a> { + pub async fn all(self, db: &'a Db) -> Result> { + self.query.all(db).await + } + pub async fn first(self, db: &Db) -> Result> { + self.query.first(db).await + } + pub async fn get(self, db: &Db) -> Result { + self.query.get(db).await + } + pub fn update(self) -> super::UpdateQuery<'a> { + super::UpdateQuery::from(self.query) + } + pub async fn delete(self, db: &Db) -> Result<()> { + self.query.delete(db).await + } + pub fn include(mut self, path: impl Into>) -> FindById<'a> { + let path = path.into(); + self.query.stmt.include(path); + self + } + pub fn filter(self, filter: stmt::Expr<'a, bool>) -> Query<'a> { + let stmt = self.into_select(); + Query::from_stmt(stmt.and(filter)) + } + pub async fn collect(self, db: &'a Db) -> Result + where + A: FromCursor, + { + self.all(db).await?.collect().await + } + pub fn todos(mut self) -> self::relation::todos::Query<'a> { + self::relation::todos::Query::with_scope(self) + } + } + impl<'a> stmt::IntoSelect<'a> for FindById<'a> { + type Model = super::User; + fn into_select(self) -> stmt::Select<'a, Self::Model> { + self.query.into_select() + } + } + impl super::User { + pub fn find_many_by_id<'a>() -> FindManyById<'a> { + FindManyById { items: vec![] } + } + } + pub struct FindManyById<'a> { + items: Vec>>, + } + impl<'a> FindManyById<'a> { + pub fn item(mut self, id: impl stmt::IntoExpr<'a, Id>) -> Self { + self.items.push(id.into_expr()); + self + } + pub async fn all(self, db: &'a Db) -> Result> { + db.all(self.into_select()).await + } + pub async fn first(self, db: &Db) -> Result> { + db.first(self.into_select()).await + } + pub async fn get(self, db: &Db) -> Result { + db.get(self.into_select()).await + } + pub fn update(self) -> super::UpdateQuery<'a> { + super::UpdateQuery::from(self.into_select()) + } + pub async fn delete(self, db: &Db) -> Result<()> { + db.delete(self.into_select()).await + } + pub fn filter(self, filter: stmt::Expr<'a, bool>) -> Query<'a> { + let stmt = self.into_select(); + Query::from_stmt(stmt.and(filter)) + } + pub async fn collect(self, db: &'a Db) -> Result + where + A: FromCursor, + { + self.all(db).await?.collect().await + } + } + impl<'a> stmt::IntoSelect<'a> for FindManyById<'a> { + type Model = super::User; + fn into_select(self) -> stmt::Select<'a, Self::Model> { + stmt::Select::from_expr(stmt::in_set(User::ID, self.items)) + } + } + impl super::User { + pub fn find_by_email<'a>(email: impl stmt::IntoExpr<'a, String>) -> FindByEmail<'a> { + FindByEmail { + query: Query::from_stmt(stmt::Select::from_expr(User::EMAIL.eq(email))), + } + } + } + pub struct FindByEmail<'a> { + query: Query<'a>, + } + impl<'a> FindByEmail<'a> { + pub async fn all(self, db: &'a Db) -> Result> { + self.query.all(db).await + } + pub async fn first(self, db: &Db) -> Result> { + self.query.first(db).await + } + pub async fn get(self, db: &Db) -> Result { + self.query.get(db).await + } + pub fn update(self) -> super::UpdateQuery<'a> { + super::UpdateQuery::from(self.query) + } + pub async fn delete(self, db: &Db) -> Result<()> { + self.query.delete(db).await + } + pub fn include(mut self, path: impl Into>) -> FindByEmail<'a> { + let path = path.into(); + self.query.stmt.include(path); + self + } + pub fn filter(self, filter: stmt::Expr<'a, bool>) -> Query<'a> { + let stmt = self.into_select(); + Query::from_stmt(stmt.and(filter)) + } + pub async fn collect(self, db: &'a Db) -> Result + where + A: FromCursor, + { + self.all(db).await?.collect().await + } + pub fn todos(mut self) -> self::relation::todos::Query<'a> { + self::relation::todos::Query::with_scope(self) + } + } + impl<'a> stmt::IntoSelect<'a> for FindByEmail<'a> { + type Model = super::User; + fn into_select(self) -> stmt::Select<'a, Self::Model> { + self.query.into_select() + } + } + impl super::User { + pub fn find_many_by_email<'a>() -> FindManyByEmail<'a> { + FindManyByEmail { items: vec![] } + } + } + pub struct FindManyByEmail<'a> { + items: Vec>, + } + impl<'a> FindManyByEmail<'a> { + pub fn item(mut self, email: impl stmt::IntoExpr<'a, String>) -> Self { + self.items.push(email.into_expr()); + self + } + pub async fn all(self, db: &'a Db) -> Result> { + db.all(self.into_select()).await + } + pub async fn first(self, db: &Db) -> Result> { + db.first(self.into_select()).await + } + pub async fn get(self, db: &Db) -> Result { + db.get(self.into_select()).await + } + pub fn update(self) -> super::UpdateQuery<'a> { + super::UpdateQuery::from(self.into_select()) + } + pub async fn delete(self, db: &Db) -> Result<()> { + db.delete(self.into_select()).await + } + pub fn filter(self, filter: stmt::Expr<'a, bool>) -> Query<'a> { + let stmt = self.into_select(); + Query::from_stmt(stmt.and(filter)) + } + pub async fn collect(self, db: &'a Db) -> Result + where + A: FromCursor, + { + self.all(db).await?.collect().await + } + } + impl<'a> stmt::IntoSelect<'a> for FindManyByEmail<'a> { + type Model = super::User; + fn into_select(self) -> stmt::Select<'a, Self::Model> { + stmt::Select::from_expr(stmt::in_set(User::EMAIL, self.items)) + } + } +} diff --git a/examples/hello-toasty-rustqlite/src/main.rs b/examples/hello-toasty-rustqlite/src/main.rs new file mode 100644 index 0000000..8994295 --- /dev/null +++ b/examples/hello-toasty-rustqlite/src/main.rs @@ -0,0 +1,157 @@ +mod db; +use std::path::PathBuf; + +use db::{Todo, User}; + +use toasty::Db; +use toasty_sqlite::Sqlite; + +fn assert_sync_send(_: T) {} + +#[tokio::main] +async fn main() { + let schema_file: PathBuf = std::env::current_dir().unwrap().join("schema.toasty"); + let schema = toasty::schema::from_file(schema_file).unwrap(); + + // NOTE enable this to see the enstire structure in STDOUT + // println!("{schema:#?}"); + + // Use rustqlite as the driver, creating a file locally in `./` + let filename = "sqlite_db.sql"; + let file_path = format!("./{}", filename); + let file = PathBuf::from(file_path.as_str()); + let driver = Sqlite::open(file).unwrap(); + + let db = Db::new(schema, driver).await; + + // For now, reset the DB. + match db.reset_db().await { + Ok(_) => {} + Err(err) => { + let err_text = err.to_string(); + if err_text.contains("already exists in CREATE TABLE") { + panic!( + "Table already exists, consider deleting the file at {} / Error: {}", + &file_path, err + ); + } + } + } + + assert_sync_send(db::User::find_by_email("hello").first(&db)); + + println!("==> let u1 = User::create()"); + let u1 = User::create() + .name("John Doe") + .email("john@example.com") + .exec(&db) + .await + .unwrap(); + + println!("==> let u2 = User::create()"); + let u2 = User::create() + .name("Nancy Huerta") + .email("nancy@example.com") + .exec(&db) + .await + .unwrap(); + + // Find by ID + println!("==> let user = User::find_by_id(&u1.id)"); + let user = User::find_by_id(&u1.id).get(&db).await.unwrap(); + println!("USER = {user:#?}"); + + // Find by email! + println!("==> let user = User::find_by_email(&u1.email)"); + let mut user = User::find_by_email(&u1.email).get(&db).await.unwrap(); + println!("USER = {user:#?}"); + + assert!(User::create() + .name("John Dos") + .email("john@example.com") + .exec(&db) + .await + .is_err()); + + user.update().name("Foo bar").exec(&db).await.unwrap(); + assert_eq!(user.name, "Foo bar"); + assert_eq!( + User::find_by_id(&user.id).get(&db).await.unwrap().name, + user.name + ); + + // Load the user again + let user = User::find_by_id(&u2.id).get(&db).await.unwrap(); + println!(" reloaded -> {user:#?}"); + + println!(" ~~~~~~~~~~~ CREATE TODOs ~~~~~~~~~~~~"); + + // n1.todos().query(); + // n1.todos().all(&db).await.unwrap(); + let todo = u2 + .todos() + .create() + .title("finish toasty") + .exec(&db) + .await + .unwrap(); + + println!("CREATED = {todo:#?}"); + + let mut todos = u2.todos().all(&db).await.unwrap(); + + while let Some(todo) = todos.next().await { + let todo = todo.unwrap(); + println!("TODO = {todo:#?}"); + println!("-> user {:?}", todo.user().find(&db).await.unwrap()); + } + + // Now, find todos by user id + // let mut todos = db::Todo::find_by_user(&u2.id).all(&db).await.unwrap(); + + // Delete user + user.delete(&db).await.unwrap(); + assert!(User::find_by_id(&u2.id).get(&db).await.is_err()); + + // Create a batch of users + User::create_many() + .item(User::create().email("foo@example.com").name("User Foo")) + .item(User::create().email("bar@example.com").name("User Bar")) + .exec(&db) + .await + .unwrap(); + + // Lets create a new user. This time, we will batch create todos for the + // user + let mut user = User::create() + .name("Ann Chovey") + .email("ann.chovey@example.com") + .todo(Todo::create().title("Make pizza")) + .todo(Todo::create().title("Sleep")) + .exec(&db) + .await + .unwrap(); + + user.update() + .todo(Todo::create().title("might delete later")) + .exec(&db) + .await + .unwrap(); + + // Get the last todo so we can unlink it + let todos = user.todos().collect::>(&db).await.unwrap(); + let len = todos.len(); + + user.todos() + .remove(todos.last().unwrap()) + .exec(&db) + .await + .unwrap(); + + assert_eq!( + len - 1, + user.todos().collect::>(&db).await.unwrap().len() + ); + + println!(">>> DONE <<<"); +}