diff --git a/Cargo.toml b/Cargo.toml index 98ccbf8..eb59fdb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ members = [".", "atmosphere-core", "atmosphere-macros"] [workspace.package] -version = "0.2.0" +version = "0.3.0" license = "Apache-2.0" edition = "2021" exclude = ["/.github", "/tests"] @@ -16,8 +16,8 @@ repository = "https://github.com/helsing-ai/atmosphere" keywords = ["sqlx", "postgres", "database", "orm", "backend"] [workspace.dependencies] -atmosphere-core = { version = "=0.2.0", path = "atmosphere-core" } -atmosphere-macros = { version = "=0.2.0", path = "atmosphere-macros" } +atmosphere-core = { version = "=0.3.0", path = "atmosphere-core" } +atmosphere-macros = { version = "=0.3.0", path = "atmosphere-macros" } async-trait = "0.1" lazy_static = "1" sqlx = { version = "0.7", features = ["chrono"] } diff --git a/README.md b/README.md index 46d6b54..aea2e92 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![SQLx](https://img.shields.io/badge/sqlx-framework-blueviolet.svg)](https://github.com/launchbadge/sqlx) [![Crate](https://img.shields.io/crates/v/atmosphere.svg)](https://crates.io/crates/atmosphere) -[![Book](https://img.shields.io/badge/book-latest-0f5225.svg)](https://bmc-labs.github.io/atmosphere) +[![Book](https://img.shields.io/badge/book-latest-0f5225.svg)](https://helsing-ai.github.io/atmosphere) [![Docs](https://img.shields.io/badge/docs-latest-153f66.svg)](https://docs.rs/atmosphere) @@ -70,7 +70,7 @@ async fn main() -> sqlx::Result<()> { // Field Queries assert_eq!( - User::find(&pool, &0).await?, + User::read(&pool, &0).await?, User::find_by_email(&pool, "some@email.com").await?.unwrap() ); @@ -167,14 +167,15 @@ Atmosphere is able to derive and generate the following queries: #### `atmosphere::Read` -- `Model::find` -- `Model::find_all` +- `Model::read`: read a `Model` by its primary key, returning a `Model`. +- `Model::find`: find a `Model` by its primary key, returning an `Option`. +- `Model::read_all`: read all `Model`s, returning a `Vec`. - `Model::reload` #### `atmosphere::Update` - `Model::update` -- `Model::save` +- `Model::upsert` #### `atmosphere::Delete` @@ -187,8 +188,8 @@ Each struct field that is marked with `#[sql(unique)]` becomes queryable. In the above example `b` was marked as unique so atmosphere implements: -- `Model::find_by_b` -- `Model::delete_by_b` +- `Model::find_by_b`: find a `Model` by its `b` field, returning an `Option`. +- `Model::delete_by_b`: delete a `Model` by its `b` field. ### Relationships & Inter-Table Queries diff --git a/atmosphere-core/src/schema/delete.rs b/atmosphere-core/src/schema/delete.rs index 25f7c5a..543ec58 100644 --- a/atmosphere-core/src/schema/delete.rs +++ b/atmosphere-core/src/schema/delete.rs @@ -10,10 +10,10 @@ use sqlx::{database::HasArguments, Database, Executor, IntoArguments}; /// Trait for deleting rows from a database. /// -/// Provides functionality for deleting rows from a table in the database. Implementors of this trait can delete -/// entities either by their instance or by their primary key. The trait ensures proper execution of hooks at -/// various stages of the delete operation, enhancing flexibility and allowing for custom behavior during the -/// deletion process. +/// Provides functionality for deleting rows from a table in the database. Implementors of this +/// trait can delete entities either by their instance or by their primary key. The trait ensures +/// proper execution of hooks at various stages of the delete operation, enhancing flexibility and +/// allowing for custom behavior during the deletion process. #[async_trait] pub trait Delete: Table + Bind + Hooks + Send + Sync + Unpin + 'static { /// Deletes the row represented by the instance from the database. Builds and executes a delete diff --git a/atmosphere-core/src/schema/read.rs b/atmosphere-core/src/schema/read.rs index ebd564d..92eada3 100644 --- a/atmosphere-core/src/schema/read.rs +++ b/atmosphere-core/src/schema/read.rs @@ -19,7 +19,7 @@ pub trait Read: Table + Bind + Hooks + Send + Sync + Unpin + 'static { /// Finds and retrieves a row by its primary key. This method constructs a query to fetch /// a single row based on the primary key, executes it, and returns the result, optionally /// triggering hooks before and after execution. - async fn find<'e, E>(executor: E, pk: &Self::PrimaryKey) -> Result + async fn read<'e, E>(executor: E, pk: &Self::PrimaryKey) -> Result where E: Executor<'e, Database = crate::Driver>, for<'q> >::Arguments: @@ -28,7 +28,7 @@ pub trait Read: Table + Bind + Hooks + Send + Sync + Unpin + 'static { /// Finds and retrieves a row by its primary key. This method constructs a query to fetch /// a single row based on the primary key, executes it, and returns the result, optionally /// triggering hooks before and after execution. - async fn find_optional<'e, E>(executor: E, pk: &Self::PrimaryKey) -> Result> + async fn find<'e, E>(executor: E, pk: &Self::PrimaryKey) -> Result> where E: Executor<'e, Database = crate::Driver>, for<'q> >::Arguments: @@ -36,7 +36,7 @@ pub trait Read: Table + Bind + Hooks + Send + Sync + Unpin + 'static { /// Retrieves all rows from the table. This method is useful for fetching the complete /// dataset of a table, executing a query to return all rows, and applying hooks as needed. - async fn find_all<'e, E>(executor: E) -> Result> + async fn read_all<'e, E>(executor: E) -> Result> where E: Executor<'e, Database = crate::Driver>, for<'q> >::Arguments: @@ -57,7 +57,7 @@ impl Read for T where T: Table + Bind + Hooks + Send + Sync + Unpin + 'static, { - async fn find<'e, E>(executor: E, pk: &Self::PrimaryKey) -> Result + async fn read<'e, E>(executor: E, pk: &Self::PrimaryKey) -> Result where E: Executor<'e, Database = crate::Driver>, for<'q> >::Arguments: @@ -91,7 +91,7 @@ where res } - async fn find_optional<'e, E>(executor: E, pk: &Self::PrimaryKey) -> Result> + async fn find<'e, E>(executor: E, pk: &Self::PrimaryKey) -> Result> where E: Executor<'e, Database = crate::Driver>, for<'q> >::Arguments: @@ -125,27 +125,20 @@ where res } - async fn reload<'e, E>(&mut self, executor: E) -> Result<()> + async fn read_all<'e, E>(executor: E) -> Result> where E: Executor<'e, Database = crate::Driver>, for<'q> >::Arguments: IntoArguments<'q, crate::Driver> + Send, { - let query = crate::runtime::sql::select_by::(T::PRIMARY_KEY.as_col()); - - hooks::execute(HookStage::PreBind, &query, HookInput::Row(self)).await?; - - let mut sql = sqlx::query_as(query.sql()); - - for c in query.bindings().columns() { - sql = self.bind(c, sql).unwrap(); - } + let query = crate::runtime::sql::select_all::(); + hooks::execute(HookStage::PreBind, &query, HookInput::None).await?; hooks::execute(HookStage::PreExec, &query, HookInput::None).await?; - let res = sql + let res = sqlx::query_as(query.sql()) .persistent(false) - .fetch_one(executor) + .fetch_all(executor) .await .map_err(QueryError::from) .map_err(Error::Query); @@ -153,29 +146,34 @@ where hooks::execute( hooks::HookStage::PostExec, &query, - QueryResult::One(&res).into(), + QueryResult::Many(&res).into(), ) .await?; - *self = res?; - - Ok(()) + res } - async fn find_all<'e, E>(executor: E) -> Result> + async fn reload<'e, E>(&mut self, executor: E) -> Result<()> where E: Executor<'e, Database = crate::Driver>, for<'q> >::Arguments: IntoArguments<'q, crate::Driver> + Send, { - let query = crate::runtime::sql::select_all::(); + let query = crate::runtime::sql::select_by::(T::PRIMARY_KEY.as_col()); + + hooks::execute(HookStage::PreBind, &query, HookInput::Row(self)).await?; + + let mut sql = sqlx::query_as(query.sql()); + + for c in query.bindings().columns() { + sql = self.bind(c, sql).unwrap(); + } - hooks::execute(HookStage::PreBind, &query, HookInput::None).await?; hooks::execute(HookStage::PreExec, &query, HookInput::None).await?; - let res = sqlx::query_as(query.sql()) + let res = sql .persistent(false) - .fetch_all(executor) + .fetch_one(executor) .await .map_err(QueryError::from) .map_err(Error::Query); @@ -183,10 +181,12 @@ where hooks::execute( hooks::HookStage::PostExec, &query, - QueryResult::Many(&res).into(), + QueryResult::One(&res).into(), ) .await?; - res + *self = res?; + + Ok(()) } } diff --git a/atmosphere-core/src/schema/update.rs b/atmosphere-core/src/schema/update.rs index 236eaf4..4bc6caf 100644 --- a/atmosphere-core/src/schema/update.rs +++ b/atmosphere-core/src/schema/update.rs @@ -10,9 +10,10 @@ use sqlx::{database::HasArguments, Database, Executor, IntoArguments}; /// Update rows in a database. /// -/// Provides functionality for updating data in tables within a SQL database. This trait defines asynchronous methods -/// for modifying existing rows in the database, either through direct updates or upserts (update or insert if not exists). -/// It ensures that hooks are executed at various stages, enabling custom logic to be integrated into the update process. +/// Provides functionality for updating data in tables within a SQL database. This trait defines +/// asynchronous methods for modifying existing rows in the database, either through direct updates +/// or upserts (update or insert if not exists). It ensures that hooks are executed at various +/// stages, enabling custom logic to be integrated into the update process. #[async_trait] pub trait Update: Table + Bind + Hooks + Send + Sync + Unpin + 'static { /// Updates an existing row in the database. This method constructs an update query, binds the @@ -27,10 +28,9 @@ pub trait Update: Table + Bind + Hooks + Send + Sync + Unpin + 'static { for<'q> >::Arguments: IntoArguments<'q, crate::Driver> + Send; - /// Similar to `update`, but uses an upsert approach. It either updates an existing row or - /// inserts a new one if it does not exist, depending on the primary key's presence and - /// uniqueness. - async fn save<'e, E>( + /// Similar to `update`, but either updates an existing row or inserts a new one if it does not + /// exist, depending on the primary key's presence and uniqueness. + async fn upsert<'e, E>( &mut self, executor: E, ) -> Result<::QueryResult> @@ -83,7 +83,10 @@ where res } - async fn save<'e, E>(&mut self, executor: E) -> Result<::QueryResult> + async fn upsert<'e, E>( + &mut self, + executor: E, + ) -> Result<::QueryResult> where E: Executor<'e, Database = crate::Driver>, for<'q> >::Arguments: diff --git a/atmosphere-core/src/testing.rs b/atmosphere-core/src/testing.rs index a76a7f9..7c375c0 100644 --- a/atmosphere-core/src/testing.rs +++ b/atmosphere-core/src/testing.rs @@ -16,21 +16,18 @@ where E: Entity + Clone + Debug + Eq + Send, { assert!( - E::find(pool, instance.pk()).await.is_err(), - "instance was found (find) before it was created" + E::read(pool, instance.pk()).await.is_err(), + "instance was found (read) before it was created" ); assert!( - E::find_optional(pool, instance.pk()) - .await - .unwrap() - .is_none(), - "instance was found (find_optional) before it was created" + E::find(pool, instance.pk()).await.unwrap().is_none(), + "instance was found (find) before it was created" ); instance.create(pool).await.expect("insertion did not work"); - let retrieved = E::find(pool, instance.pk()) + let retrieved = E::read(pool, instance.pk()) .await .expect("instance not found after insertion"); @@ -47,32 +44,29 @@ where E: Entity + Clone + Debug + Eq + Send, { assert!( - E::find(pool, instance.pk()).await.is_err(), - "instance was found (find) after deletion" + E::read(pool, instance.pk()).await.is_err(), + "instance was found (read) after deletion" ); assert!( - E::find_optional(pool, instance.pk()) - .await - .unwrap() - .is_none(), - "instance was found (find_optional) after deletion" + E::find(pool, instance.pk()).await.unwrap().is_none(), + "instance was found (find) after deletion" ); assert!( - E::find_all(pool).await.unwrap().is_empty(), + E::read_all(pool).await.unwrap().is_empty(), "there was an instance found in the database before creating" ); instance.create(pool).await.expect("insertion did not work"); - let retrieved = E::find(pool, instance.pk()) + let retrieved = E::read(pool, instance.pk()) .await .expect("instance not found after insertion"); assert_eq!(instance, retrieved); - assert_eq!(E::find_all(pool).await.unwrap(), vec![instance.clone()]); + assert_eq!(E::read_all(pool).await.unwrap(), vec![instance.clone()]); } /// Tests updating of an entity in the database. @@ -83,7 +77,7 @@ pub async fn update(pool: &crate::Pool, mut instance: E, updates: Vec) where E: Entity + Clone + Debug + Eq + Send, { - instance.save(pool).await.expect("insertion did not work"); + instance.upsert(pool).await.expect("insertion did not work"); for mut update in updates { update @@ -98,16 +92,16 @@ where assert_eq!(instance, update); - let retrieved = E::find(pool, instance.pk()) + let retrieved = E::read(pool, instance.pk()) .await .expect("instance not found after update"); assert_eq!(instance, retrieved); - let retrieved = E::find_optional(pool, instance.pk()) + let retrieved = E::find(pool, instance.pk()) .await .unwrap() - .expect("instance not found (find_optional) after update"); + .expect("instance not found (find) after update"); assert_eq!(instance, retrieved); } @@ -131,16 +125,13 @@ where .expect_err("instance could be reloaded from db after deletion"); assert!( - E::find(pool, instance.pk()).await.is_err(), - "instance was found (find) after deletion" + E::read(pool, instance.pk()).await.is_err(), + "instance was found (read) after deletion" ); assert!( - E::find_optional(pool, instance.pk()) - .await - .unwrap() - .is_none(), - "instance was found (find_optional) after deletion" + E::find(pool, instance.pk()).await.unwrap().is_none(), + "instance was found (find) after deletion" ); instance.create(pool).await.expect("insertion did not work"); diff --git a/atmosphere-macros/Cargo.toml b/atmosphere-macros/Cargo.toml index c018997..a6bea7c 100644 --- a/atmosphere-macros/Cargo.toml +++ b/atmosphere-macros/Cargo.toml @@ -14,7 +14,10 @@ proc-macro = true atmosphere-core.workspace = true sqlx.workspace = true proc-macro2 = { version = "1.0.36", default-features = false } -syn = { version = "2.0.39", default-features = false, features = ["parsing", "proc-macro"] } +syn = { version = "2.0.39", default-features = false, features = [ + "parsing", + "proc-macro", +] } quote = { version = "1.0.14", default-features = false } lazy_static = "1.4.0" diff --git a/atmosphere-macros/src/schema/table.rs b/atmosphere-macros/src/schema/table.rs index c9980b7..c88736c 100644 --- a/atmosphere-macros/src/schema/table.rs +++ b/atmosphere-macros/src/schema/table.rs @@ -55,7 +55,12 @@ impl Parse for TableId { #[derive(Clone, Debug)] pub struct Table { + // TODO(flrn): + // confirm what the fields `vis` and `generics` were + // intended for; remove them if they are not needed + #[allow(dead_code)] pub vis: Visibility, + #[allow(dead_code)] pub generics: Generics, pub ident: Ident, diff --git a/docs/book.toml b/docs/book.toml index d6b3a2d..1848611 100644 --- a/docs/book.toml +++ b/docs/book.toml @@ -1,5 +1,5 @@ [book] -authors = ["Mara Schulke "] +authors = ["Mara Schulke "] language = "en" multilingual = false src = "src" diff --git a/docs/src/getting-started/queries.md b/docs/src/getting-started/queries.md index 59e2302..b07a733 100644 --- a/docs/src/getting-started/queries.md +++ b/docs/src/getting-started/queries.md @@ -50,7 +50,7 @@ user.delete(&pool).await?; user.create(&pool).await?; assert_eq!( - User::find(&pool, &0).await?, + User::read(&pool, &0).await?, User::find_by_email(&pool, &"some@email.com".to_string()).await?.unwrap() ); diff --git a/docs/src/index.md b/docs/src/index.md index 71fdaef..5bd27d0 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -15,5 +15,5 @@ enabling low level usage of the underlying `sqlx` concepts. **[Traits](traits/index.md)** -[GitHub]: https://github.com/bmc-labs/atmosphere/tree/main +[GitHub]: https://github.com/helsing-ai/atmosphere/tree/main [`sqlx`]: https://github.com/launchbadge/sqlx diff --git a/tests/db/crud.rs b/tests/db/crud.rs index e2b76ca..a14490a 100644 --- a/tests/db/crud.rs +++ b/tests/db/crud.rs @@ -36,7 +36,7 @@ async fn create(pool: sqlx::PgPool) { name: "place".to_owned(), location: "holder".to_owned(), } - .save(&pool) + .upsert(&pool) .await .unwrap(); @@ -60,7 +60,7 @@ async fn read(pool: sqlx::PgPool) { name: "place".to_owned(), location: "holder".to_owned(), } - .save(&pool) + .upsert(&pool) .await .unwrap(); @@ -101,7 +101,7 @@ async fn update(pool: sqlx::PgPool) { name: "place".to_owned(), location: "holder".to_owned(), } - .save(&pool) + .upsert(&pool) .await .unwrap(); @@ -110,7 +110,7 @@ async fn update(pool: sqlx::PgPool) { name: "place".to_owned(), location: "holder".to_owned(), } - .save(&pool) + .upsert(&pool) .await .unwrap(); @@ -139,7 +139,7 @@ async fn delete(pool: sqlx::PgPool) { name: "place".to_owned(), location: "holder".to_owned(), } - .save(&pool) + .upsert(&pool) .await .unwrap();