From 159d715e76cf7bac24f026b867078bf361c9a49a Mon Sep 17 00:00:00 2001 From: Ari Breitkreuz Date: Sun, 25 Dec 2022 13:42:02 +0100 Subject: [PATCH 01/78] dummy commit for discussion From c132064f7d0e32a6acdd976c3331af41b8d72240 Mon Sep 17 00:00:00 2001 From: Ari Breitkreuz Date: Mon, 26 Dec 2022 19:19:12 +0100 Subject: [PATCH 02/78] wip making async --- butane/Cargo.toml | 5 +++-- butane/tests/basic.rs | 2 +- butane/tests/custom_pg.rs | 2 +- butane_core/Cargo.toml | 5 +++-- butane_core/src/custom.rs | 4 +++- butane_core/src/db/connmethods.rs | 6 ++++-- butane_core/src/db/macros.rs | 5 +++-- butane_core/src/db/mod.rs | 5 ++++- butane_core/src/db/pg.rs | 17 +++++++++++------ butane_core/src/db/sqlite.rs | 14 +++++++++----- butane_core/src/lib.rs | 17 +++++++++++------ butane_core/src/migrations/mod.rs | 16 +++++++++++----- butane_core/src/query/mod.rs | 8 ++++---- 13 files changed, 68 insertions(+), 38 deletions(-) diff --git a/butane/Cargo.toml b/butane/Cargo.toml index c31b764d..f3783cbc 100644 --- a/butane/Cargo.toml +++ b/butane/Cargo.toml @@ -40,11 +40,12 @@ log_for_test = {package="log", version = "0.4"} quote = "1.0" proc-macro2="1.0" once_cell="1.5.2" -postgres = { version = "0.19", features=["with-geo-types-0_7"] } +tokio-postgres = { version = "0.7", features=["with-geo-types-0_7"] } +# tokio-postgres = { version = "0.7" } r2d2_for_test = {package="r2d2", version = "0.8"} rusqlite = {workspace=true} serde_json = "1.0" uuid_for_test = {package="uuid", version = "1.2", features=["v4"] } [package.metadata.docs.rs] -all-features = true \ No newline at end of file +all-features = true diff --git a/butane/tests/basic.rs b/butane/tests/basic.rs index d1dcec7a..15a5c67c 100644 --- a/butane/tests/basic.rs +++ b/butane/tests/basic.rs @@ -4,7 +4,7 @@ use butane::{butane_type, find, model, query}; use butane::{ForeignKey, ObjectState}; use paste; #[cfg(feature = "pg")] -use postgres; +use tokio_postgres as postgres; #[cfg(feature = "sqlite")] use rusqlite; diff --git a/butane/tests/custom_pg.rs b/butane/tests/custom_pg.rs index 4e44ec50..b00f70f0 100644 --- a/butane/tests/custom_pg.rs +++ b/butane/tests/custom_pg.rs @@ -9,7 +9,7 @@ mod custom_pg { use butane::{butane_type, db::Connection, model, ObjectState}; use butane::{FieldType, FromSql, SqlType, SqlVal, SqlValRef, ToSql}; use geo_types; - use postgres; + use tokio_postgres as postgres; use std::result::Result; // newtype so we can implement traits for it. diff --git a/butane_core/Cargo.toml b/butane_core/Cargo.toml index 7b68c807..4586821e 100644 --- a/butane_core/Cargo.toml +++ b/butane_core/Cargo.toml @@ -15,7 +15,7 @@ debug = ["log"] sqlite = ["rusqlite"] sqlite-bundled = ["rusqlite/bundled"] tls = ["postgres-native-tls", "native-tls"] -pg = ["postgres", "bytes"] +pg = ["tokio-postgres", "bytes"] [dependencies] @@ -28,7 +28,7 @@ hex = "0.4" once_cell="1.5" log = { version="0.4", optional=true } native-tls={ version = "0.2", optional = true } -postgres={ version = "0.19", features=["with-chrono-0_4"], optional = true} +tokio-postgres={ version = "0.7", features=["with-chrono-0_4"], optional = true} postgres-native-tls={ version = "0.5", optional = true } proc-macro2 = "1.0" pin-project = "1" @@ -42,3 +42,4 @@ syn = { version = "1.0", features = ["full", "extra-traits"] } thiserror = "1.0" chrono = { version = "0.4", features=["serde"], optional = true } uuid = {workspace=true, optional=true} +async-trait = "0.1" diff --git a/butane_core/src/custom.rs b/butane_core/src/custom.rs index 1c77f846..6495cd77 100644 --- a/butane_core/src/custom.rs +++ b/butane_core/src/custom.rs @@ -7,12 +7,13 @@ use serde::{Deserialize, Serialize}; use std::fmt; +use tokio_postgres as postgres; /// For use with [SqlType::Custom](crate::SqlType) #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub enum SqlTypeCustom { #[cfg(feature = "pg")] - Pg(#[serde(with = "pgtypeser")] postgres::types::Type), + Pg(#[serde(with = "pgtypeser")] tokio_postgres::types::Type), } /// For use with [SqlVal::Custom](crate::SqlVal) @@ -137,6 +138,7 @@ impl From> for SqlValCustom { #[cfg(feature = "pg")] mod pgtypeser { use serde::{Deserialize, Deserializer, Serialize, Serializer}; + use tokio_postgres as postgres; pub fn serialize(ty: &postgres::types::Type, serializer: S) -> Result where diff --git a/butane_core/src/db/connmethods.rs b/butane_core/src/db/connmethods.rs index 85078ec4..5cffddbe 100644 --- a/butane_core/src/db/connmethods.rs +++ b/butane_core/src/db/connmethods.rs @@ -5,14 +5,16 @@ use crate::query::{BoolExpr, Expr, Order}; use crate::{Result, SqlType, SqlVal, SqlValRef}; use std::ops::{Deref, DerefMut}; use std::vec::Vec; +use async_trait::async_trait; /// Methods available on a database connection. Most users do not need /// to call these methods directly and will instead use methods on /// [DataObject][crate::DataObject] or the `query!` macro. This trait is /// implemented by both database connections and transactions. -pub trait ConnectionMethods { +#[async_trait] +pub trait ConnectionMethods: Sync { fn execute(&self, sql: &str) -> Result<()>; - fn query<'a, 'b, 'c: 'a>( + async fn query<'a, 'b, 'c: 'a>( &'c self, table: &str, columns: &'b [Column], diff --git a/butane_core/src/db/macros.rs b/butane_core/src/db/macros.rs index 6a1f1897..a64fb9f7 100644 --- a/butane_core/src/db/macros.rs +++ b/butane_core/src/db/macros.rs @@ -1,11 +1,12 @@ #[macro_export] macro_rules! connection_method_wrapper { ($ty:path) => { + #[async_trait::async_trait] impl ConnectionMethods for $ty { fn execute(&self, sql: &str) -> Result<()> { ConnectionMethods::execute(self.wrapped_connection_methods()?, sql) } - fn query<'a, 'b, 'c: 'a>( + async fn query<'a, 'b, 'c: 'a>( &'c self, table: &str, columns: &'b [Column], @@ -15,7 +16,7 @@ macro_rules! connection_method_wrapper { sort: Option<&[$crate::query::Order]>, ) -> Result> { self.wrapped_connection_methods()? - .query(table, columns, expr, limit, offset, sort) + .query(table, columns, expr, limit, offset, sort).await } fn insert_returning_pk( &self, diff --git a/butane_core/src/db/mod.rs b/butane_core/src/db/mod.rs index 85978135..8f306a1e 100644 --- a/butane_core/src/db/mod.rs +++ b/butane_core/src/db/mod.rs @@ -20,6 +20,7 @@ use std::fs; use std::io::Write; use std::ops::{Deref, DerefMut}; use std::path::Path; +use async_trait::async_trait; mod connmethods; mod helper; @@ -42,6 +43,7 @@ pub use connmethods::{ }; /// Database connection. +#[async_trait] pub trait BackendConnection: ConnectionMethods + Send + 'static { /// Begin a database transaction. The transaction object must be /// used in place of this connection until it is committed and aborted. @@ -166,6 +168,7 @@ pub fn connect(spec: &ConnectionSpec) -> Result { .connect(&spec.conn_str) } +#[async_trait] trait BackendTransaction<'c>: ConnectionMethods { /// Commit the transaction Unfortunately because we use this as a /// trait object, we can't consume self. It should be understood @@ -173,7 +176,7 @@ trait BackendTransaction<'c>: ConnectionMethods { /// not public, and that behavior is enforced by Transaction fn commit(&mut self) -> Result<()>; /// Roll back the transaction. Same comment about consuming self as above. - fn rollback(&mut self) -> Result<()>; + async fn rollback(&mut self) -> Result<()>; // Workaround for https://github.com/rust-lang/rfcs/issues/2765 fn connection_methods(&self) -> &dyn ConnectionMethods; diff --git a/butane_core/src/db/pg.rs b/butane_core/src/db/pg.rs index 96613722..2f8c91d2 100644 --- a/butane_core/src/db/pg.rs +++ b/butane_core/src/db/pg.rs @@ -9,10 +9,12 @@ use crate::{Result, SqlType, SqlVal, SqlValRef}; use bytes::BufMut; #[cfg(feature = "datetime")] use chrono::NaiveDateTime; -use postgres::fallible_iterator::FallibleIterator; -use postgres::GenericClient; +use tokio_postgres::fallible_iterator::FallibleIterator; +use tokio_postgres::GenericClient; +use tokio_postgres as postgres; use std::cell::RefCell; use std::fmt::Write; +use async_trait::async_trait; /// The name of the postgres backend. pub const BACKEND_NAME: &str = "pg"; @@ -113,6 +115,7 @@ pub trait PgConnectionLike { fn cell(&self) -> Result<&RefCell>; } +#[async_trait] impl ConnectionMethods for T where T: PgConnectionLike, @@ -125,7 +128,7 @@ where Ok(()) } - fn query<'a, 'b, 'c: 'a>( + async fn query<'a, 'b, 'c: 'a>( &'c self, table: &str, columns: &'b [Column], @@ -168,7 +171,7 @@ where let stmt = self .cell()? .try_borrow_mut()? - .prepare_typed(&sqlquery, types.as_ref())?; + .prepare_typed(&sqlquery, types.as_ref()).await?; // todo avoid intermediate vec? let rowvec: Vec = self .cell()? @@ -323,6 +326,7 @@ impl<'c> PgConnectionLike for PgTransaction<'c> { } } +#[async_trait] impl<'c> BackendTransaction<'c> for PgTransaction<'c> { fn commit(&mut self) -> Result<()> { match self.trans.take() { @@ -330,10 +334,11 @@ impl<'c> BackendTransaction<'c> for PgTransaction<'c> { Some(trans) => Ok(trans.into_inner().commit()?), } } - fn rollback(&mut self) -> Result<()> { + + async fn rollback(&mut self) -> Result<()> { match self.trans.take() { None => Err(Self::already_consumed()), - Some(trans) => Ok(trans.into_inner().rollback()?), + Some(trans) => Ok(trans.into_inner().rollback().await?), } } // Workaround for https://github.com/rust-lang/rfcs/issues/2765 diff --git a/butane_core/src/db/sqlite.rs b/butane_core/src/db/sqlite.rs index b1f0bc2e..7f4a11d1 100644 --- a/butane_core/src/db/sqlite.rs +++ b/butane_core/src/db/sqlite.rs @@ -14,6 +14,7 @@ use pin_project::pin_project; use std::borrow::Cow; use std::fmt::Write; use std::pin::Pin; +use async_trait::async_trait; #[cfg(feature = "datetime")] const SQLITE_DT_FORMAT: &str = "%Y-%m-%d %H:%M:%S"; @@ -61,7 +62,7 @@ impl Backend for SQLiteBackend { /// SQLite database connection. pub struct SQLiteConnection { - conn: rusqlite::Connection, + conn: std::sync::Mutex, } impl SQLiteConnection { fn open(path: impl AsRef) -> Result { @@ -95,6 +96,7 @@ impl BackendConnection for SQLiteConnection { } } +#[async_trait] impl ConnectionMethods for rusqlite::Connection { fn execute(&self, sql: &str) -> Result<()> { if cfg!(feature = "log") { @@ -104,7 +106,7 @@ impl ConnectionMethods for rusqlite::Connection { Ok(()) } - fn query<'a, 'b, 'c: 'a>( + async fn query<'a, 'b, 'c: 'a>( &'c self, table: &str, columns: &'b [Column], @@ -250,7 +252,7 @@ impl ConnectionMethods for rusqlite::Connection { } struct SqliteTransaction<'c> { - trans: Option>, + trans: std::sync::Mutex>>, } impl<'c> SqliteTransaction<'c> { fn new(trans: rusqlite::Transaction<'c>) -> Self { @@ -270,14 +272,16 @@ impl<'c> SqliteTransaction<'c> { } } connection_method_wrapper!(SqliteTransaction<'_>); + +#[async_trait] impl<'c> BackendTransaction<'c> for SqliteTransaction<'c> { fn commit(&mut self) -> Result<()> { - match self.trans.take() { + match self.trans.take().take() { None => Err(Self::already_consumed()), Some(trans) => Ok(trans.commit()?), } } - fn rollback(&mut self) -> Result<()> { + async fn rollback(&mut self) -> Result<()> { match self.trans.take() { None => Err(Self::already_consumed()), Some(trans) => Ok(trans.rollback()?), diff --git a/butane_core/src/lib.rs b/butane_core/src/lib.rs index ccdc2412..2548f436 100644 --- a/butane_core/src/lib.rs +++ b/butane_core/src/lib.rs @@ -5,6 +5,7 @@ use std::borrow::Borrow; use std::cmp::{Eq, PartialEq}; use std::default::Default; use thiserror::Error as ThisError; +use async_trait::async_trait; pub mod codegen; pub mod custom; @@ -53,6 +54,7 @@ impl Eq for ObjectState {} /// object type. The purpose of a result type which is not also an /// object type is to allow a query to retrieve a subset of an /// object's columns. +#[async_trait] pub trait DataResult: Sized { /// Corresponding object type. type DBO: DataObject; @@ -61,13 +63,14 @@ pub trait DataResult: Sized { where Self: Sized; /// Create a blank query (matching all rows) for this type. - fn query() -> Query; + async fn query() -> Query; } /// An object in the database. /// /// Rather than implementing this type manually, use the /// `#[model]` attribute. +#[async_trait] pub trait DataObject: DataResult { /// The type of the primary key field. type PKType: PrimaryKeyType; @@ -82,21 +85,23 @@ pub trait DataObject: DataResult { /// Get the primary key fn pk(&self) -> &Self::PKType; /// Find this object in the database based on primary key. - fn get(conn: &impl ConnectionMethods, id: impl Borrow) -> Result + async fn get(conn: &impl ConnectionMethods, id: impl Borrow) -> Result where Self: Sized, + Self::PKType: Sync, { - ::query() - .filter(query::BoolExpr::Eq( + let query = ::query().await; + query.filter(query::BoolExpr::Eq( Self::PKCOL, query::Expr::Val(id.borrow().to_sql()), )) .limit(1) - .load(conn)? + .load(conn).await? .into_iter() .nth(0) .ok_or(Error::NoSuchObject) } + /// Save the object to the database. fn save(&mut self, conn: &impl ConnectionMethods) -> Result<()>; /// Delete the object from the database. @@ -162,7 +167,7 @@ pub enum Error { SQLiteFromSQL(rusqlite::types::FromSqlError), #[cfg(feature = "pg")] #[error("Postgres error {0}")] - Postgres(#[from] postgres::Error), + Postgres(#[from] tokio_postgres::Error), #[cfg(feature = "datetime")] #[error("Chrono error {0}")] Chrono(#[from] chrono::ParseError), diff --git a/butane_core/src/migrations/mod.rs b/butane_core/src/migrations/mod.rs index 531eb75c..928c423b 100644 --- a/butane_core/src/migrations/mod.rs +++ b/butane_core/src/migrations/mod.rs @@ -20,8 +20,10 @@ mod fsmigrations; pub use fsmigrations::{FsMigration, FsMigrations}; mod memmigrations; pub use memmigrations::{MemMigration, MemMigrations}; +use async_trait::async_trait; /// A collection of migrations. +#[async_trait] pub trait Migrations { type M: Migration; @@ -66,8 +68,8 @@ pub trait Migrations { } /// Get migrations which have not yet been applied to the database - fn unapplied_migrations(&self, conn: &impl ConnectionMethods) -> Result> { - match self.last_applied_migration(conn)? { + async fn unapplied_migrations(&self, conn: &impl ConnectionMethods) -> Result> { + match self.last_applied_migration(conn).await? { None => self.all_migrations(), Some(m) => self.migrations_since(&m), } @@ -75,7 +77,7 @@ pub trait Migrations { /// Get the last migration that has been applied to the database or None /// if no migrations have been applied - fn last_applied_migration(&self, conn: &impl ConnectionMethods) -> Result> { + async fn last_applied_migration(&self, conn: &impl ConnectionMethods) -> Result> { if !conn.has_table(ButaneMigration::TABLE)? { return Ok(None); } @@ -87,7 +89,7 @@ pub trait Migrations { None, None, None, - )? + ).await? .mapped(ButaneMigration::from_row) .collect()?; @@ -233,6 +235,8 @@ pub fn copy_migration(from: &impl Migration, to: &mut impl MigrationMut) -> Resu struct ButaneMigration { name: String, } + +#[async_trait] impl DataResult for ButaneMigration { type DBO = Self; const COLUMNS: &'static [Column] = &[Column::new("name", SqlType::Text)]; @@ -246,10 +250,12 @@ impl DataResult for ButaneMigration { name: FromSql::from_sql_ref(row.get(0, SqlType::Text).unwrap())?, }) } - fn query() -> query::Query { + + async fn query() -> query::Query { query::Query::new("butane_migrations") } } + impl DataObject for ButaneMigration { type PKType = String; type Fields = (); // we don't need Fields as we never filter diff --git a/butane_core/src/query/mod.rs b/butane_core/src/query/mod.rs index 705cc5f2..1b64e4ac 100644 --- a/butane_core/src/query/mod.rs +++ b/butane_core/src/query/mod.rs @@ -179,20 +179,20 @@ impl Query { } /// Executes the query against `conn` and returns the first result (if any). - pub fn load_first(self, conn: &impl ConnectionMethods) -> Result> { - conn.query(&self.table, T::COLUMNS, self.filter, Some(1), None, None)? + pub async fn load_first(self, conn: &impl ConnectionMethods) -> Result> { + conn.query(&self.table, T::COLUMNS, self.filter, Some(1), None, None).await? .mapped(T::from_row) .nth(0) } /// Executes the query against `conn`. - pub fn load(self, conn: &impl ConnectionMethods) -> Result> { + pub async fn load(self, conn: &impl ConnectionMethods) -> Result> { let sort = if self.sort.is_empty() { None } else { Some(self.sort.as_slice()) }; - conn.query(&self.table, T::COLUMNS, self.filter, self.limit, self.offset, sort)? + conn.query(&self.table, T::COLUMNS, self.filter, self.limit, self.offset, sort).await? .mapped(T::from_row) .collect() } From e340746be108600ce2d8b1d094fc4f7fe7202886 Mon Sep 17 00:00:00 2001 From: Ari Breitkreuz Date: Sun, 8 Jan 2023 10:58:34 +0100 Subject: [PATCH 03/78] more wip on postgres --- butane_core/src/db/pg.rs | 36 ++++++++++++------------------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/butane_core/src/db/pg.rs b/butane_core/src/db/pg.rs index 2f8c91d2..c221a67c 100644 --- a/butane_core/src/db/pg.rs +++ b/butane_core/src/db/pg.rs @@ -9,7 +9,6 @@ use crate::{Result, SqlType, SqlVal, SqlValRef}; use bytes::BufMut; #[cfg(feature = "datetime")] use chrono::NaiveDateTime; -use tokio_postgres::fallible_iterator::FallibleIterator; use tokio_postgres::GenericClient; use tokio_postgres as postgres; use std::cell::RefCell; @@ -55,12 +54,12 @@ impl Backend for PgBackend { /// Pg database connection. pub struct PgConnection { - conn: RefCell, + conn: postgres::Client, } impl PgConnection { fn open(params: &str) -> Result { Ok(PgConnection { - conn: RefCell::new(Self::connect(params)?), + conn: Self::connect(params)?, }) } fn connect(params: &str) -> Result { @@ -83,7 +82,7 @@ impl PgConnectionLike for PgConnection { } impl BackendConnection for PgConnection { fn transaction(&mut self) -> Result> { - let trans: postgres::Transaction<'_> = self.conn.get_mut().transaction()?; + let trans: postgres::Transaction<'_> = self.conn.transaction()?; let trans = Box::new(PgTransaction::new(trans)); Ok(Transaction::new(trans)) } @@ -94,7 +93,7 @@ impl BackendConnection for PgConnection { BACKEND_NAME } fn is_closed(&self) -> bool { - self.conn.borrow().is_closed() + self.conn.is_closed() } } @@ -112,19 +111,19 @@ fn sqlvalref_for_pg_query<'a>(v: &'a SqlValRef<'a>) -> &'a dyn postgres::types:: /// transaction. Implementation detail. Semver exempt. pub trait PgConnectionLike { type Client: postgres::GenericClient; - fn cell(&self) -> Result<&RefCell>; + fn cell(&self) -> Result; } #[async_trait] impl ConnectionMethods for T where - T: PgConnectionLike, + T: PgConnectionLike + std::marker::Sync, { fn execute(&self, sql: &str) -> Result<()> { if cfg!(feature = "log") { debug!("execute sql {}", sql); } - self.cell()?.try_borrow_mut()?.batch_execute(sql.as_ref())?; + self.cell()?.batch_execute(sql.as_ref())?; Ok(()) } @@ -170,12 +169,10 @@ where let types: Vec = values.iter().map(pgtype_for_val).collect(); let stmt = self .cell()? - .try_borrow_mut()? .prepare_typed(&sqlquery, types.as_ref()).await?; // todo avoid intermediate vec? let rowvec: Vec = self .cell()? - .try_borrow_mut()? .query_raw(&stmt, values.iter().map(sqlval_for_pg_query))? .map_err(Error::Postgres) .map(|r| { @@ -207,7 +204,6 @@ where // use query instead of execute so we can get our result back let pk: Option = self .cell()? - .try_borrow_mut()? .query_raw(sql.as_str(), values.iter().map(sqlvalref_for_pg_query))? .map_err(Error::Postgres) .map(|r| sql_val_from_postgres(&r, 0, pkcol)) @@ -224,7 +220,6 @@ where ); let params: Vec<&DynToSqlPg> = values.iter().map(|v| v as &DynToSqlPg).collect(); self.cell()? - .try_borrow_mut()? .execute(sql.as_str(), params.as_slice())?; Ok(()) } @@ -239,7 +234,6 @@ where sql_insert_or_replace_with_placeholders(table, columns, pkcol, &mut sql); let params: Vec<&DynToSqlPg> = values.iter().map(|v| v as &DynToSqlPg).collect(); self.cell()? - .try_borrow_mut()? .execute(sql.as_str(), params.as_slice())?; Ok(()) } @@ -268,7 +262,6 @@ where debug!("update sql {}", sql); } self.cell()? - .try_borrow_mut()? .execute(sql.as_str(), params.as_slice())?; Ok(()) } @@ -285,7 +278,6 @@ where let params: Vec<&DynToSqlPg> = values.iter().map(|v| v as &DynToSqlPg).collect(); let cnt = self .cell()? - .try_borrow_mut()? .execute(sql.as_str(), params.as_slice())?; Ok(cnt as usize) } @@ -293,7 +285,6 @@ where // future improvement, should be schema-aware let stmt = self .cell()? - .try_borrow_mut()? .prepare("SELECT table_name FROM information_schema.tables WHERE table_name=$1;")?; let rows = self.cell()?.try_borrow_mut()?.query(&stmt, &[&table])?; Ok(!rows.is_empty()) @@ -301,19 +292,16 @@ where } struct PgTransaction<'c> { - trans: Option>>, + trans: Option>, } impl<'c> PgTransaction<'c> { fn new(trans: postgres::Transaction<'c>) -> Self { PgTransaction { - trans: Some(RefCell::new(trans)), + trans: Some(trans), } } - fn get(&self) -> Result<&RefCell>> { - match &self.trans { - None => Err(Self::already_consumed()), - Some(trans) => Ok(trans), - } + fn get(&self) -> Result<&postgres::Transaction<'c>> { + &self.trans.ok_or(Self::already_consumed()) } fn already_consumed() -> Error { Error::Internal("transaction has already been consumed".to_string()) @@ -321,7 +309,7 @@ impl<'c> PgTransaction<'c> { } impl<'c> PgConnectionLike for PgTransaction<'c> { type Client = postgres::Transaction<'c>; - fn cell(&self) -> Result<&RefCell> { + fn cell(&self) -> Result<&Self::Client> { self.get() } } From fa490b8f4506238ed680ce990bbb471d09b34265 Mon Sep 17 00:00:00 2001 From: Ari Breitkreuz Date: Sun, 8 Jan 2023 16:25:07 +0100 Subject: [PATCH 04/78] wip --- butane_core/Cargo.toml | 1 + butane_core/src/db/mod.rs | 4 - butane_core/src/db/pg.rs | 37 ++- butane_core/src/db/sqlite.rs | 618 ----------------------------------- butane_core/src/lib.rs | 1 + 5 files changed, 27 insertions(+), 634 deletions(-) delete mode 100644 butane_core/src/db/sqlite.rs diff --git a/butane_core/Cargo.toml b/butane_core/Cargo.toml index 4586821e..1b1d1fa9 100644 --- a/butane_core/Cargo.toml +++ b/butane_core/Cargo.toml @@ -43,3 +43,4 @@ thiserror = "1.0" chrono = { version = "0.4", features=["serde"], optional = true } uuid = {workspace=true, optional=true} async-trait = "0.1" +tokio = { version = "1.24", features=["rt"] } diff --git a/butane_core/src/db/mod.rs b/butane_core/src/db/mod.rs index 8f306a1e..a8e27b0e 100644 --- a/butane_core/src/db/mod.rs +++ b/butane_core/src/db/mod.rs @@ -27,8 +27,6 @@ mod helper; mod macros; #[cfg(feature = "pg")] pub mod pg; -#[cfg(feature = "sqlite")] -pub mod sqlite; #[cfg(feature = "r2d2")] mod r2; @@ -152,8 +150,6 @@ impl Backend for Box { /// Find a backend by name. pub fn get_backend(name: &str) -> Option> { match name { - #[cfg(feature = "sqlite")] - sqlite::BACKEND_NAME => Some(Box::new(sqlite::SQLiteBackend::new())), #[cfg(feature = "pg")] pg::BACKEND_NAME => Some(Box::new(pg::PgBackend::new())), _ => None, diff --git a/butane_core/src/db/pg.rs b/butane_core/src/db/pg.rs index c221a67c..8ed088fb 100644 --- a/butane_core/src/db/pg.rs +++ b/butane_core/src/db/pg.rs @@ -11,9 +11,9 @@ use bytes::BufMut; use chrono::NaiveDateTime; use tokio_postgres::GenericClient; use tokio_postgres as postgres; -use std::cell::RefCell; use std::fmt::Write; use async_trait::async_trait; +use tokio; /// The name of the postgres backend. pub const BACKEND_NAME: &str = "pg"; @@ -52,17 +52,21 @@ impl Backend for PgBackend { } } +// type PgConnHandle = tokio::task::JoinHandle>; + /// Pg database connection. pub struct PgConnection { - conn: postgres::Client, + client: postgres::Client, + // Save the handle to the task running the connection to keep it alive + conn_handle: tokio::task::JoinHandle<()>, } + impl PgConnection { - fn open(params: &str) -> Result { - Ok(PgConnection { - conn: Self::connect(params)?, - }) + async fn open(params: &str) -> Result { + let (client, conn_handle) = Self::connect(params).await?; + Ok( Self { client, conn_handle }) } - fn connect(params: &str) -> Result { + async fn connect(params: &str) -> Result<(postgres::Client, tokio::task::JoinHandle<()>)> { cfg_if::cfg_if! { if #[cfg(feature = "tls")] { let connector = native_tls::TlsConnector::new()?; @@ -71,13 +75,19 @@ impl PgConnection { let connector = postgres::NoTls; } } - Ok(postgres::Client::connect(params, connector)?) + let (client, conn) = postgres::connect(params, connector).await?; + let conn_handle = tokio::spawn(async move { + if let Err(e) = conn.await { + // TODO don't panic + panic!() + } + }); + Ok((client, conn_handle)) } } impl PgConnectionLike for PgConnection { - type Client = postgres::Client; - fn cell(&self) -> Result<&RefCell> { - Ok(&self.conn) + fn cell(&self) -> Result<&postgres::Client> { + Ok(&self.client) } } impl BackendConnection for PgConnection { @@ -301,7 +311,10 @@ impl<'c> PgTransaction<'c> { } } fn get(&self) -> Result<&postgres::Transaction<'c>> { - &self.trans.ok_or(Self::already_consumed()) + match self.trans { + Some(x) => Ok(&x), + None => Err(Self::already_consumed()), + } } fn already_consumed() -> Error { Error::Internal("transaction has already been consumed".to_string()) diff --git a/butane_core/src/db/sqlite.rs b/butane_core/src/db/sqlite.rs deleted file mode 100644 index 7f4a11d1..00000000 --- a/butane_core/src/db/sqlite.rs +++ /dev/null @@ -1,618 +0,0 @@ -//! SQLite database backend -use super::helper; -use super::*; -use crate::db::connmethods::BackendRows; -use crate::debug; -use crate::migrations::adb::{AColumn, ATable, Operation, TypeIdentifier, ADB}; -use crate::query; -use crate::query::Order; -use crate::{Result, SqlType, SqlVal, SqlValRef}; -#[cfg(feature = "datetime")] -use chrono::naive::NaiveDateTime; -use fallible_streaming_iterator::FallibleStreamingIterator; -use pin_project::pin_project; -use std::borrow::Cow; -use std::fmt::Write; -use std::pin::Pin; -use async_trait::async_trait; - -#[cfg(feature = "datetime")] -const SQLITE_DT_FORMAT: &str = "%Y-%m-%d %H:%M:%S"; - -/// The name of the sqlite backend. -pub const BACKEND_NAME: &str = "sqlite"; - -/// SQLite [Backend][crate::db::Backend] implementation. -#[derive(Default)] -pub struct SQLiteBackend {} -impl SQLiteBackend { - pub fn new() -> SQLiteBackend { - SQLiteBackend {} - } -} -impl SQLiteBackend { - fn connect(&self, path: &str) -> Result { - SQLiteConnection::open(Path::new(path)) - } -} -impl Backend for SQLiteBackend { - fn name(&self) -> &'static str { - BACKEND_NAME - } - - fn create_migration_sql(&self, current: &ADB, ops: Vec) -> Result { - let mut current: ADB = (*current).clone(); - Ok(ops - .into_iter() - .map(|o| { - let sql = sql_for_op(&mut current, &o); - current.transform_with(o); - sql - }) - .collect::>>()? - .join("\n")) - } - - fn connect(&self, path: &str) -> Result { - Ok(Connection { - conn: Box::new(self.connect(path)?), - }) - } -} - -/// SQLite database connection. -pub struct SQLiteConnection { - conn: std::sync::Mutex, -} -impl SQLiteConnection { - fn open(path: impl AsRef) -> Result { - rusqlite::Connection::open(path) - .map(|conn| SQLiteConnection { conn }) - .map_err(|e| e.into()) - } - - // For use with connection_method_wrapper macro - #[allow(clippy::unnecessary_wraps)] - fn wrapped_connection_methods(&self) -> Result<&rusqlite::Connection> { - Ok(&self.conn) - } -} -connection_method_wrapper!(SQLiteConnection); - -impl BackendConnection for SQLiteConnection { - fn transaction(&mut self) -> Result> { - let trans: rusqlite::Transaction<'_> = self.conn.transaction()?; - let trans = Box::new(SqliteTransaction::new(trans)); - Ok(Transaction::new(trans)) - } - fn backend(&self) -> Box { - Box::new(SQLiteBackend {}) - } - fn backend_name(&self) -> &'static str { - "sqlite" - } - fn is_closed(&self) -> bool { - false - } -} - -#[async_trait] -impl ConnectionMethods for rusqlite::Connection { - fn execute(&self, sql: &str) -> Result<()> { - if cfg!(feature = "log") { - debug!("execute sql {}", sql); - } - self.execute_batch(sql.as_ref())?; - Ok(()) - } - - async fn query<'a, 'b, 'c: 'a>( - &'c self, - table: &str, - columns: &'b [Column], - expr: Option, - limit: Option, - offset: Option, - order: Option<&[Order]>, - ) -> Result> { - let mut sqlquery = String::new(); - helper::sql_select(columns, table, &mut sqlquery); - let mut values: Vec = Vec::new(); - if let Some(expr) = expr { - sqlquery.write_str(" WHERE ").unwrap(); - sql_for_expr( - query::Expr::Condition(Box::new(expr)), - &mut values, - &mut SQLitePlaceholderSource::new(), - &mut sqlquery, - ); - } - - if let Some(order) = order { - helper::sql_order(order, &mut sqlquery) - } - - if let Some(limit) = limit { - helper::sql_limit(limit, &mut sqlquery) - } - - if let Some(offset) = offset { - if limit.is_none() { - // Sqlite only supports offset in conjunction with - // limit, so add a max limit if we don't have one - // already. - helper::sql_limit(i32::MAX, &mut sqlquery) - } - helper::sql_offset(offset, &mut sqlquery) - } - - debug!("query sql {}", sqlquery); - - let stmt = self.prepare(&sqlquery)?; - let adapter = QueryAdapter::new(stmt, rusqlite::params_from_iter(values))?; - Ok(Box::new(adapter)) - } - fn insert_returning_pk( - &self, - table: &str, - columns: &[Column], - pkcol: &Column, - values: &[SqlValRef<'_>], - ) -> Result { - let mut sql = String::new(); - helper::sql_insert_with_placeholders( - table, - columns, - &mut SQLitePlaceholderSource::new(), - &mut sql, - ); - if cfg!(feature = "log") { - debug!("insert sql {}", sql); - } - self.execute(&sql, rusqlite::params_from_iter(values))?; - let pk: SqlVal = self.query_row_and_then( - &format!( - "SELECT {} FROM {} WHERE ROWID = last_insert_rowid()", - pkcol.name(), - table - ), - [], - |row| sql_val_from_rusqlite(row.get_ref_unwrap(0), pkcol), - )?; - Ok(pk) - } - fn insert_only(&self, table: &str, columns: &[Column], values: &[SqlValRef<'_>]) -> Result<()> { - let mut sql = String::new(); - helper::sql_insert_with_placeholders( - table, - columns, - &mut SQLitePlaceholderSource::new(), - &mut sql, - ); - if cfg!(feature = "log") { - debug!("insert sql {}", sql); - } - self.execute(&sql, rusqlite::params_from_iter(values))?; - Ok(()) - } - fn insert_or_replace( - &self, - table: &str, - columns: &[Column], - _pkcol: &Column, - values: &[SqlValRef], - ) -> Result<()> { - let mut sql = String::new(); - sql_insert_or_update(table, columns, &mut sql); - self.execute(&sql, rusqlite::params_from_iter(values))?; - Ok(()) - } - fn update( - &self, - table: &str, - pkcol: Column, - pk: SqlValRef, - columns: &[Column], - values: &[SqlValRef<'_>], - ) -> Result<()> { - let mut sql = String::new(); - helper::sql_update_with_placeholders( - table, - pkcol, - columns, - &mut SQLitePlaceholderSource::new(), - &mut sql, - ); - let placeholder_values = [values, &[pk]].concat(); - if cfg!(feature = "log") { - debug!("update sql {}", sql); - } - self.execute(&sql, rusqlite::params_from_iter(placeholder_values))?; - Ok(()) - } - fn delete_where(&self, table: &str, expr: BoolExpr) -> Result { - let mut sql = String::new(); - let mut values: Vec = Vec::new(); - write!(&mut sql, "DELETE FROM {} WHERE ", table).unwrap(); - sql_for_expr( - query::Expr::Condition(Box::new(expr)), - &mut values, - &mut SQLitePlaceholderSource::new(), - &mut sql, - ); - let cnt = self.execute(&sql, rusqlite::params_from_iter(values))?; - Ok(cnt) - } - fn has_table(&self, table: &str) -> Result { - let mut stmt = - self.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?;")?; - let mut rows = stmt.query([table])?; - Ok(rows.next()?.is_some()) - } -} - -struct SqliteTransaction<'c> { - trans: std::sync::Mutex>>, -} -impl<'c> SqliteTransaction<'c> { - fn new(trans: rusqlite::Transaction<'c>) -> Self { - SqliteTransaction { trans: Some(trans) } - } - fn get(&self) -> Result<&rusqlite::Transaction<'c>> { - match &self.trans { - None => Err(Self::already_consumed()), - Some(trans) => Ok(trans), - } - } - fn wrapped_connection_methods(&self) -> Result<&rusqlite::Connection> { - Ok(self.get()?.deref()) - } - fn already_consumed() -> Error { - Error::Internal("transaction has already been consumed".to_string()) - } -} -connection_method_wrapper!(SqliteTransaction<'_>); - -#[async_trait] -impl<'c> BackendTransaction<'c> for SqliteTransaction<'c> { - fn commit(&mut self) -> Result<()> { - match self.trans.take().take() { - None => Err(Self::already_consumed()), - Some(trans) => Ok(trans.commit()?), - } - } - async fn rollback(&mut self) -> Result<()> { - match self.trans.take() { - None => Err(Self::already_consumed()), - Some(trans) => Ok(trans.rollback()?), - } - } - // Workaround for https://github.com/rust-lang/rfcs/issues/2765 - fn connection_methods(&self) -> &dyn ConnectionMethods { - self - } - fn connection_methods_mut(&mut self) -> &mut dyn ConnectionMethods { - self - } -} - -impl rusqlite::ToSql for SqlVal { - fn to_sql(&self) -> rusqlite::Result> { - Ok(sqlvalref_to_sqlite(&self.as_ref())) - } -} - -impl<'a> rusqlite::ToSql for SqlValRef<'a> { - fn to_sql<'b>(&'b self) -> rusqlite::Result> { - Ok(sqlvalref_to_sqlite(self)) - } -} - -fn sqlvalref_to_sqlite<'a, 'b>(valref: &'b SqlValRef<'a>) -> rusqlite::types::ToSqlOutput<'a> { - use rusqlite::types::{ToSqlOutput::Borrowed, ToSqlOutput::Owned, Value, ValueRef}; - use SqlValRef::*; - match valref { - Bool(b) => Owned(Value::Integer(*b as i64)), - Int(i) => Owned(Value::Integer(*i as i64)), - BigInt(i) => Owned(Value::Integer(*i)), - Real(r) => Owned(Value::Real(*r)), - Text(t) => Borrowed(ValueRef::Text(t.as_bytes())), - Blob(b) => Borrowed(ValueRef::Blob(b)), - #[cfg(feature = "datetime")] - Timestamp(dt) => { - let f = dt.format(SQLITE_DT_FORMAT); - Owned(Value::Text(f.to_string())) - } - Null => Owned(Value::Null), - Custom(_) => panic!("Custom types not supported in sqlite"), - } -} - -#[pin_project] -struct QueryAdapterInner<'a> { - stmt: rusqlite::Statement<'a>, - // will always be Some when the constructor has finished. We use an option only to get the - // stmt in place before we can reference it. - rows: Option>, -} - -impl<'a> QueryAdapterInner<'a> { - fn new(stmt: rusqlite::Statement<'a>, params: impl rusqlite::Params) -> Result>> { - let mut q = Box::pin(QueryAdapterInner { stmt, rows: None }); - unsafe { - //Soundness: we pin a QueryAdapterInner value containing - // both the stmt and the rows referencing the statement - // together. It is not possible to drop/move the stmt without - // bringing the referencing rows along with it. - let q_ref = Pin::get_unchecked_mut(Pin::as_mut(&mut q)); - let stmt_ref: *mut rusqlite::Statement<'a> = &mut q_ref.stmt; - q_ref.rows = Some((*stmt_ref).query(params)?) - } - Ok(q) - } - - fn next<'b>(self: Pin<&'b mut Self>) -> Result>> { - let this = self.project(); - let rows: &mut rusqlite::Rows<'a> = this.rows.as_mut().unwrap(); - Ok(rows.next()?) - } - - fn current(self: Pin<&Self>) -> Option<&rusqlite::Row> { - let this = self.project_ref(); - this.rows.as_ref().unwrap().get() - } -} - -struct QueryAdapter<'a> { - inner: Pin>>, -} -impl<'a> QueryAdapter<'a> { - fn new(stmt: rusqlite::Statement<'a>, params: impl rusqlite::Params) -> Result { - Ok(QueryAdapter { - inner: QueryAdapterInner::new(stmt, params)?, - }) - } -} - -impl<'a> BackendRows for QueryAdapter<'a> { - fn next<'b>(&'b mut self) -> Result> { - Ok(self - .inner - .as_mut() - .next()? - .map(|row| row as &dyn BackendRow)) - } - fn current<'b>(&'b self) -> Option<&'b (dyn BackendRow + 'b)> { - self.inner - .as_ref() - .current() - .map(|row| row as &dyn BackendRow) - } -} - -impl BackendRow for rusqlite::Row<'_> { - fn get(&self, idx: usize, ty: SqlType) -> Result { - sql_valref_from_rusqlite(self.get_ref(idx)?, &ty) - } - fn len(&self) -> usize { - self.as_ref().column_count() - } -} - -fn sql_for_expr( - expr: query::Expr, - values: &mut Vec, - pls: &mut SQLitePlaceholderSource, - w: &mut W, -) where - W: Write, -{ - helper::sql_for_expr(expr, sql_for_expr, values, pls, w) -} - -fn sql_val_from_rusqlite(val: rusqlite::types::ValueRef, col: &Column) -> Result { - sql_valref_from_rusqlite(val, col.ty()).map(|v| v.into()) -} - -fn sql_valref_from_rusqlite<'a>( - val: rusqlite::types::ValueRef<'a>, - ty: &SqlType, -) -> Result> { - if let rusqlite::types::ValueRef::Null = val { - return Ok(SqlValRef::Null); - } - Ok(match ty { - SqlType::Bool => SqlValRef::Bool(val.as_i64()? != 0), - SqlType::Int => SqlValRef::Int(val.as_i64()? as i32), - SqlType::BigInt => SqlValRef::BigInt(val.as_i64()?), - SqlType::Real => SqlValRef::Real(val.as_f64()?), - SqlType::Text => SqlValRef::Text(val.as_str()?), - #[cfg(feature = "datetime")] - SqlType::Timestamp => SqlValRef::Timestamp(NaiveDateTime::parse_from_str( - val.as_str()?, - SQLITE_DT_FORMAT, - )?), - SqlType::Blob => SqlValRef::Blob(val.as_blob()?), - SqlType::Custom(v) => { - return Err(Error::IncompatibleCustomT(v.deref().clone(), BACKEND_NAME)) - } - }) -} - -fn sql_for_op(current: &mut ADB, op: &Operation) -> Result { - match op { - Operation::AddTable(table) => Ok(create_table(table, false)), - Operation::AddTableIfNotExists(table) => Ok(create_table(table, true)), - Operation::RemoveTable(name) => Ok(drop_table(name)), - Operation::AddColumn(tbl, col) => add_column(tbl, col), - Operation::RemoveColumn(tbl, name) => Ok(remove_column(current, tbl, name)), - Operation::ChangeColumn(tbl, old, new) => Ok(change_column(current, tbl, old, Some(new))), - } -} - -fn create_table(table: &ATable, allow_exists: bool) -> String { - let coldefs = table - .columns - .iter() - .map(define_column) - .collect::>() - .join(",\n"); - let modifier = if allow_exists { "IF NOT EXISTS " } else { "" }; - format!("CREATE TABLE {}{} (\n{}\n);", modifier, table.name, coldefs) -} - -fn define_column(col: &AColumn) -> String { - let mut constraints: Vec = Vec::new(); - if !col.nullable() { - constraints.push("NOT NULL".to_string()); - } - if col.is_pk() { - constraints.push("PRIMARY KEY".to_string()); - } - if col.is_auto() && !col.is_pk() { - // integer primary key is automatically an alias for ROWID, - // and we only allow auto on integer types - constraints.push("AUTOINCREMENT".to_string()); - } - if col.unique() { - constraints.push("UNIQUE".to_string()); - } - format!( - "{} {} {}", - &col.name(), - col_sqltype(col), - constraints.join(" ") - ) -} - -fn col_sqltype(col: &AColumn) -> Cow { - match col.typeid() { - Ok(TypeIdentifier::Ty(ty)) => Cow::Borrowed(sqltype(&ty)), - Ok(TypeIdentifier::Name(name)) => Cow::Owned(name), - // sqlite doesn't actually require that the column type be - // specified - Err(_) => Cow::Borrowed(""), - } -} - -fn sqltype(ty: &SqlType) -> &'static str { - match ty { - SqlType::Bool => "INTEGER", - SqlType::Int => "INTEGER", - SqlType::BigInt => "INTEGER", - SqlType::Real => "REAL", - SqlType::Text => "TEXT", - #[cfg(feature = "datetime")] - SqlType::Timestamp => "TEXT", - SqlType::Blob => "BLOB", - SqlType::Custom(_) => panic!("Custom types not supported by sqlite backend"), - } -} - -fn drop_table(name: &str) -> String { - format!("DROP TABLE {};", name) -} - -fn add_column(tbl_name: &str, col: &AColumn) -> Result { - let default: SqlVal = helper::column_default(col)?; - Ok(format!( - "ALTER TABLE {} ADD COLUMN {} DEFAULT {};", - tbl_name, - define_column(col), - helper::sql_literal_value(default)? - )) -} - -fn remove_column(current: &mut ADB, tbl_name: &str, name: &str) -> String { - let old = current - .get_table(tbl_name) - .and_then(|table| table.column(name)) - .cloned(); - match old { - Some(col) => change_column(current, tbl_name, &col, None), - None => { - crate::warn!( - "Cannot remove column {} that does not exist from table {}", - name, - tbl_name - ); - "".to_string() - } - } -} - -fn copy_table(old: &ATable, new: &ATable) -> String { - let column_names = new - .columns - .iter() - .map(|col| col.name()) - .collect::>() - .join(", "); - format!( - "INSERT INTO {} SELECT {} FROM {};", - &new.name, column_names, &old.name - ) -} - -fn tmp_table_name(name: &str) -> String { - format!("{}__butane_tmp", name) -} - -fn change_column( - current: &mut ADB, - tbl_name: &str, - old: &AColumn, - new: Option<&AColumn>, -) -> String { - let table = current.get_table(tbl_name); - if table.is_none() { - crate::warn!( - "Cannot alter column {} from table {} that does not exist", - &old.name(), - tbl_name - ); - return "".to_string(); - } - let old_table = table.unwrap(); - let mut new_table = old_table.clone(); - new_table.name = tmp_table_name(&new_table.name); - match new { - Some(col) => new_table.replace_column(col.clone()), - None => new_table.remove_column(old.name()), - } - let stmts: [&str; 4] = [ - &create_table(&new_table, false), - ©_table(old_table, &new_table), - &drop_table(&old_table.name), - &format!("ALTER TABLE {} RENAME TO {};", &new_table.name, tbl_name), - ]; - let result = stmts.join("\n"); - new_table.name = old_table.name.clone(); - current.replace_table(new_table); - result -} - -pub fn sql_insert_or_update(table: &str, columns: &[Column], w: &mut impl Write) { - write!(w, "INSERT OR REPLACE ").unwrap(); - write!(w, "INTO {} (", table).unwrap(); - helper::list_columns(columns, w); - write!(w, ") VALUES (").unwrap(); - columns.iter().fold("", |sep, _| { - write!(w, "{}?", sep).unwrap(); - ", " - }); - write!(w, ")").unwrap(); -} - -struct SQLitePlaceholderSource {} -impl SQLitePlaceholderSource { - fn new() -> Self { - SQLitePlaceholderSource {} - } -} -impl helper::PlaceholderSource for SQLitePlaceholderSource { - fn next_placeholder(&mut self) -> Cow { - // sqlite placeholder is always a question mark. - Cow::Borrowed("?") - } -} diff --git a/butane_core/src/lib.rs b/butane_core/src/lib.rs index 2548f436..d354f859 100644 --- a/butane_core/src/lib.rs +++ b/butane_core/src/lib.rs @@ -100,6 +100,7 @@ pub trait DataObject: DataResult { .into_iter() .nth(0) .ok_or(Error::NoSuchObject) + .await } /// Save the object to the database. From 5e9c861c37dc1851d5773dda88f93c2c90879f9c Mon Sep 17 00:00:00 2001 From: James Oakley Date: Sat, 14 Jan 2023 21:23:49 -0500 Subject: [PATCH 05/78] Missing feature cfg on a use statement --- butane_core/src/custom.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/butane_core/src/custom.rs b/butane_core/src/custom.rs index 6495cd77..765329a1 100644 --- a/butane_core/src/custom.rs +++ b/butane_core/src/custom.rs @@ -7,6 +7,8 @@ use serde::{Deserialize, Serialize}; use std::fmt; + +#[cfg(feature = "pg")] use tokio_postgres as postgres; /// For use with [SqlType::Custom](crate::SqlType) From e59707bac368e4ed2a54d8d7283dcd26ee07b185 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Sat, 14 Jan 2023 21:24:21 -0500 Subject: [PATCH 06/78] Fix methods which do not need to be async --- butane_core/src/migrations/mod.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/butane_core/src/migrations/mod.rs b/butane_core/src/migrations/mod.rs index 928c423b..80ad330d 100644 --- a/butane_core/src/migrations/mod.rs +++ b/butane_core/src/migrations/mod.rs @@ -19,8 +19,8 @@ mod fs; mod fsmigrations; pub use fsmigrations::{FsMigration, FsMigrations}; mod memmigrations; -pub use memmigrations::{MemMigration, MemMigrations}; use async_trait::async_trait; +pub use memmigrations::{MemMigration, MemMigrations}; /// A collection of migrations. #[async_trait] @@ -77,7 +77,10 @@ pub trait Migrations { /// Get the last migration that has been applied to the database or None /// if no migrations have been applied - async fn last_applied_migration(&self, conn: &impl ConnectionMethods) -> Result> { + async fn last_applied_migration( + &self, + conn: &impl ConnectionMethods, + ) -> Result> { if !conn.has_table(ButaneMigration::TABLE)? { return Ok(None); } @@ -89,7 +92,8 @@ pub trait Migrations { None, None, None, - ).await? + ) + .await? .mapped(ButaneMigration::from_row) .collect()?; @@ -251,7 +255,7 @@ impl DataResult for ButaneMigration { }) } - async fn query() -> query::Query { + fn query() -> query::Query { query::Query::new("butane_migrations") } } From 053f29d7bc3f3f74e8e093568a34c8ec3db6be21 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Sat, 14 Jan 2023 21:24:36 -0500 Subject: [PATCH 07/78] Support async in Many --- butane_core/src/many.rs | 63 +++++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/butane_core/src/many.rs b/butane_core/src/many.rs index c6969f69..be537c92 100644 --- a/butane_core/src/many.rs +++ b/butane_core/src/many.rs @@ -126,32 +126,45 @@ where /// Loads the values referred to by this foreign key from the /// database if necessary and returns a reference to them. - pub fn load(&self, conn: &impl ConnectionMethods) -> Result> { - let vals: Result<&Vec> = self.all_values.get_or_try_init(|| { - //if we don't have an owner then there are no values - let owner: &SqlVal = match &self.owner { - Some(o) => o, - None => return Ok(Vec::new()), - }; - let mut vals = T::query() - .filter(BoolExpr::Subquery { - col: T::PKCOL, - tbl2: self.item_table.clone(), - tbl2_col: "has", - expr: Box::new(BoolExpr::Eq("owner", Expr::Val(owner.clone()))), - }) - .load(conn)?; - // Now add in the values for things not saved to the db yet - if !self.new_values.is_empty() { - vals.append( - &mut T::query() - .filter(BoolExpr::In(T::PKCOL, self.new_values.clone())) - .load(conn)?, - ); + pub async fn load(&self, conn: &impl ConnectionMethods) -> Result> { + let vals: Option<&Vec> = self.all_values.get(); + if let Some(vals) = vals { + // We already cached the value + return Ok(vals.iter()); + } + + //////////// + // load the value from the database + + let mut vals: Vec = match &self.owner { + None => Vec::new(), //if we don't have an owner then there are no values + Some(owner) => { + T::query() + .filter(BoolExpr::Subquery { + col: T::PKCOL, + tbl2: self.item_table.clone(), + tbl2_col: "has", + expr: Box::new(BoolExpr::Eq("owner", Expr::Val(owner.clone()))), + }) + .load(conn) + .await? } - Ok(vals) - }); - vals.map(|v| v.iter()) + }; + // Now add in the values for things not saved to the db yet (if any) + if !self.new_values.is_empty() { + vals.append( + &mut T::query() + .filter(BoolExpr::In(T::PKCOL, self.new_values.clone())) + .load(conn) + .await?, + ); + } + + // cache what we loaded (and added to) + Ok(match self.all_values.try_insert(vals) { + Ok(v) => v.iter(), + Err((existing, v)) => existing.iter(), + }) } pub fn columns(&self) -> [Column; 2] { [ From 63dbd311befcaac47f8a25b329a9902e4bd6b963 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Sat, 14 Jan 2023 21:25:26 -0500 Subject: [PATCH 08/78] Update Cargo.lock --- Cargo.lock | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index da52a0f9..4d840ba6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -146,12 +146,12 @@ dependencies = [ "log", "once_cell", "paste", - "postgres", "proc-macro2 1.0.47", "quote 1.0.21", "r2d2", "rusqlite", "serde_json", + "tokio-postgres", "uuid", ] @@ -183,6 +183,7 @@ dependencies = [ name = "butane_core" version = "0.5.0" dependencies = [ + "async-trait", "bytes", "cfg-if", "chrono", @@ -194,7 +195,6 @@ dependencies = [ "native-tls", "once_cell", "pin-project", - "postgres", "postgres-native-tls", "proc-macro2 1.0.47", "quote 1.0.21", @@ -205,6 +205,8 @@ dependencies = [ "serde_json", "syn 1.0.103", "thiserror", + "tokio", + "tokio-postgres", "uuid", ] @@ -1027,20 +1029,6 @@ version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" -[[package]] -name = "postgres" -version = "0.19.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960c214283ef8f0027974c03e9014517ced5db12f021a9abb66185a5751fab0a" -dependencies = [ - "bytes", - "fallible-iterator", - "futures-util", - "log", - "tokio", - "tokio-postgres", -] - [[package]] name = "postgres-native-tls" version = "0.5.0" @@ -1504,9 +1492,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.21.2" +version = "1.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" +checksum = "1d9f76183f91ecfb55e1d7d5602bd1d979e38a3a522fe900241cf195624d67ae" dependencies = [ "autocfg", "bytes", @@ -1515,7 +1503,7 @@ dependencies = [ "mio", "pin-project-lite", "socket2", - "winapi", + "windows-sys 0.42.0", ] [[package]] From 9900a9b51eeabe600a4e8da345d5ae4327b34bdc Mon Sep 17 00:00:00 2001 From: James Oakley Date: Sat, 14 Jan 2023 21:27:16 -0500 Subject: [PATCH 09/78] Apply rustfmt --- butane/tests/basic.rs | 4 ++-- butane/tests/custom_pg.rs | 2 +- butane/tests/nullable.rs | 2 +- butane_core/src/db/connmethods.rs | 2 +- butane_core/src/db/macros.rs | 3 ++- butane_core/src/db/mod.rs | 4 +++- butane_core/src/db/pg.rs | 31 ++++++++++++++----------------- butane_core/src/lib.rs | 8 +++++--- butane_core/src/query/mod.rs | 17 +++++++++++++---- 9 files changed, 42 insertions(+), 31 deletions(-) diff --git a/butane/tests/basic.rs b/butane/tests/basic.rs index 15a5c67c..07d94d89 100644 --- a/butane/tests/basic.rs +++ b/butane/tests/basic.rs @@ -3,10 +3,10 @@ use butane::prelude::*; use butane::{butane_type, find, model, query}; use butane::{ForeignKey, ObjectState}; use paste; -#[cfg(feature = "pg")] -use tokio_postgres as postgres; #[cfg(feature = "sqlite")] use rusqlite; +#[cfg(feature = "pg")] +use tokio_postgres as postgres; mod common; diff --git a/butane/tests/custom_pg.rs b/butane/tests/custom_pg.rs index b00f70f0..7873172f 100644 --- a/butane/tests/custom_pg.rs +++ b/butane/tests/custom_pg.rs @@ -9,8 +9,8 @@ mod custom_pg { use butane::{butane_type, db::Connection, model, ObjectState}; use butane::{FieldType, FromSql, SqlType, SqlVal, SqlValRef, ToSql}; use geo_types; - use tokio_postgres as postgres; use std::result::Result; + use tokio_postgres as postgres; // newtype so we can implement traits for it. #[butane_type(Custom(POINT))] diff --git a/butane/tests/nullable.rs b/butane/tests/nullable.rs index 45372a41..05e31dcc 100644 --- a/butane/tests/nullable.rs +++ b/butane/tests/nullable.rs @@ -1,7 +1,7 @@ -use paste; use butane::db::Connection; use butane::prelude::*; use butane::{model, query}; +use paste; mod common; diff --git a/butane_core/src/db/connmethods.rs b/butane_core/src/db/connmethods.rs index 5cffddbe..b4c9f57d 100644 --- a/butane_core/src/db/connmethods.rs +++ b/butane_core/src/db/connmethods.rs @@ -3,9 +3,9 @@ use crate::query::{BoolExpr, Expr, Order}; use crate::{Result, SqlType, SqlVal, SqlValRef}; +use async_trait::async_trait; use std::ops::{Deref, DerefMut}; use std::vec::Vec; -use async_trait::async_trait; /// Methods available on a database connection. Most users do not need /// to call these methods directly and will instead use methods on diff --git a/butane_core/src/db/macros.rs b/butane_core/src/db/macros.rs index a64fb9f7..088a540b 100644 --- a/butane_core/src/db/macros.rs +++ b/butane_core/src/db/macros.rs @@ -16,7 +16,8 @@ macro_rules! connection_method_wrapper { sort: Option<&[$crate::query::Order]>, ) -> Result> { self.wrapped_connection_methods()? - .query(table, columns, expr, limit, offset, sort).await + .query(table, columns, expr, limit, offset, sort) + .await } fn insert_returning_pk( &self, diff --git a/butane_core/src/db/mod.rs b/butane_core/src/db/mod.rs index a8e27b0e..2d95e73c 100644 --- a/butane_core/src/db/mod.rs +++ b/butane_core/src/db/mod.rs @@ -14,19 +14,21 @@ use crate::query::BoolExpr; use crate::{migrations::adb, Error, Result, SqlVal, SqlValRef}; +use async_trait::async_trait; use serde::{Deserialize, Serialize}; use std::borrow::Cow; use std::fs; use std::io::Write; use std::ops::{Deref, DerefMut}; use std::path::Path; -use async_trait::async_trait; mod connmethods; mod helper; mod macros; #[cfg(feature = "pg")] pub mod pg; +#[cfg(feature = "sqlite")] +pub mod sqlite; #[cfg(feature = "r2d2")] mod r2; diff --git a/butane_core/src/db/pg.rs b/butane_core/src/db/pg.rs index 8ed088fb..756a0eef 100644 --- a/butane_core/src/db/pg.rs +++ b/butane_core/src/db/pg.rs @@ -6,14 +6,14 @@ use crate::custom::{SqlTypeCustom, SqlValRefCustom}; use crate::migrations::adb::{AColumn, ATable, Operation, TypeIdentifier, ADB}; use crate::{debug, query}; use crate::{Result, SqlType, SqlVal, SqlValRef}; +use async_trait::async_trait; use bytes::BufMut; #[cfg(feature = "datetime")] use chrono::NaiveDateTime; -use tokio_postgres::GenericClient; -use tokio_postgres as postgres; use std::fmt::Write; -use async_trait::async_trait; use tokio; +use tokio_postgres as postgres; +use tokio_postgres::GenericClient; /// The name of the postgres backend. pub const BACKEND_NAME: &str = "pg"; @@ -64,7 +64,10 @@ pub struct PgConnection { impl PgConnection { async fn open(params: &str) -> Result { let (client, conn_handle) = Self::connect(params).await?; - Ok( Self { client, conn_handle }) + Ok(Self { + client, + conn_handle, + }) } async fn connect(params: &str) -> Result<(postgres::Client, tokio::task::JoinHandle<()>)> { cfg_if::cfg_if! { @@ -179,7 +182,8 @@ where let types: Vec = values.iter().map(pgtype_for_val).collect(); let stmt = self .cell()? - .prepare_typed(&sqlquery, types.as_ref()).await?; + .prepare_typed(&sqlquery, types.as_ref()) + .await?; // todo avoid intermediate vec? let rowvec: Vec = self .cell()? @@ -229,8 +233,7 @@ where &mut sql, ); let params: Vec<&DynToSqlPg> = values.iter().map(|v| v as &DynToSqlPg).collect(); - self.cell()? - .execute(sql.as_str(), params.as_slice())?; + self.cell()?.execute(sql.as_str(), params.as_slice())?; Ok(()) } fn insert_or_replace<'a>( @@ -243,8 +246,7 @@ where let mut sql = String::new(); sql_insert_or_replace_with_placeholders(table, columns, pkcol, &mut sql); let params: Vec<&DynToSqlPg> = values.iter().map(|v| v as &DynToSqlPg).collect(); - self.cell()? - .execute(sql.as_str(), params.as_slice())?; + self.cell()?.execute(sql.as_str(), params.as_slice())?; Ok(()) } fn update( @@ -271,8 +273,7 @@ where if cfg!(feature = "log") { debug!("update sql {}", sql); } - self.cell()? - .execute(sql.as_str(), params.as_slice())?; + self.cell()?.execute(sql.as_str(), params.as_slice())?; Ok(()) } fn delete_where(&self, table: &str, expr: BoolExpr) -> Result { @@ -286,9 +287,7 @@ where &mut sql, ); let params: Vec<&DynToSqlPg> = values.iter().map(|v| v as &DynToSqlPg).collect(); - let cnt = self - .cell()? - .execute(sql.as_str(), params.as_slice())?; + let cnt = self.cell()?.execute(sql.as_str(), params.as_slice())?; Ok(cnt as usize) } fn has_table(&self, table: &str) -> Result { @@ -306,9 +305,7 @@ struct PgTransaction<'c> { } impl<'c> PgTransaction<'c> { fn new(trans: postgres::Transaction<'c>) -> Self { - PgTransaction { - trans: Some(trans), - } + PgTransaction { trans: Some(trans) } } fn get(&self) -> Result<&postgres::Transaction<'c>> { match self.trans { diff --git a/butane_core/src/lib.rs b/butane_core/src/lib.rs index d354f859..72bf1216 100644 --- a/butane_core/src/lib.rs +++ b/butane_core/src/lib.rs @@ -1,11 +1,11 @@ #![allow(clippy::iter_nth_zero)] #![allow(clippy::upper_case_acronyms)] //grandfathered, not going to break API to rename +use async_trait::async_trait; use serde::{Deserialize, Serialize}; use std::borrow::Borrow; use std::cmp::{Eq, PartialEq}; use std::default::Default; use thiserror::Error as ThisError; -use async_trait::async_trait; pub mod codegen; pub mod custom; @@ -91,12 +91,14 @@ pub trait DataObject: DataResult { Self::PKType: Sync, { let query = ::query().await; - query.filter(query::BoolExpr::Eq( + query + .filter(query::BoolExpr::Eq( Self::PKCOL, query::Expr::Val(id.borrow().to_sql()), )) .limit(1) - .load(conn).await? + .load(conn) + .await? .into_iter() .nth(0) .ok_or(Error::NoSuchObject) diff --git a/butane_core/src/query/mod.rs b/butane_core/src/query/mod.rs index 1b64e4ac..a5ad352a 100644 --- a/butane_core/src/query/mod.rs +++ b/butane_core/src/query/mod.rs @@ -180,7 +180,8 @@ impl Query { /// Executes the query against `conn` and returns the first result (if any). pub async fn load_first(self, conn: &impl ConnectionMethods) -> Result> { - conn.query(&self.table, T::COLUMNS, self.filter, Some(1), None, None).await? + conn.query(&self.table, T::COLUMNS, self.filter, Some(1), None, None) + .await? .mapped(T::from_row) .nth(0) } @@ -192,9 +193,17 @@ impl Query { } else { Some(self.sort.as_slice()) }; - conn.query(&self.table, T::COLUMNS, self.filter, self.limit, self.offset, sort).await? - .mapped(T::from_row) - .collect() + conn.query( + &self.table, + T::COLUMNS, + self.filter, + self.limit, + self.offset, + sort, + ) + .await? + .mapped(T::from_row) + .collect() } /// Executes the query against `conn` and deletes all matching objects. From 0c59454d4c02ff4071371e4b48ac0572a7dc8711 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Sun, 15 Jan 2023 21:00:32 -0500 Subject: [PATCH 10/78] DataResult::query should not be async --- butane_core/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/butane_core/src/lib.rs b/butane_core/src/lib.rs index 72bf1216..9203319c 100644 --- a/butane_core/src/lib.rs +++ b/butane_core/src/lib.rs @@ -63,7 +63,7 @@ pub trait DataResult: Sized { where Self: Sized; /// Create a blank query (matching all rows) for this type. - async fn query() -> Query; + fn query() -> Query; } /// An object in the database. From 2b6c1d68ef1c7b483d904d68e1de50a601fae568 Mon Sep 17 00:00:00 2001 From: Ari Breitkreuz Date: Mon, 23 Jan 2023 20:27:42 +0100 Subject: [PATCH 11/78] more wip --- Cargo.lock | 27 +- butane_core/Cargo.toml | 2 +- butane_core/src/db/connmethods.rs | 18 +- butane_core/src/db/macros.rs | 24 +- butane_core/src/db/mod.rs | 40 +- butane_core/src/db/pg.rs | 125 ++-- butane_core/src/db/sqlite.rs | 618 -------------------- butane_core/src/fkey.rs | 27 +- butane_core/src/lib.rs | 8 +- butane_core/src/many.rs | 25 +- butane_core/src/migrations/fsmigrations.rs | 12 +- butane_core/src/migrations/memmigrations.rs | 5 +- butane_core/src/migrations/migration.rs | 25 +- butane_core/src/migrations/mod.rs | 14 +- butane_core/src/query/mod.rs | 4 +- butane_core/src/sqlval.rs | 2 +- 16 files changed, 187 insertions(+), 789 deletions(-) delete mode 100644 butane_core/src/db/sqlite.rs diff --git a/Cargo.lock b/Cargo.lock index da52a0f9..6666fbda 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -146,12 +146,12 @@ dependencies = [ "log", "once_cell", "paste", - "postgres", "proc-macro2 1.0.47", "quote 1.0.21", "r2d2", "rusqlite", "serde_json", + "tokio-postgres", "uuid", ] @@ -183,6 +183,7 @@ dependencies = [ name = "butane_core" version = "0.5.0" dependencies = [ + "async-trait", "bytes", "cfg-if", "chrono", @@ -192,9 +193,7 @@ dependencies = [ "hex", "log", "native-tls", - "once_cell", "pin-project", - "postgres", "postgres-native-tls", "proc-macro2 1.0.47", "quote 1.0.21", @@ -205,6 +204,8 @@ dependencies = [ "serde_json", "syn 1.0.103", "thiserror", + "tokio", + "tokio-postgres", "uuid", ] @@ -1027,20 +1028,6 @@ version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" -[[package]] -name = "postgres" -version = "0.19.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960c214283ef8f0027974c03e9014517ced5db12f021a9abb66185a5751fab0a" -dependencies = [ - "bytes", - "fallible-iterator", - "futures-util", - "log", - "tokio", - "tokio-postgres", -] - [[package]] name = "postgres-native-tls" version = "0.5.0" @@ -1504,9 +1491,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.21.2" +version = "1.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" +checksum = "597a12a59981d9e3c38d216785b0c37399f6e415e8d0712047620f189371b0bb" dependencies = [ "autocfg", "bytes", @@ -1515,7 +1502,7 @@ dependencies = [ "mio", "pin-project-lite", "socket2", - "winapi", + "windows-sys 0.42.0", ] [[package]] diff --git a/butane_core/Cargo.toml b/butane_core/Cargo.toml index 4586821e..02bf5770 100644 --- a/butane_core/Cargo.toml +++ b/butane_core/Cargo.toml @@ -25,7 +25,6 @@ fallible-iterator = "0.2" fallible-streaming-iterator = "0.1" fs2 = "0.4" # for file locks hex = "0.4" -once_cell="1.5" log = { version="0.4", optional=true } native-tls={ version = "0.2", optional = true } tokio-postgres={ version = "0.7", features=["with-chrono-0_4"], optional = true} @@ -43,3 +42,4 @@ thiserror = "1.0" chrono = { version = "0.4", features=["serde"], optional = true } uuid = {workspace=true, optional=true} async-trait = "0.1" +tokio = { version = "1.24", features=["rt"] } diff --git a/butane_core/src/db/connmethods.rs b/butane_core/src/db/connmethods.rs index 5cffddbe..aeea141e 100644 --- a/butane_core/src/db/connmethods.rs +++ b/butane_core/src/db/connmethods.rs @@ -13,7 +13,7 @@ use async_trait::async_trait; /// implemented by both database connections and transactions. #[async_trait] pub trait ConnectionMethods: Sync { - fn execute(&self, sql: &str) -> Result<()>; + async fn execute(&self, sql: &str) -> Result<()>; async fn query<'a, 'b, 'c: 'a>( &'c self, table: &str, @@ -23,7 +23,7 @@ pub trait ConnectionMethods: Sync { offset: Option, sort: Option<&[Order]>, ) -> Result>; - fn insert_returning_pk( + async fn insert_returning_pk( &self, table: &str, columns: &[Column], @@ -31,16 +31,16 @@ pub trait ConnectionMethods: Sync { values: &[SqlValRef<'_>], ) -> Result; /// Like `insert_returning_pk` but with no return value - fn insert_only(&self, table: &str, columns: &[Column], values: &[SqlValRef<'_>]) -> Result<()>; + async fn insert_only(&self, table: &str, columns: &[Column], values: &[SqlValRef<'_>]) -> Result<()>; /// Insert unless there's a conflict on the primary key column, in which case update - fn insert_or_replace( + async fn insert_or_replace( &self, table: &str, columns: &[Column], pkcol: &Column, values: &[SqlValRef<'_>], ) -> Result<()>; - fn update( + async fn update( &self, table: &str, pkcol: Column, @@ -48,13 +48,13 @@ pub trait ConnectionMethods: Sync { columns: &[Column], values: &[SqlValRef<'_>], ) -> Result<()>; - fn delete(&self, table: &str, pkcol: &'static str, pk: SqlVal) -> Result<()> { - self.delete_where(table, BoolExpr::Eq(pkcol, Expr::Val(pk)))?; + async fn delete(&self, table: &str, pkcol: &'static str, pk: SqlVal) -> Result<()> { + self.delete_where(table, BoolExpr::Eq(pkcol, Expr::Val(pk))).await?; Ok(()) } - fn delete_where(&self, table: &str, expr: BoolExpr) -> Result; + async fn delete_where(&self, table: &str, expr: BoolExpr) -> Result; /// Tests if a table exists in the database. - fn has_table(&self, table: &str) -> Result; + async fn has_table(&self, table: &str) -> Result; } /// Represents a database column. Most users do not need to use this diff --git a/butane_core/src/db/macros.rs b/butane_core/src/db/macros.rs index a64fb9f7..99bf42be 100644 --- a/butane_core/src/db/macros.rs +++ b/butane_core/src/db/macros.rs @@ -3,8 +3,8 @@ macro_rules! connection_method_wrapper { ($ty:path) => { #[async_trait::async_trait] impl ConnectionMethods for $ty { - fn execute(&self, sql: &str) -> Result<()> { - ConnectionMethods::execute(self.wrapped_connection_methods()?, sql) + async fn execute(&self, sql: &str) -> Result<()> { + ConnectionMethods::execute(self.wrapped_connection_methods()?, sql).await } async fn query<'a, 'b, 'c: 'a>( &'c self, @@ -18,7 +18,7 @@ macro_rules! connection_method_wrapper { self.wrapped_connection_methods()? .query(table, columns, expr, limit, offset, sort).await } - fn insert_returning_pk( + async fn insert_returning_pk( &self, table: &str, columns: &[Column], @@ -27,8 +27,9 @@ macro_rules! connection_method_wrapper { ) -> Result { self.wrapped_connection_methods()? .insert_returning_pk(table, columns, pkcol, values) + .await } - fn insert_only( + async fn insert_only( &self, table: &str, columns: &[Column], @@ -36,8 +37,9 @@ macro_rules! connection_method_wrapper { ) -> Result<()> { self.wrapped_connection_methods()? .insert_only(table, columns, values) + .await } - fn insert_or_replace( + async fn insert_or_replace( &self, table: &str, columns: &[Column], @@ -46,8 +48,9 @@ macro_rules! connection_method_wrapper { ) -> Result<()> { self.wrapped_connection_methods()? .insert_or_replace(table, columns, pkcol, values) + .await } - fn update( + async fn update( &self, table: &str, pkcol: Column, @@ -57,12 +60,13 @@ macro_rules! connection_method_wrapper { ) -> Result<()> { self.wrapped_connection_methods()? .update(table, pkcol, pk, columns, values) + .await } - fn delete_where(&self, table: &str, expr: BoolExpr) -> Result { - self.wrapped_connection_methods()?.delete_where(table, expr) + async fn delete_where(&self, table: &str, expr: BoolExpr) -> Result { + self.wrapped_connection_methods()?.delete_where(table, expr).await } - fn has_table(&self, table: &str) -> Result { - self.wrapped_connection_methods()?.has_table(table) + async fn has_table(&self, table: &str) -> Result { + self.wrapped_connection_methods()?.has_table(table).await } } }; diff --git a/butane_core/src/db/mod.rs b/butane_core/src/db/mod.rs index 8f306a1e..3c5fbba6 100644 --- a/butane_core/src/db/mod.rs +++ b/butane_core/src/db/mod.rs @@ -27,8 +27,6 @@ mod helper; mod macros; #[cfg(feature = "pg")] pub mod pg; -#[cfg(feature = "sqlite")] -pub mod sqlite; #[cfg(feature = "r2d2")] mod r2; @@ -47,7 +45,7 @@ pub use connmethods::{ pub trait BackendConnection: ConnectionMethods + Send + 'static { /// Begin a database transaction. The transaction object must be /// used in place of this connection until it is committed and aborted. - fn transaction(&mut self) -> Result; + async fn transaction(&mut self) -> Result; /// Retrieve the backend backend this connection fn backend(&self) -> Box; fn backend_name(&self) -> &'static str; @@ -62,8 +60,8 @@ pub struct Connection { conn: Box, } impl Connection { - pub fn execute(&mut self, sql: impl AsRef) -> Result<()> { - self.conn.execute(sql.as_ref()) + pub async fn execute(&mut self, sql: impl AsRef) -> Result<()> { + self.conn.execute(sql.as_ref()).await } // For use with connection_method_wrapper macro #[allow(clippy::unnecessary_wraps)] @@ -71,9 +69,10 @@ impl Connection { Ok(self.conn.as_ref()) } } +#[async_trait] impl BackendConnection for Connection { - fn transaction(&mut self) -> Result { - self.conn.transaction() + async fn transaction(&mut self) -> Result { + self.conn.transaction().await } fn backend(&self) -> Box { self.conn.backend() @@ -131,12 +130,14 @@ fn conn_complete_if_dir(path: &Path) -> Cow { } /// Database backend. A boxed implementation can be returned by name via [get_backend][crate::db::get_backend]. -pub trait Backend { +#[async_trait] +pub trait Backend: Sync { fn name(&self) -> &'static str; fn create_migration_sql(&self, current: &adb::ADB, ops: Vec) -> Result; - fn connect(&self, conn_str: &str) -> Result; + async fn connect(&self, conn_str: &str) -> Result; } +#[async_trait] impl Backend for Box { fn name(&self) -> &'static str { self.deref().name() @@ -144,16 +145,14 @@ impl Backend for Box { fn create_migration_sql(&self, current: &adb::ADB, ops: Vec) -> Result { self.deref().create_migration_sql(current, ops) } - fn connect(&self, conn_str: &str) -> Result { - self.deref().connect(conn_str) + async fn connect(&self, conn_str: &str) -> Result { + self.deref().connect(conn_str).await } } /// Find a backend by name. pub fn get_backend(name: &str) -> Option> { match name { - #[cfg(feature = "sqlite")] - sqlite::BACKEND_NAME => Some(Box::new(sqlite::SQLiteBackend::new())), #[cfg(feature = "pg")] pg::BACKEND_NAME => Some(Box::new(pg::PgBackend::new())), _ => None, @@ -162,19 +161,20 @@ pub fn get_backend(name: &str) -> Option> { /// Connect to a database. For non-boxed connections, see individual /// [Backend][crate::db::Backend] implementations. -pub fn connect(spec: &ConnectionSpec) -> Result { +pub async fn connect(spec: &ConnectionSpec) -> Result { get_backend(&spec.backend_name) .ok_or_else(|| Error::UnknownBackend(spec.backend_name.clone()))? .connect(&spec.conn_str) + .await } #[async_trait] -trait BackendTransaction<'c>: ConnectionMethods { +trait BackendTransaction<'c>: ConnectionMethods + Send { /// Commit the transaction Unfortunately because we use this as a /// trait object, we can't consume self. It should be understood /// that no methods should be called after commit. This trait is /// not public, and that behavior is enforced by Transaction - fn commit(&mut self) -> Result<()>; + async fn commit(&mut self) -> Result<()>; /// Roll back the transaction. Same comment about consuming self as above. async fn rollback(&mut self) -> Result<()>; @@ -197,12 +197,12 @@ impl<'c> Transaction<'c> { Transaction { trans } } /// Commit the transaction - pub fn commit(mut self) -> Result<()> { - self.trans.deref_mut().commit() + pub async fn commit(mut self) -> Result<()> { + self.trans.commit().await } /// Roll back the transaction. Equivalent to dropping it. - pub fn rollback(mut self) -> Result<()> { - self.trans.deref_mut().rollback() + pub async fn rollback(mut self) -> Result<()> { + self.trans.deref_mut().rollback().await } // For use with connection_method_wrapper macro #[allow(clippy::unnecessary_wraps)] diff --git a/butane_core/src/db/pg.rs b/butane_core/src/db/pg.rs index c221a67c..efdd9297 100644 --- a/butane_core/src/db/pg.rs +++ b/butane_core/src/db/pg.rs @@ -11,9 +11,9 @@ use bytes::BufMut; use chrono::NaiveDateTime; use tokio_postgres::GenericClient; use tokio_postgres as postgres; -use std::cell::RefCell; use std::fmt::Write; use async_trait::async_trait; +use tokio; /// The name of the postgres backend. pub const BACKEND_NAME: &str = "pg"; @@ -27,10 +27,12 @@ impl PgBackend { } } impl PgBackend { - fn connect(&self, params: &str) -> Result { - PgConnection::open(params) + async fn connect(&self, params: &str) -> Result { + PgConnection::open(params).await } } + +#[async_trait] impl Backend for PgBackend { fn name(&self) -> &'static str { BACKEND_NAME @@ -45,24 +47,28 @@ impl Backend for PgBackend { .join("\n")) } - fn connect(&self, path: &str) -> Result { + async fn connect(&self, path: &str) -> Result { Ok(Connection { - conn: Box::new(self.connect(path)?), + conn: Box::new(self.connect(path).await?), }) } } +// type PgConnHandle = tokio::task::JoinHandle>; + /// Pg database connection. pub struct PgConnection { - conn: postgres::Client, + client: postgres::Client, + // Save the handle to the task running the connection to keep it alive + conn_handle: tokio::task::JoinHandle<()>, } + impl PgConnection { - fn open(params: &str) -> Result { - Ok(PgConnection { - conn: Self::connect(params)?, - }) + async fn open(params: &str) -> Result { + let (client, conn_handle) = Self::connect(params).await?; + Ok( Self { client, conn_handle }) } - fn connect(params: &str) -> Result { + async fn connect(params: &str) -> Result<(postgres::Client, tokio::task::JoinHandle<()>)> { cfg_if::cfg_if! { if #[cfg(feature = "tls")] { let connector = native_tls::TlsConnector::new()?; @@ -71,18 +77,27 @@ impl PgConnection { let connector = postgres::NoTls; } } - Ok(postgres::Client::connect(params, connector)?) + let (client, conn) = postgres::connect(params, connector).await?; + let conn_handle = tokio::spawn(async move { + if let Err(e) = conn.await { + // TODO don't panic + panic!() + } + }); + Ok((client, conn_handle)) } } impl PgConnectionLike for PgConnection { type Client = postgres::Client; - fn cell(&self) -> Result<&RefCell> { - Ok(&self.conn) + fn client(&self) -> Result<&Self::Client> { + Ok(&self.client) } } + +#[async_trait] impl BackendConnection for PgConnection { - fn transaction(&mut self) -> Result> { - let trans: postgres::Transaction<'_> = self.conn.transaction()?; + async fn transaction(&mut self) -> Result> { + let trans: postgres::Transaction<'_> = self.client.transaction().await?; let trans = Box::new(PgTransaction::new(trans)); Ok(Transaction::new(trans)) } @@ -93,7 +108,7 @@ impl BackendConnection for PgConnection { BACKEND_NAME } fn is_closed(&self) -> bool { - self.conn.is_closed() + self.client.is_closed() } } @@ -110,8 +125,8 @@ fn sqlvalref_for_pg_query<'a>(v: &'a SqlValRef<'a>) -> &'a dyn postgres::types:: /// Shared functionality between connection and /// transaction. Implementation detail. Semver exempt. pub trait PgConnectionLike { - type Client: postgres::GenericClient; - fn cell(&self) -> Result; + type Client: postgres::GenericClient + Send; + fn client(&self) -> Result<&Self::Client>; } #[async_trait] @@ -119,11 +134,11 @@ impl ConnectionMethods for T where T: PgConnectionLike + std::marker::Sync, { - fn execute(&self, sql: &str) -> Result<()> { + async fn execute(&self, sql: &str) -> Result<()> { if cfg!(feature = "log") { debug!("execute sql {}", sql); } - self.cell()?.batch_execute(sql.as_ref())?; + self.client()?.batch_execute(sql.as_ref()).await?; Ok(()) } @@ -168,21 +183,20 @@ where let types: Vec = values.iter().map(pgtype_for_val).collect(); let stmt = self - .cell()? + .client()? .prepare_typed(&sqlquery, types.as_ref()).await?; // todo avoid intermediate vec? - let rowvec: Vec = self - .cell()? - .query_raw(&stmt, values.iter().map(sqlval_for_pg_query))? + let rowvec = self + .client()? + .query_raw(&stmt, values.iter().map(sqlval_for_pg_query)).await .map_err(Error::Postgres) .map(|r| { check_columns(&r, columns)?; Ok(r) - }) - .collect()?; + })??; Ok(Box::new(VecRows::new(rowvec))) } - fn insert_returning_pk( + async fn insert_returning_pk( &self, table: &str, columns: &[Column], @@ -203,14 +217,14 @@ where // use query instead of execute so we can get our result back let pk: Option = self - .cell()? - .query_raw(sql.as_str(), values.iter().map(sqlvalref_for_pg_query))? + .client()? + .query_raw(sql.as_str(), values.iter().map(sqlvalref_for_pg_query)).await .map_err(Error::Postgres) - .map(|r| sql_val_from_postgres(&r, 0, pkcol)) + .map(|r| sql_val_from_postgres(&r, 0, pkcol))?? .nth(0)?; pk.ok_or_else(|| Error::Internal("could not get pk".to_string())) } - fn insert_only(&self, table: &str, columns: &[Column], values: &[SqlValRef<'_>]) -> Result<()> { + async fn insert_only(&self, table: &str, columns: &[Column], values: &[SqlValRef<'_>]) -> Result<()> { let mut sql = String::new(); helper::sql_insert_with_placeholders( table, @@ -219,11 +233,11 @@ where &mut sql, ); let params: Vec<&DynToSqlPg> = values.iter().map(|v| v as &DynToSqlPg).collect(); - self.cell()? - .execute(sql.as_str(), params.as_slice())?; + self.client()? + .execute(sql.as_str(), params.as_slice()).await?; Ok(()) } - fn insert_or_replace<'a>( + async fn insert_or_replace<'a>( &self, table: &str, columns: &[Column], @@ -233,11 +247,11 @@ where let mut sql = String::new(); sql_insert_or_replace_with_placeholders(table, columns, pkcol, &mut sql); let params: Vec<&DynToSqlPg> = values.iter().map(|v| v as &DynToSqlPg).collect(); - self.cell()? - .execute(sql.as_str(), params.as_slice())?; + self.client()? + .execute(sql.as_str(), params.as_slice()).await?; Ok(()) } - fn update( + async fn update( &self, table: &str, pkcol: Column, @@ -261,11 +275,11 @@ where if cfg!(feature = "log") { debug!("update sql {}", sql); } - self.cell()? - .execute(sql.as_str(), params.as_slice())?; + self.client()? + .execute(sql.as_str(), params.as_slice()).await?; Ok(()) } - fn delete_where(&self, table: &str, expr: BoolExpr) -> Result { + async fn delete_where(&self, table: &str, expr: BoolExpr) -> Result { let mut sql = String::new(); let mut values: Vec = Vec::new(); write!(&mut sql, "DELETE FROM {} WHERE ", table).unwrap(); @@ -277,16 +291,16 @@ where ); let params: Vec<&DynToSqlPg> = values.iter().map(|v| v as &DynToSqlPg).collect(); let cnt = self - .cell()? - .execute(sql.as_str(), params.as_slice())?; + .client()? + .execute(sql.as_str(), params.as_slice()).await?; Ok(cnt as usize) } - fn has_table(&self, table: &str) -> Result { + async fn has_table(&self, table: &str) -> Result { // future improvement, should be schema-aware let stmt = self - .cell()? - .prepare("SELECT table_name FROM information_schema.tables WHERE table_name=$1;")?; - let rows = self.cell()?.try_borrow_mut()?.query(&stmt, &[&table])?; + .client()? + .prepare("SELECT table_name FROM information_schema.tables WHERE table_name=$1;").await?; + let rows = self.client()?.query(&stmt, &[&table]).await?; Ok(!rows.is_empty()) } } @@ -301,7 +315,10 @@ impl<'c> PgTransaction<'c> { } } fn get(&self) -> Result<&postgres::Transaction<'c>> { - &self.trans.ok_or(Self::already_consumed()) + match self.trans { + Some(x) => Ok(&x), + None => Err(Self::already_consumed()), + } } fn already_consumed() -> Error { Error::Internal("transaction has already been consumed".to_string()) @@ -309,24 +326,24 @@ impl<'c> PgTransaction<'c> { } impl<'c> PgConnectionLike for PgTransaction<'c> { type Client = postgres::Transaction<'c>; - fn cell(&self) -> Result<&Self::Client> { + fn client(&self) -> Result<&Self::Client> { self.get() } } #[async_trait] impl<'c> BackendTransaction<'c> for PgTransaction<'c> { - fn commit(&mut self) -> Result<()> { + async fn commit(&mut self) -> Result<()> { match self.trans.take() { None => Err(Self::already_consumed()), - Some(trans) => Ok(trans.into_inner().commit()?), + Some(trans) => Ok(trans.commit().await?), } } async fn rollback(&mut self) -> Result<()> { match self.trans.take() { None => Err(Self::already_consumed()), - Some(trans) => Ok(trans.into_inner().rollback().await?), + Some(trans) => Ok(trans.rollback().await?), } } // Workaround for https://github.com/rust-lang/rfcs/issues/2765 @@ -451,7 +468,7 @@ impl<'a> postgres::types::FromSql<'a> for SqlValRef<'a> { } } -fn check_columns(row: &postgres::Row, cols: &[Column]) -> Result<()> { +fn check_columns(row: &postgres::RowStream, cols: &[Column]) -> Result<()> { if cols.len() != row.len() { Err(Error::Internal(format!( "postgres returns columns {} doesn't match requested columns {}", @@ -483,7 +500,7 @@ fn sql_for_expr( helper::sql_for_expr(expr, sql_for_expr, values, pls, w) } -fn sql_val_from_postgres(row: &postgres::Row, idx: I, col: &Column) -> Result +fn sql_val_from_postgres(row: &postgres::RowStream, idx: I, col: &Column) -> Result where I: postgres::row::RowIndex + std::fmt::Display, { diff --git a/butane_core/src/db/sqlite.rs b/butane_core/src/db/sqlite.rs deleted file mode 100644 index 7f4a11d1..00000000 --- a/butane_core/src/db/sqlite.rs +++ /dev/null @@ -1,618 +0,0 @@ -//! SQLite database backend -use super::helper; -use super::*; -use crate::db::connmethods::BackendRows; -use crate::debug; -use crate::migrations::adb::{AColumn, ATable, Operation, TypeIdentifier, ADB}; -use crate::query; -use crate::query::Order; -use crate::{Result, SqlType, SqlVal, SqlValRef}; -#[cfg(feature = "datetime")] -use chrono::naive::NaiveDateTime; -use fallible_streaming_iterator::FallibleStreamingIterator; -use pin_project::pin_project; -use std::borrow::Cow; -use std::fmt::Write; -use std::pin::Pin; -use async_trait::async_trait; - -#[cfg(feature = "datetime")] -const SQLITE_DT_FORMAT: &str = "%Y-%m-%d %H:%M:%S"; - -/// The name of the sqlite backend. -pub const BACKEND_NAME: &str = "sqlite"; - -/// SQLite [Backend][crate::db::Backend] implementation. -#[derive(Default)] -pub struct SQLiteBackend {} -impl SQLiteBackend { - pub fn new() -> SQLiteBackend { - SQLiteBackend {} - } -} -impl SQLiteBackend { - fn connect(&self, path: &str) -> Result { - SQLiteConnection::open(Path::new(path)) - } -} -impl Backend for SQLiteBackend { - fn name(&self) -> &'static str { - BACKEND_NAME - } - - fn create_migration_sql(&self, current: &ADB, ops: Vec) -> Result { - let mut current: ADB = (*current).clone(); - Ok(ops - .into_iter() - .map(|o| { - let sql = sql_for_op(&mut current, &o); - current.transform_with(o); - sql - }) - .collect::>>()? - .join("\n")) - } - - fn connect(&self, path: &str) -> Result { - Ok(Connection { - conn: Box::new(self.connect(path)?), - }) - } -} - -/// SQLite database connection. -pub struct SQLiteConnection { - conn: std::sync::Mutex, -} -impl SQLiteConnection { - fn open(path: impl AsRef) -> Result { - rusqlite::Connection::open(path) - .map(|conn| SQLiteConnection { conn }) - .map_err(|e| e.into()) - } - - // For use with connection_method_wrapper macro - #[allow(clippy::unnecessary_wraps)] - fn wrapped_connection_methods(&self) -> Result<&rusqlite::Connection> { - Ok(&self.conn) - } -} -connection_method_wrapper!(SQLiteConnection); - -impl BackendConnection for SQLiteConnection { - fn transaction(&mut self) -> Result> { - let trans: rusqlite::Transaction<'_> = self.conn.transaction()?; - let trans = Box::new(SqliteTransaction::new(trans)); - Ok(Transaction::new(trans)) - } - fn backend(&self) -> Box { - Box::new(SQLiteBackend {}) - } - fn backend_name(&self) -> &'static str { - "sqlite" - } - fn is_closed(&self) -> bool { - false - } -} - -#[async_trait] -impl ConnectionMethods for rusqlite::Connection { - fn execute(&self, sql: &str) -> Result<()> { - if cfg!(feature = "log") { - debug!("execute sql {}", sql); - } - self.execute_batch(sql.as_ref())?; - Ok(()) - } - - async fn query<'a, 'b, 'c: 'a>( - &'c self, - table: &str, - columns: &'b [Column], - expr: Option, - limit: Option, - offset: Option, - order: Option<&[Order]>, - ) -> Result> { - let mut sqlquery = String::new(); - helper::sql_select(columns, table, &mut sqlquery); - let mut values: Vec = Vec::new(); - if let Some(expr) = expr { - sqlquery.write_str(" WHERE ").unwrap(); - sql_for_expr( - query::Expr::Condition(Box::new(expr)), - &mut values, - &mut SQLitePlaceholderSource::new(), - &mut sqlquery, - ); - } - - if let Some(order) = order { - helper::sql_order(order, &mut sqlquery) - } - - if let Some(limit) = limit { - helper::sql_limit(limit, &mut sqlquery) - } - - if let Some(offset) = offset { - if limit.is_none() { - // Sqlite only supports offset in conjunction with - // limit, so add a max limit if we don't have one - // already. - helper::sql_limit(i32::MAX, &mut sqlquery) - } - helper::sql_offset(offset, &mut sqlquery) - } - - debug!("query sql {}", sqlquery); - - let stmt = self.prepare(&sqlquery)?; - let adapter = QueryAdapter::new(stmt, rusqlite::params_from_iter(values))?; - Ok(Box::new(adapter)) - } - fn insert_returning_pk( - &self, - table: &str, - columns: &[Column], - pkcol: &Column, - values: &[SqlValRef<'_>], - ) -> Result { - let mut sql = String::new(); - helper::sql_insert_with_placeholders( - table, - columns, - &mut SQLitePlaceholderSource::new(), - &mut sql, - ); - if cfg!(feature = "log") { - debug!("insert sql {}", sql); - } - self.execute(&sql, rusqlite::params_from_iter(values))?; - let pk: SqlVal = self.query_row_and_then( - &format!( - "SELECT {} FROM {} WHERE ROWID = last_insert_rowid()", - pkcol.name(), - table - ), - [], - |row| sql_val_from_rusqlite(row.get_ref_unwrap(0), pkcol), - )?; - Ok(pk) - } - fn insert_only(&self, table: &str, columns: &[Column], values: &[SqlValRef<'_>]) -> Result<()> { - let mut sql = String::new(); - helper::sql_insert_with_placeholders( - table, - columns, - &mut SQLitePlaceholderSource::new(), - &mut sql, - ); - if cfg!(feature = "log") { - debug!("insert sql {}", sql); - } - self.execute(&sql, rusqlite::params_from_iter(values))?; - Ok(()) - } - fn insert_or_replace( - &self, - table: &str, - columns: &[Column], - _pkcol: &Column, - values: &[SqlValRef], - ) -> Result<()> { - let mut sql = String::new(); - sql_insert_or_update(table, columns, &mut sql); - self.execute(&sql, rusqlite::params_from_iter(values))?; - Ok(()) - } - fn update( - &self, - table: &str, - pkcol: Column, - pk: SqlValRef, - columns: &[Column], - values: &[SqlValRef<'_>], - ) -> Result<()> { - let mut sql = String::new(); - helper::sql_update_with_placeholders( - table, - pkcol, - columns, - &mut SQLitePlaceholderSource::new(), - &mut sql, - ); - let placeholder_values = [values, &[pk]].concat(); - if cfg!(feature = "log") { - debug!("update sql {}", sql); - } - self.execute(&sql, rusqlite::params_from_iter(placeholder_values))?; - Ok(()) - } - fn delete_where(&self, table: &str, expr: BoolExpr) -> Result { - let mut sql = String::new(); - let mut values: Vec = Vec::new(); - write!(&mut sql, "DELETE FROM {} WHERE ", table).unwrap(); - sql_for_expr( - query::Expr::Condition(Box::new(expr)), - &mut values, - &mut SQLitePlaceholderSource::new(), - &mut sql, - ); - let cnt = self.execute(&sql, rusqlite::params_from_iter(values))?; - Ok(cnt) - } - fn has_table(&self, table: &str) -> Result { - let mut stmt = - self.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?;")?; - let mut rows = stmt.query([table])?; - Ok(rows.next()?.is_some()) - } -} - -struct SqliteTransaction<'c> { - trans: std::sync::Mutex>>, -} -impl<'c> SqliteTransaction<'c> { - fn new(trans: rusqlite::Transaction<'c>) -> Self { - SqliteTransaction { trans: Some(trans) } - } - fn get(&self) -> Result<&rusqlite::Transaction<'c>> { - match &self.trans { - None => Err(Self::already_consumed()), - Some(trans) => Ok(trans), - } - } - fn wrapped_connection_methods(&self) -> Result<&rusqlite::Connection> { - Ok(self.get()?.deref()) - } - fn already_consumed() -> Error { - Error::Internal("transaction has already been consumed".to_string()) - } -} -connection_method_wrapper!(SqliteTransaction<'_>); - -#[async_trait] -impl<'c> BackendTransaction<'c> for SqliteTransaction<'c> { - fn commit(&mut self) -> Result<()> { - match self.trans.take().take() { - None => Err(Self::already_consumed()), - Some(trans) => Ok(trans.commit()?), - } - } - async fn rollback(&mut self) -> Result<()> { - match self.trans.take() { - None => Err(Self::already_consumed()), - Some(trans) => Ok(trans.rollback()?), - } - } - // Workaround for https://github.com/rust-lang/rfcs/issues/2765 - fn connection_methods(&self) -> &dyn ConnectionMethods { - self - } - fn connection_methods_mut(&mut self) -> &mut dyn ConnectionMethods { - self - } -} - -impl rusqlite::ToSql for SqlVal { - fn to_sql(&self) -> rusqlite::Result> { - Ok(sqlvalref_to_sqlite(&self.as_ref())) - } -} - -impl<'a> rusqlite::ToSql for SqlValRef<'a> { - fn to_sql<'b>(&'b self) -> rusqlite::Result> { - Ok(sqlvalref_to_sqlite(self)) - } -} - -fn sqlvalref_to_sqlite<'a, 'b>(valref: &'b SqlValRef<'a>) -> rusqlite::types::ToSqlOutput<'a> { - use rusqlite::types::{ToSqlOutput::Borrowed, ToSqlOutput::Owned, Value, ValueRef}; - use SqlValRef::*; - match valref { - Bool(b) => Owned(Value::Integer(*b as i64)), - Int(i) => Owned(Value::Integer(*i as i64)), - BigInt(i) => Owned(Value::Integer(*i)), - Real(r) => Owned(Value::Real(*r)), - Text(t) => Borrowed(ValueRef::Text(t.as_bytes())), - Blob(b) => Borrowed(ValueRef::Blob(b)), - #[cfg(feature = "datetime")] - Timestamp(dt) => { - let f = dt.format(SQLITE_DT_FORMAT); - Owned(Value::Text(f.to_string())) - } - Null => Owned(Value::Null), - Custom(_) => panic!("Custom types not supported in sqlite"), - } -} - -#[pin_project] -struct QueryAdapterInner<'a> { - stmt: rusqlite::Statement<'a>, - // will always be Some when the constructor has finished. We use an option only to get the - // stmt in place before we can reference it. - rows: Option>, -} - -impl<'a> QueryAdapterInner<'a> { - fn new(stmt: rusqlite::Statement<'a>, params: impl rusqlite::Params) -> Result>> { - let mut q = Box::pin(QueryAdapterInner { stmt, rows: None }); - unsafe { - //Soundness: we pin a QueryAdapterInner value containing - // both the stmt and the rows referencing the statement - // together. It is not possible to drop/move the stmt without - // bringing the referencing rows along with it. - let q_ref = Pin::get_unchecked_mut(Pin::as_mut(&mut q)); - let stmt_ref: *mut rusqlite::Statement<'a> = &mut q_ref.stmt; - q_ref.rows = Some((*stmt_ref).query(params)?) - } - Ok(q) - } - - fn next<'b>(self: Pin<&'b mut Self>) -> Result>> { - let this = self.project(); - let rows: &mut rusqlite::Rows<'a> = this.rows.as_mut().unwrap(); - Ok(rows.next()?) - } - - fn current(self: Pin<&Self>) -> Option<&rusqlite::Row> { - let this = self.project_ref(); - this.rows.as_ref().unwrap().get() - } -} - -struct QueryAdapter<'a> { - inner: Pin>>, -} -impl<'a> QueryAdapter<'a> { - fn new(stmt: rusqlite::Statement<'a>, params: impl rusqlite::Params) -> Result { - Ok(QueryAdapter { - inner: QueryAdapterInner::new(stmt, params)?, - }) - } -} - -impl<'a> BackendRows for QueryAdapter<'a> { - fn next<'b>(&'b mut self) -> Result> { - Ok(self - .inner - .as_mut() - .next()? - .map(|row| row as &dyn BackendRow)) - } - fn current<'b>(&'b self) -> Option<&'b (dyn BackendRow + 'b)> { - self.inner - .as_ref() - .current() - .map(|row| row as &dyn BackendRow) - } -} - -impl BackendRow for rusqlite::Row<'_> { - fn get(&self, idx: usize, ty: SqlType) -> Result { - sql_valref_from_rusqlite(self.get_ref(idx)?, &ty) - } - fn len(&self) -> usize { - self.as_ref().column_count() - } -} - -fn sql_for_expr( - expr: query::Expr, - values: &mut Vec, - pls: &mut SQLitePlaceholderSource, - w: &mut W, -) where - W: Write, -{ - helper::sql_for_expr(expr, sql_for_expr, values, pls, w) -} - -fn sql_val_from_rusqlite(val: rusqlite::types::ValueRef, col: &Column) -> Result { - sql_valref_from_rusqlite(val, col.ty()).map(|v| v.into()) -} - -fn sql_valref_from_rusqlite<'a>( - val: rusqlite::types::ValueRef<'a>, - ty: &SqlType, -) -> Result> { - if let rusqlite::types::ValueRef::Null = val { - return Ok(SqlValRef::Null); - } - Ok(match ty { - SqlType::Bool => SqlValRef::Bool(val.as_i64()? != 0), - SqlType::Int => SqlValRef::Int(val.as_i64()? as i32), - SqlType::BigInt => SqlValRef::BigInt(val.as_i64()?), - SqlType::Real => SqlValRef::Real(val.as_f64()?), - SqlType::Text => SqlValRef::Text(val.as_str()?), - #[cfg(feature = "datetime")] - SqlType::Timestamp => SqlValRef::Timestamp(NaiveDateTime::parse_from_str( - val.as_str()?, - SQLITE_DT_FORMAT, - )?), - SqlType::Blob => SqlValRef::Blob(val.as_blob()?), - SqlType::Custom(v) => { - return Err(Error::IncompatibleCustomT(v.deref().clone(), BACKEND_NAME)) - } - }) -} - -fn sql_for_op(current: &mut ADB, op: &Operation) -> Result { - match op { - Operation::AddTable(table) => Ok(create_table(table, false)), - Operation::AddTableIfNotExists(table) => Ok(create_table(table, true)), - Operation::RemoveTable(name) => Ok(drop_table(name)), - Operation::AddColumn(tbl, col) => add_column(tbl, col), - Operation::RemoveColumn(tbl, name) => Ok(remove_column(current, tbl, name)), - Operation::ChangeColumn(tbl, old, new) => Ok(change_column(current, tbl, old, Some(new))), - } -} - -fn create_table(table: &ATable, allow_exists: bool) -> String { - let coldefs = table - .columns - .iter() - .map(define_column) - .collect::>() - .join(",\n"); - let modifier = if allow_exists { "IF NOT EXISTS " } else { "" }; - format!("CREATE TABLE {}{} (\n{}\n);", modifier, table.name, coldefs) -} - -fn define_column(col: &AColumn) -> String { - let mut constraints: Vec = Vec::new(); - if !col.nullable() { - constraints.push("NOT NULL".to_string()); - } - if col.is_pk() { - constraints.push("PRIMARY KEY".to_string()); - } - if col.is_auto() && !col.is_pk() { - // integer primary key is automatically an alias for ROWID, - // and we only allow auto on integer types - constraints.push("AUTOINCREMENT".to_string()); - } - if col.unique() { - constraints.push("UNIQUE".to_string()); - } - format!( - "{} {} {}", - &col.name(), - col_sqltype(col), - constraints.join(" ") - ) -} - -fn col_sqltype(col: &AColumn) -> Cow { - match col.typeid() { - Ok(TypeIdentifier::Ty(ty)) => Cow::Borrowed(sqltype(&ty)), - Ok(TypeIdentifier::Name(name)) => Cow::Owned(name), - // sqlite doesn't actually require that the column type be - // specified - Err(_) => Cow::Borrowed(""), - } -} - -fn sqltype(ty: &SqlType) -> &'static str { - match ty { - SqlType::Bool => "INTEGER", - SqlType::Int => "INTEGER", - SqlType::BigInt => "INTEGER", - SqlType::Real => "REAL", - SqlType::Text => "TEXT", - #[cfg(feature = "datetime")] - SqlType::Timestamp => "TEXT", - SqlType::Blob => "BLOB", - SqlType::Custom(_) => panic!("Custom types not supported by sqlite backend"), - } -} - -fn drop_table(name: &str) -> String { - format!("DROP TABLE {};", name) -} - -fn add_column(tbl_name: &str, col: &AColumn) -> Result { - let default: SqlVal = helper::column_default(col)?; - Ok(format!( - "ALTER TABLE {} ADD COLUMN {} DEFAULT {};", - tbl_name, - define_column(col), - helper::sql_literal_value(default)? - )) -} - -fn remove_column(current: &mut ADB, tbl_name: &str, name: &str) -> String { - let old = current - .get_table(tbl_name) - .and_then(|table| table.column(name)) - .cloned(); - match old { - Some(col) => change_column(current, tbl_name, &col, None), - None => { - crate::warn!( - "Cannot remove column {} that does not exist from table {}", - name, - tbl_name - ); - "".to_string() - } - } -} - -fn copy_table(old: &ATable, new: &ATable) -> String { - let column_names = new - .columns - .iter() - .map(|col| col.name()) - .collect::>() - .join(", "); - format!( - "INSERT INTO {} SELECT {} FROM {};", - &new.name, column_names, &old.name - ) -} - -fn tmp_table_name(name: &str) -> String { - format!("{}__butane_tmp", name) -} - -fn change_column( - current: &mut ADB, - tbl_name: &str, - old: &AColumn, - new: Option<&AColumn>, -) -> String { - let table = current.get_table(tbl_name); - if table.is_none() { - crate::warn!( - "Cannot alter column {} from table {} that does not exist", - &old.name(), - tbl_name - ); - return "".to_string(); - } - let old_table = table.unwrap(); - let mut new_table = old_table.clone(); - new_table.name = tmp_table_name(&new_table.name); - match new { - Some(col) => new_table.replace_column(col.clone()), - None => new_table.remove_column(old.name()), - } - let stmts: [&str; 4] = [ - &create_table(&new_table, false), - ©_table(old_table, &new_table), - &drop_table(&old_table.name), - &format!("ALTER TABLE {} RENAME TO {};", &new_table.name, tbl_name), - ]; - let result = stmts.join("\n"); - new_table.name = old_table.name.clone(); - current.replace_table(new_table); - result -} - -pub fn sql_insert_or_update(table: &str, columns: &[Column], w: &mut impl Write) { - write!(w, "INSERT OR REPLACE ").unwrap(); - write!(w, "INTO {} (", table).unwrap(); - helper::list_columns(columns, w); - write!(w, ") VALUES (").unwrap(); - columns.iter().fold("", |sep, _| { - write!(w, "{}?", sep).unwrap(); - ", " - }); - write!(w, ")").unwrap(); -} - -struct SQLitePlaceholderSource {} -impl SQLitePlaceholderSource { - fn new() -> Self { - SQLitePlaceholderSource {} - } -} -impl helper::PlaceholderSource for SQLitePlaceholderSource { - fn next_placeholder(&mut self) -> Cow { - // sqlite placeholder is always a question mark. - Cow::Borrowed("?") - } -} diff --git a/butane_core/src/fkey.rs b/butane_core/src/fkey.rs index 834e12a8..c48e23d4 100644 --- a/butane_core/src/fkey.rs +++ b/butane_core/src/fkey.rs @@ -1,6 +1,6 @@ use crate::db::ConnectionMethods; use crate::*; -use once_cell::unsync::OnceCell; +use tokio::sync::OnceCell; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::borrow::Cow; use std::fmt::{Debug, Formatter}; @@ -55,17 +55,6 @@ impl ForeignKey { } } - /// Loads the value referred to by this foreign key from the - /// database if necessary and returns a reference to it. - pub fn load(&self, conn: &impl ConnectionMethods) -> Result<&T> { - self.val - .get_or_try_init(|| { - let pk = self.valpk.get().unwrap(); - T::get(conn, &T::PKType::from_sql_ref(pk.as_ref())?).map(Box::new) - }) - .map(|v| v.as_ref()) - } - fn new_raw() -> Self { ForeignKey { val: OnceCell::new(), @@ -85,6 +74,20 @@ impl ForeignKey { } } +impl ForeignKey { + /// Loads the value referred to by this foreign key from the + /// database if necessary and returns a reference to it. + pub async fn load(&self, conn: &impl ConnectionMethods) -> Result<&T> { + self.val + .get_or_try_init(|| async { + let pk = self.valpk.get().unwrap(); + T::get(conn, &T::PKType::from_sql_ref(pk.as_ref())?).await.map(Box::new) + }) + .await + .map(|v| v.as_ref()) + } +} + impl From for ForeignKey { fn from(obj: T) -> Self { let ret = Self::new_raw(); diff --git a/butane_core/src/lib.rs b/butane_core/src/lib.rs index 2548f436..748b750a 100644 --- a/butane_core/src/lib.rs +++ b/butane_core/src/lib.rs @@ -71,7 +71,7 @@ pub trait DataResult: Sized { /// Rather than implementing this type manually, use the /// `#[model]` attribute. #[async_trait] -pub trait DataObject: DataResult { +pub trait DataObject: DataResult + Sync { /// The type of the primary key field. type PKType: PrimaryKeyType; type Fields: Default; @@ -85,7 +85,7 @@ pub trait DataObject: DataResult { /// Get the primary key fn pk(&self) -> &Self::PKType; /// Find this object in the database based on primary key. - async fn get(conn: &impl ConnectionMethods, id: impl Borrow) -> Result + async fn get(conn: &impl ConnectionMethods, id: impl Borrow + Send + Sync) -> Result where Self: Sized, Self::PKType: Sync, @@ -103,9 +103,9 @@ pub trait DataObject: DataResult { } /// Save the object to the database. - fn save(&mut self, conn: &impl ConnectionMethods) -> Result<()>; + async fn save(&mut self, conn: &impl ConnectionMethods) -> Result<()>; /// Delete the object from the database. - fn delete(&self, conn: &impl ConnectionMethods) -> Result<()>; + async fn delete(&self, conn: &impl ConnectionMethods) -> Result<()>; } pub trait ModelTyped { diff --git a/butane_core/src/many.rs b/butane_core/src/many.rs index c6969f69..58e404a8 100644 --- a/butane_core/src/many.rs +++ b/butane_core/src/many.rs @@ -1,12 +1,13 @@ use crate::db::{Column, ConnectionMethods}; use crate::query::{BoolExpr, Expr}; use crate::{DataObject, Error, FieldType, Result, SqlType, SqlVal, ToSql}; -use once_cell::unsync::OnceCell; +use tokio::sync::OnceCell; use serde::{Deserialize, Serialize}; use std::borrow::Cow; fn default_oc() -> OnceCell> { - OnceCell::default() + // Same as impl Default for once_cell::unsync::OnceCell + OnceCell::new() } /// Used to implement a many-to-many relationship between models. @@ -102,7 +103,7 @@ where } /// Used by macro-generated code. You do not need to call this directly. - pub fn save(&mut self, conn: &impl ConnectionMethods) -> Result<()> { + pub async fn save(&mut self, conn: &impl ConnectionMethods) -> Result<()> { let owner = self.owner.as_ref().ok_or(Error::NotInitialized)?; while !self.new_values.is_empty() { conn.insert_only( @@ -112,13 +113,13 @@ where owner.as_ref(), self.new_values.pop().unwrap().as_ref().clone(), ], - )?; + ).await?; } if !self.removed_values.is_empty() { conn.delete_where( &self.item_table, BoolExpr::In("has", std::mem::take(&mut self.removed_values)), - )?; + ).await?; } self.new_values.clear(); Ok(()) @@ -126,31 +127,31 @@ where /// Loads the values referred to by this foreign key from the /// database if necessary and returns a reference to them. - pub fn load(&self, conn: &impl ConnectionMethods) -> Result> { - let vals: Result<&Vec> = self.all_values.get_or_try_init(|| { + pub async fn load(&self, conn: &impl ConnectionMethods) -> Result> { + let vals: Result<&Vec> = self.all_values.get_or_try_init(|| async { //if we don't have an owner then there are no values let owner: &SqlVal = match &self.owner { Some(o) => o, None => return Ok(Vec::new()), }; - let mut vals = T::query() + let mut vals = T::query().await .filter(BoolExpr::Subquery { col: T::PKCOL, tbl2: self.item_table.clone(), tbl2_col: "has", expr: Box::new(BoolExpr::Eq("owner", Expr::Val(owner.clone()))), }) - .load(conn)?; + .load(conn).await?; // Now add in the values for things not saved to the db yet if !self.new_values.is_empty() { vals.append( - &mut T::query() + &mut T::query().await .filter(BoolExpr::In(T::PKCOL, self.new_values.clone())) - .load(conn)?, + .load(conn).await?, ); } Ok(vals) - }); + }).await; vals.map(|v| v.iter()) } pub fn columns(&self) -> [Column; 2] { diff --git a/butane_core/src/migrations/fsmigrations.rs b/butane_core/src/migrations/fsmigrations.rs index 328ac66d..a52d8dac 100644 --- a/butane_core/src/migrations/fsmigrations.rs +++ b/butane_core/src/migrations/fsmigrations.rs @@ -10,7 +10,6 @@ use std::fs::{File, OpenOptions}; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; -use std::rc::Rc; type SqlTypeMap = BTreeMap; const TYPES_FILENAME: &str = "types.json"; @@ -43,7 +42,7 @@ impl MigrationsState { /// A migration stored in the filesystem pub struct FsMigration { - fs: Rc, + fs: std::sync::Arc, root: PathBuf, } @@ -236,13 +235,13 @@ impl Eq for FsMigration {} /// A collection of migrations stored in the filesystem. pub struct FsMigrations { - fs: Rc, + fs: std::sync::Arc, root: PathBuf, current: FsMigration, } impl FsMigrations { pub fn new(root: PathBuf) -> Self { - let fs = Rc::new(OsFilesystem {}); + let fs = std::sync::Arc::new(OsFilesystem {}); let current = FsMigration { fs: fs.clone(), root: root.join("current"), @@ -297,6 +296,7 @@ impl Migrations for FsMigrations { } } +#[async_trait::async_trait] impl MigrationsMut for FsMigrations { fn current(&mut self) -> &mut Self::M { &mut self.current @@ -320,7 +320,7 @@ impl MigrationsMut for FsMigrations { Ok(()) } - fn clear_migrations(&mut self, conn: &impl ConnectionMethods) -> Result<()> { + async fn clear_migrations(&mut self, conn: &impl ConnectionMethods) -> Result<()> { for entry in std::fs::read_dir(&self.root)? { let entry = entry?; if matches!(entry.path().file_name(), Some(name) if name == "current") { @@ -332,7 +332,7 @@ impl MigrationsMut for FsMigrations { std::fs::remove_file(entry.path())?; } } - conn.delete_where(super::ButaneMigration::TABLE, crate::query::BoolExpr::True)?; + conn.delete_where(super::ButaneMigration::TABLE, crate::query::BoolExpr::True).await?; Ok(()) } } diff --git a/butane_core/src/migrations/memmigrations.rs b/butane_core/src/migrations/memmigrations.rs index 030d4c11..6f5fd2d1 100644 --- a/butane_core/src/migrations/memmigrations.rs +++ b/butane_core/src/migrations/memmigrations.rs @@ -128,6 +128,7 @@ impl Migrations for MemMigrations { } } +#[async_trait::async_trait] impl MigrationsMut for MemMigrations { fn current(&mut self) -> &mut Self::M { &mut self.current @@ -152,10 +153,10 @@ impl MigrationsMut for MemMigrations { Ok(()) } - fn clear_migrations(&mut self, conn: &impl ConnectionMethods) -> Result<()> { + async fn clear_migrations(&mut self, conn: &impl ConnectionMethods) -> Result<()> { self.migrations.clear(); self.latest = None; - conn.delete_where(ButaneMigration::TABLE, BoolExpr::True)?; + conn.delete_where(ButaneMigration::TABLE, BoolExpr::True).await?; Ok(()) } } diff --git a/butane_core/src/migrations/migration.rs b/butane_core/src/migrations/migration.rs index b73b5d39..d15dbbc5 100644 --- a/butane_core/src/migrations/migration.rs +++ b/butane_core/src/migrations/migration.rs @@ -13,6 +13,7 @@ use std::cmp::PartialEq; /// /// A Migration cannot be constructed directly, only retrieved from /// [Migrations][crate::migrations::Migrations]. +#[async_trait::async_trait] pub trait Migration: PartialEq { /// Retrieves the full abstract database state describing all tables fn db(&self) -> Result; @@ -37,46 +38,46 @@ pub trait Migration: PartialEq { /// Apply the migration to a database connection. The connection /// must be for the same type of database as this and the database /// must be in the state of the migration prior to this one - fn apply(&self, conn: &mut impl db::BackendConnection) -> Result<()> { + async fn apply(&self, conn: &mut impl db::BackendConnection) -> Result<()> { let backend_name = conn.backend_name(); - let tx = conn.transaction()?; + let tx = conn.transaction().await?; let sql = self .up_sql(backend_name)? .ok_or_else(|| Error::UnknownBackend(backend_name.to_string()))?; - tx.execute(&sql)?; - self.mark_applied(&tx)?; - tx.commit() + tx.execute(&sql).await?; + self.mark_applied(&tx).await?; + tx.commit().await } /// Mark the migration as being applied without doing any /// work. Use carefully -- the caller must ensure that the /// database schema already matches that expected by this /// migration. - fn mark_applied(&self, conn: &impl db::ConnectionMethods) -> Result<()> { + async fn mark_applied(&self, conn: &impl db::ConnectionMethods) -> Result<()> { conn.insert_only( ButaneMigration::TABLE, ButaneMigration::COLUMNS, &[self.name().as_ref().to_sql_ref()], - ) + ).await } /// Un-apply (downgrade) the migration to a database /// connection. The connection must be for the same type of /// database as this and this must be the latest migration applied /// to the database. - fn downgrade(&self, conn: &mut impl db::BackendConnection) -> Result<()> { + async fn downgrade(&self, conn: &mut impl db::BackendConnection) -> Result<()> { let backend_name = conn.backend_name(); - let tx = conn.transaction()?; + let tx = conn.transaction().await?; let sql = self .down_sql(backend_name)? .ok_or_else(|| Error::UnknownBackend(backend_name.to_string()))?; - tx.execute(&sql)?; + tx.execute(&sql).await?; let nameval = self.name().as_ref().to_sql(); tx.delete_where( ButaneMigration::TABLE, BoolExpr::Eq(ButaneMigration::PKCOL, Expr::Val(nameval)), - )?; - tx.commit() + ).await?; + tx.commit().await } } diff --git a/butane_core/src/migrations/mod.rs b/butane_core/src/migrations/mod.rs index 928c423b..21c53020 100644 --- a/butane_core/src/migrations/mod.rs +++ b/butane_core/src/migrations/mod.rs @@ -78,7 +78,7 @@ pub trait Migrations { /// Get the last migration that has been applied to the database or None /// if no migrations have been applied async fn last_applied_migration(&self, conn: &impl ConnectionMethods) -> Result> { - if !conn.has_table(ButaneMigration::TABLE)? { + if !conn.has_table(ButaneMigration::TABLE).await? { return Ok(None); } let migrations: Vec = conn @@ -108,6 +108,7 @@ pub trait Migrations { } } +#[async_trait] pub trait MigrationsMut: Migrations where Self::M: MigrationMut, @@ -126,7 +127,7 @@ where /// any storage backing it) and deleting the record of their /// existence/application from the database. The database schema /// is not modified, nor is any other data removed. Use carefully. - fn clear_migrations(&mut self, conn: &impl ConnectionMethods) -> Result<()>; + async fn clear_migrations(&mut self, conn: &impl ConnectionMethods) -> Result<()>; /// Get a pseudo-migration representing the current state as /// determined by the last build of models. This does not @@ -256,6 +257,7 @@ impl DataResult for ButaneMigration { } } +#[async_trait] impl DataObject for ButaneMigration { type PKType = String; type Fields = (); // we don't need Fields as we never filter @@ -265,7 +267,7 @@ impl DataObject for ButaneMigration { fn pk(&self) -> &String { &self.name } - fn save(&mut self, conn: &impl ConnectionMethods) -> Result<()> { + async fn save(&mut self, conn: &impl ConnectionMethods) -> Result<()> { let mut values: Vec> = Vec::with_capacity(2usize); values.push(self.name.to_sql_ref()); conn.insert_or_replace( @@ -273,9 +275,9 @@ impl DataObject for ButaneMigration { ::COLUMNS, &Column::new(Self::PKCOL, SqlType::Text), &values, - ) + ).await } - fn delete(&self, conn: &impl ConnectionMethods) -> Result<()> { - conn.delete(Self::TABLE, Self::PKCOL, self.pk().to_sql()) + async fn delete(&self, conn: &impl ConnectionMethods) -> Result<()> { + conn.delete(Self::TABLE, Self::PKCOL, self.pk().to_sql()).await } } diff --git a/butane_core/src/query/mod.rs b/butane_core/src/query/mod.rs index 1b64e4ac..f0157e4c 100644 --- a/butane_core/src/query/mod.rs +++ b/butane_core/src/query/mod.rs @@ -198,7 +198,7 @@ impl Query { } /// Executes the query against `conn` and deletes all matching objects. - pub fn delete(self, conn: &impl ConnectionMethods) -> Result { - conn.delete_where(&self.table, self.filter.unwrap_or(BoolExpr::True)) + pub async fn delete(self, conn: &impl ConnectionMethods) -> Result { + conn.delete_where(&self.table, self.filter.unwrap_or(BoolExpr::True)).await } } diff --git a/butane_core/src/sqlval.rs b/butane_core/src/sqlval.rs index 2efd50e2..990dcbdc 100644 --- a/butane_core/src/sqlval.rs +++ b/butane_core/src/sqlval.rs @@ -269,7 +269,7 @@ pub trait FieldType: ToSql + FromSql { } /// Marker trait for a type suitable for being a primary key -pub trait PrimaryKeyType: FieldType + Clone + PartialEq {} +pub trait PrimaryKeyType: FieldType + Clone + PartialEq + Sync{} /// Trait for referencing the primary key for a given model. Used to /// implement ForeignKey equality tests. From 44f5266550f4952f4c7f663f452f1943937af768 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Sun, 5 Feb 2023 18:31:42 -0500 Subject: [PATCH 12/78] Fixups --- Cargo.lock | 73 ++- butane_core/Cargo.toml | 5 +- butane_core/src/db/connmethods.rs | 10 +- butane_core/src/db/helper.rs | 4 +- butane_core/src/db/macros.rs | 6 +- butane_core/src/db/pg.rs | 12 +- butane_core/src/db/sqlite.rs | 764 ++++++++++++++++++++++++++++++ butane_core/src/many.rs | 54 +-- 8 files changed, 876 insertions(+), 52 deletions(-) create mode 100644 butane_core/src/db/sqlite.rs diff --git a/Cargo.lock b/Cargo.lock index d249f31c..a8bd4593 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,6 +110,12 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" + [[package]] name = "bitflags" version = "1.3.2" @@ -151,7 +157,7 @@ dependencies = [ "r2d2", "rusqlite", "serde_json", - "tokio-postgres", + "tokio-postgres 0.7.7 (registry+https://github.com/rust-lang/crates.io-index)", "uuid", ] @@ -190,6 +196,7 @@ dependencies = [ "fallible-iterator", "fallible-streaming-iterator", "fs2", + "futures-util", "hex", "log", "native-tls", @@ -205,7 +212,7 @@ dependencies = [ "syn 1.0.107", "thiserror", "tokio", - "tokio-postgres", + "tokio-postgres 0.7.7 (git+https://github.com/sfackler/rust-postgres)", "uuid", ] @@ -1038,7 +1045,7 @@ dependencies = [ "native-tls", "tokio", "tokio-native-tls", - "tokio-postgres", + "tokio-postgres 0.7.7 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1047,7 +1054,24 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "878c6cbf956e03af9aa8204b407b9cbf47c072164800aa918c516cd4b056c50c" dependencies = [ - "base64", + "base64 0.13.1", + "byteorder", + "bytes", + "fallible-iterator", + "hmac", + "md-5", + "memchr", + "rand", + "sha2", + "stringprep", +] + +[[package]] +name = "postgres-protocol" +version = "0.6.4" +source = "git+https://github.com/sfackler/rust-postgres#f4d8d603f1b9d3edd9b93889465f3db5bba0fd40" +dependencies = [ + "base64 0.21.0", "byteorder", "bytes", "fallible-iterator", @@ -1066,10 +1090,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73d946ec7d256b04dfadc4e6a3292324e6f417124750fc5c0950f981b703a0f1" dependencies = [ "bytes", - "chrono", "fallible-iterator", "geo-types", - "postgres-protocol", + "postgres-protocol 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "postgres-types" +version = "0.2.4" +source = "git+https://github.com/sfackler/rust-postgres#f4d8d603f1b9d3edd9b93889465f3db5bba0fd40" +dependencies = [ + "bytes", + "chrono", + "fallible-iterator", + "postgres-protocol 0.6.4 (git+https://github.com/sfackler/rust-postgres)", ] [[package]] @@ -1531,8 +1565,31 @@ dependencies = [ "percent-encoding", "phf", "pin-project-lite", - "postgres-protocol", - "postgres-types", + "postgres-protocol 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)", + "postgres-types 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", + "socket2", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-postgres" +version = "0.7.7" +source = "git+https://github.com/sfackler/rust-postgres#f4d8d603f1b9d3edd9b93889465f3db5bba0fd40" +dependencies = [ + "async-trait", + "byteorder", + "bytes", + "fallible-iterator", + "futures-channel", + "futures-util", + "log", + "parking_lot", + "percent-encoding", + "phf", + "pin-project-lite", + "postgres-protocol 0.6.4 (git+https://github.com/sfackler/rust-postgres)", + "postgres-types 0.2.4 (git+https://github.com/sfackler/rust-postgres)", "socket2", "tokio", "tokio-util", diff --git a/butane_core/Cargo.toml b/butane_core/Cargo.toml index 02bf5770..fefee16d 100644 --- a/butane_core/Cargo.toml +++ b/butane_core/Cargo.toml @@ -24,10 +24,13 @@ cfg-if = "1.0" fallible-iterator = "0.2" fallible-streaming-iterator = "0.1" fs2 = "0.4" # for file locks +futures-util = "0.3" hex = "0.4" log = { version="0.4", optional=true } native-tls={ version = "0.2", optional = true } -tokio-postgres={ version = "0.7", features=["with-chrono-0_4"], optional = true} +# Note, this is for the fix https://github.com/sfackler/rust-postgres/commit/e38e435665af8dbd55e7d803f019405653f99205 +# Once a new version beyond 0.7.7 is released, we should be able to go back to a regular dependency. +tokio-postgres={ git = "https://github.com/sfackler/rust-postgres", features=["with-chrono-0_4"], optional = true} postgres-native-tls={ version = "0.5", optional = true } proc-macro2 = "1.0" pin-project = "1" diff --git a/butane_core/src/db/connmethods.rs b/butane_core/src/db/connmethods.rs index f25e8890..8176641b 100644 --- a/butane_core/src/db/connmethods.rs +++ b/butane_core/src/db/connmethods.rs @@ -31,7 +31,12 @@ pub trait ConnectionMethods: Sync { values: &[SqlValRef<'_>], ) -> Result; /// Like `insert_returning_pk` but with no return value - async fn insert_only(&self, table: &str, columns: &[Column], values: &[SqlValRef<'_>]) -> Result<()>; + async fn insert_only( + &self, + table: &str, + columns: &[Column], + values: &[SqlValRef<'_>], + ) -> Result<()>; /// Insert unless there's a conflict on the primary key column, in which case update async fn insert_or_replace( &self, @@ -49,7 +54,8 @@ pub trait ConnectionMethods: Sync { values: &[SqlValRef<'_>], ) -> Result<()>; async fn delete(&self, table: &str, pkcol: &'static str, pk: SqlVal) -> Result<()> { - self.delete_where(table, BoolExpr::Eq(pkcol, Expr::Val(pk))).await?; + self.delete_where(table, BoolExpr::Eq(pkcol, Expr::Val(pk))) + .await?; Ok(()) } async fn delete_where(&self, table: &str, expr: BoolExpr) -> Result; diff --git a/butane_core/src/db/helper.rs b/butane_core/src/db/helper.rs index a419c09d..a712c4ae 100644 --- a/butane_core/src/db/helper.rs +++ b/butane_core/src/db/helper.rs @@ -204,7 +204,9 @@ pub fn column_default(col: &AColumn) -> Result { SqlType::Text => SqlVal::Text("".to_string()), SqlType::Blob => SqlVal::Blob(Vec::new()), #[cfg(feature = "datetime")] - SqlType::Timestamp => SqlVal::Timestamp(NaiveDateTime::from_timestamp(0, 0)), + SqlType::Timestamp => { + SqlVal::Timestamp(NaiveDateTime::from_timestamp_opt(0, 0).unwrap()) + } SqlType::Custom(_) => return Err(Error::NoCustomDefault), }, TypeIdentifier::Name(_) => return Err(Error::NoCustomDefault), diff --git a/butane_core/src/db/macros.rs b/butane_core/src/db/macros.rs index ecb167a9..b979bf10 100644 --- a/butane_core/src/db/macros.rs +++ b/butane_core/src/db/macros.rs @@ -64,10 +64,12 @@ macro_rules! connection_method_wrapper { .await } async fn delete_where(&self, table: &str, expr: BoolExpr) -> Result { - self.wrapped_connection_methods()?.delete_where(table, expr).await + self.wrapped_connection_methods()? + .delete_where(table, expr) + .await } async fn has_table(&self, table: &str) -> Result { - self.wrapped_connection_methods()?.has_table(table).await + self.wrapped_connection_methods()?.has_table(table) } } }; diff --git a/butane_core/src/db/pg.rs b/butane_core/src/db/pg.rs index dbbdccf3..f6e3ae48 100644 --- a/butane_core/src/db/pg.rs +++ b/butane_core/src/db/pg.rs @@ -4,12 +4,13 @@ use super::helper; use super::*; use crate::custom::{SqlTypeCustom, SqlValRefCustom}; use crate::migrations::adb::{AColumn, ATable, Operation, TypeIdentifier, ADB}; -use crate::{debug, query}; +use crate::{debug, query, warn}; use crate::{Result, SqlType, SqlVal, SqlValRef}; use async_trait::async_trait; use bytes::BufMut; #[cfg(feature = "datetime")] use chrono::NaiveDateTime; +use futures_util::StreamExt; use std::fmt::Write; use tokio; use tokio_postgres as postgres; @@ -54,8 +55,6 @@ impl Backend for PgBackend { } } -// type PgConnHandle = tokio::task::JoinHandle>; - /// Pg database connection. pub struct PgConnection { client: postgres::Client, @@ -83,8 +82,7 @@ impl PgConnection { let (client, conn) = postgres::connect(params, connector).await?; let conn_handle = tokio::spawn(async move { if let Err(e) = conn.await { - // TODO don't panic - panic!() + warn!("Postgres connection error {}", e); } }); Ok((client, conn_handle)) @@ -249,12 +247,12 @@ where .await?; Ok(()) } - async fn insert_or_replace<'a>( + async fn insert_or_replace( &self, table: &str, columns: &[Column], pkcol: &Column, - values: &[SqlValRef<'a>], + values: &[SqlValRef<'_>], ) -> Result<()> { let mut sql = String::new(); sql_insert_or_replace_with_placeholders(table, columns, pkcol, &mut sql); diff --git a/butane_core/src/db/sqlite.rs b/butane_core/src/db/sqlite.rs new file mode 100644 index 00000000..3de62ef9 --- /dev/null +++ b/butane_core/src/db/sqlite.rs @@ -0,0 +1,764 @@ +//! SQLite database backend +use super::helper; +use super::*; +use crate::db::connmethods::BackendRows; +use crate::debug; +use crate::migrations::adb::{AColumn, ATable, Operation, TypeIdentifier, ADB}; +use crate::query; +use crate::query::Order; +use crate::{Result, SqlType, SqlVal, SqlValRef}; +#[cfg(feature = "datetime")] +use chrono::naive::NaiveDateTime; +use fallible_streaming_iterator::FallibleStreamingIterator; +use pin_project::pin_project; +use std::borrow::Cow; +use std::fmt::Write; +use std::pin::Pin; +use std::sync::{Mutex, MutexGuard}; + +#[cfg(feature = "datetime")] +const SQLITE_DT_FORMAT: &str = "%Y-%m-%d %H:%M:%S"; + +/// The name of the sqlite backend. +pub const BACKEND_NAME: &str = "sqlite"; + +/// SQLite [Backend][crate::db::Backend] implementation. +#[derive(Default)] +pub struct SQLiteBackend {} +impl SQLiteBackend { + pub fn new() -> SQLiteBackend { + SQLiteBackend {} + } +} +impl SQLiteBackend { + fn connect(&self, path: &str) -> Result { + SQLiteConnection::open(Path::new(path)) + } +} +impl Backend for SQLiteBackend { + fn name(&self) -> &'static str { + BACKEND_NAME + } + + fn create_migration_sql(&self, current: &ADB, ops: Vec) -> Result { + let mut current: ADB = (*current).clone(); + Ok(ops + .into_iter() + .map(|o| { + let sql = sql_for_op(&mut current, &o); + current.transform_with(o); + sql + }) + .collect::>>()? + .join("\n")) + } + + fn connect(&self, path: &str) -> Result { + Ok(Connection { + conn: Box::new(self.connect(path)?), + }) + } +} + +/// SQLite database connection. +pub struct SQLiteConnection { + conn: Mutex, +} +impl SQLiteConnection { + fn open(path: impl AsRef) -> Result { + rusqlite::Connection::open(path) + .map(|conn| SQLiteConnection { + conn: Mutex::new(conn), + }) + .map_err(|e| e.into()) + } + + // For use with connection_method_wrapper macro + #[allow(clippy::unnecessary_wraps)] + fn wrapped_connection_methods(&self) -> Result<&Mutex> { + Ok(&self.conn) + } +} +connection_method_wrapper!(SQLiteConnection); + +impl BackendConnection for SQLiteConnection { + fn transaction(&mut self) -> Result> { + let trans: rusqlite::Transaction<'_> = self.conn.lock()?.transaction()?; + let trans = Box::new(SqliteTransaction::new(trans)); + Ok(Transaction::new(trans)) + } + fn backend(&self) -> Box { + Box::new(SQLiteBackend {}) + } + fn backend_name(&self) -> &'static str { + "sqlite" + } + fn is_closed(&self) -> bool { + false + } +} + +fn rs_conn_execute(conn: &rusqlite::Connection, sql: &str) -> Result<()> { + if cfg!(feature = "log") { + debug!("execute sql {}", sql); + } + conn.execute_batch(sql.as_ref())?; + Ok(()) +} + +fn rs_conn_query<'a, 'b, 'c: 'a>( + conn: &'c rusqlite::Connection, + table: &str, + columns: &'b [Column], + expr: Option, + limit: Option, + offset: Option, + order: Option<&[Order]>, +) -> Result> { + let mut sqlquery = String::new(); + helper::sql_select(columns, table, &mut sqlquery); + let mut values: Vec = Vec::new(); + if let Some(expr) = expr { + sqlquery.write_str(" WHERE ").unwrap(); + sql_for_expr( + query::Expr::Condition(Box::new(expr)), + &mut values, + &mut SQLitePlaceholderSource::new(), + &mut sqlquery, + ); + } + + if let Some(order) = order { + helper::sql_order(order, &mut sqlquery) + } + + if let Some(limit) = limit { + helper::sql_limit(limit, &mut sqlquery) + } + + if let Some(offset) = offset { + if limit.is_none() { + // Sqlite only supports offset in conjunction with + // limit, so add a max limit if we don't have one + // already. + helper::sql_limit(i32::MAX, &mut sqlquery) + } + helper::sql_offset(offset, &mut sqlquery) + } + + debug!("query sql {}", sqlquery); + let stmt = conn.prepare(&sqlquery)?; + // TODO + /*let rows = stmt.query().mapped(|row| { + for col in columns { + + } + + }*/ + let adapter = QueryAdapter::new(stmt, rusqlite::params_from_iter(values))?; + Ok(Box::new(adapter)) +} + +fn rs_conn_insert_returning_pk( + conn: &rusqlite::Connection, + table: &str, + columns: &[Column], + pkcol: &Column, + values: &[SqlValRef<'_>], +) -> Result { + let mut sql = String::new(); + helper::sql_insert_with_placeholders( + table, + columns, + &mut SQLitePlaceholderSource::new(), + &mut sql, + ); + if cfg!(feature = "log") { + debug!("insert sql {}", sql); + } + conn.execute(&sql, rusqlite::params_from_iter(values))?; + let pk: SqlVal = conn.query_row_and_then( + &format!( + "SELECT {} FROM {} WHERE ROWID = last_insert_rowid()", + pkcol.name(), + table + ), + [], + |row| sql_val_from_rusqlite(row.get_ref_unwrap(0), pkcol), + )?; + Ok(pk) +} +fn rs_conn_insert_only( + conn: &rusqlite::Connection, + table: &str, + columns: &[Column], + values: &[SqlValRef<'_>], +) -> Result<()> { + let mut sql = String::new(); + helper::sql_insert_with_placeholders( + table, + columns, + &mut SQLitePlaceholderSource::new(), + &mut sql, + ); + if cfg!(feature = "log") { + debug!("insert sql {}", sql); + } + conn.execute(&sql, rusqlite::params_from_iter(values))?; + Ok(()) +} +fn rs_conn_insert_or_replace( + conn: &rusqlite::Connection, + table: &str, + columns: &[Column], + _pkcol: &Column, + values: &[SqlValRef], +) -> Result<()> { + let mut sql = String::new(); + sql_insert_or_update(table, columns, &mut sql); + conn.execute(&sql, rusqlite::params_from_iter(values))?; + Ok(()) +} +fn rs_conn_update( + conn: &rusqlite::Connection, + table: &str, + pkcol: Column, + pk: SqlValRef, + columns: &[Column], + values: &[SqlValRef<'_>], +) -> Result<()> { + let mut sql = String::new(); + helper::sql_update_with_placeholders( + table, + pkcol, + columns, + &mut SQLitePlaceholderSource::new(), + &mut sql, + ); + let placeholder_values = [values, &[pk]].concat(); + if cfg!(feature = "log") { + debug!("update sql {}", sql); + } + conn.execute(&sql, rusqlite::params_from_iter(placeholder_values))?; + Ok(()) +} +fn rs_conn_delete_where(conn: &rusqlite::Connection, table: &str, expr: BoolExpr) -> Result { + let mut sql = String::new(); + let mut values: Vec = Vec::new(); + write!(&mut sql, "DELETE FROM {} WHERE ", table).unwrap(); + sql_for_expr( + query::Expr::Condition(Box::new(expr)), + &mut values, + &mut SQLitePlaceholderSource::new(), + &mut sql, + ); + let cnt = conn.execute(&sql, rusqlite::params_from_iter(values))?; + Ok(cnt) +} +fn rs_conn_has_table(conn: &rusqlite::Connection, table: &str) -> Result { + let mut stmt = conn.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?;")?; + let mut rows = stmt.query([table])?; + Ok(rows.next()?.is_some()) +} + +#[async_trait] +impl ConnectionMethods for Mutex { + fn execute(&self, sql: &str) -> Result<()> { + rs_conn_execute(self.lock()?.deref(), sql) + } + + async fn query<'a, 'b, 'c: 'a>( + &'c self, + table: &str, + columns: &'b [Column], + expr: Option, + limit: Option, + offset: Option, + order: Option<&[Order]>, + ) -> Result> { + rs_conn_query( + self.lock()?.deref(), + table, + columns, + expr, + limit, + offset, + order, + ) + } + fn insert_returning_pk( + &self, + table: &str, + columns: &[Column], + pkcol: &Column, + values: &[SqlValRef<'_>], + ) -> Result { + rs_conn_insert_returning_pk(self.lock()?.deref(), table, columns, pkcol, values) + } + fn insert_only(&self, table: &str, columns: &[Column], values: &[SqlValRef<'_>]) -> Result<()> { + rs_conn_insert_only(self.lock()?.deref(), table, columns, values) + } + fn insert_or_replace( + &self, + table: &str, + columns: &[Column], + pkcol: &Column, + values: &[SqlValRef], + ) -> Result<()> { + rs_conn_insert_or_replace(self.lock()?.deref(), table, columns, pkcol, values) + } + fn update( + &self, + table: &str, + pkcol: Column, + pk: SqlValRef, + columns: &[Column], + values: &[SqlValRef<'_>], + ) -> Result<()> { + rs_conn_update(self.lock()?.deref(), table, pkcol, pk, columns, values) + } + fn delete_where(&self, table: &str, expr: BoolExpr) -> Result { + rs_conn_delete_where(self.lock()?.deref(), table, expr) + } + fn has_table(&self, table: &str) -> Result { + rs_conn_has_table(&self.lock()?.deref(), table) + } +} + +struct SqliteTransaction<'c> { + trans: Option>>, +} +impl<'c> SqliteTransaction<'c> { + fn new(trans: rusqlite::Transaction<'c>) -> Self { + SqliteTransaction { + trans: Some(Mutex::new(trans)), + } + } + fn get(&self) -> Result<&Mutex>> { + match &self.trans { + None => Err(Self::already_consumed()), + Some(trans) => Ok(trans), + } + } + fn wrapped_connection_methods(&self) -> Result<&Mutex>> { + Ok(self.get()?) + } + fn already_consumed() -> Error { + Error::Internal("transaction has already been consumed".to_string()) + } +} + +#[async_trait] +impl ConnectionMethods for SqliteTransaction<'_> { + fn execute(&self, sql: &str) -> Result<()> { + rs_conn_execute(self.get()?.lock()?.deref(), sql) + } + + async fn query<'a, 'b, 'c: 'a>( + &'c self, + table: &str, + columns: &'b [Column], + expr: Option, + limit: Option, + offset: Option, + order: Option<&[Order]>, + ) -> Result> { + rs_conn_query( + self.get()?.lock()?.deref(), + table, + columns, + expr, + limit, + offset, + order, + ) + } + fn insert_returning_pk( + &self, + table: &str, + columns: &[Column], + pkcol: &Column, + values: &[SqlValRef<'_>], + ) -> Result { + rs_conn_insert_returning_pk(self.get()?.lock()?.deref(), table, columns, pkcol, values) + } + fn insert_only(&self, table: &str, columns: &[Column], values: &[SqlValRef<'_>]) -> Result<()> { + rs_conn_insert_only(self.get()?.lock()?.deref(), table, columns, values) + } + fn insert_or_replace( + &self, + table: &str, + columns: &[Column], + pkcol: &Column, + values: &[SqlValRef], + ) -> Result<()> { + rs_conn_insert_or_replace(self.get()?.lock()?.deref(), table, columns, pkcol, values) + } + fn update( + &self, + table: &str, + pkcol: Column, + pk: SqlValRef, + columns: &[Column], + values: &[SqlValRef<'_>], + ) -> Result<()> { + rs_conn_update( + self.get()?.lock()?.deref(), + table, + pkcol, + pk, + columns, + values, + ) + } + fn delete_where(&self, table: &str, expr: BoolExpr) -> Result { + rs_conn_delete_where(self.get()?.lock()?.deref(), table, expr) + } + fn has_table(&self, table: &str) -> Result { + rs_conn_has_table(&self.get()?.lock()?.deref(), table) + } +} + +#[async_trait] +impl<'c> BackendTransaction<'c> for SqliteTransaction<'c> { + fn commit(&mut self) -> Result<()> { + match self.trans.take() { + None => Err(Self::already_consumed()), + Some(trans) => Ok(trans.commit()?), + } + } + async fn rollback(&mut self) -> Result<()> { + match self.trans.take() { + None => Err(Self::already_consumed()), + Some(trans) => Ok(trans.rollback()?), + } + } + // Workaround for https://github.com/rust-lang/rfcs/issues/2765 + fn connection_methods(&self) -> &dyn ConnectionMethods { + self + } + fn connection_methods_mut(&mut self) -> &mut dyn ConnectionMethods { + self + } +} + +impl rusqlite::ToSql for SqlVal { + fn to_sql(&self) -> rusqlite::Result> { + Ok(sqlvalref_to_sqlite(&self.as_ref())) + } +} + +impl<'a> rusqlite::ToSql for SqlValRef<'a> { + fn to_sql<'b>(&'b self) -> rusqlite::Result> { + Ok(sqlvalref_to_sqlite(self)) + } +} + +fn sqlvalref_to_sqlite<'a, 'b>(valref: &'b SqlValRef<'a>) -> rusqlite::types::ToSqlOutput<'a> { + use rusqlite::types::{ToSqlOutput::Borrowed, ToSqlOutput::Owned, Value, ValueRef}; + use SqlValRef::*; + match valref { + Bool(b) => Owned(Value::Integer(*b as i64)), + Int(i) => Owned(Value::Integer(*i as i64)), + BigInt(i) => Owned(Value::Integer(*i)), + Real(r) => Owned(Value::Real(*r)), + Text(t) => Borrowed(ValueRef::Text(t.as_bytes())), + Blob(b) => Borrowed(ValueRef::Blob(b)), + #[cfg(feature = "datetime")] + Timestamp(dt) => { + let f = dt.format(SQLITE_DT_FORMAT); + Owned(Value::Text(f.to_string())) + } + Null => Owned(Value::Null), + Custom(_) => panic!("Custom types not supported in sqlite"), + } +} + +#[pin_project] +struct QueryAdapterInner<'a> { + stmt: rusqlite::Statement<'a>, + // will always be Some when the constructor has finished. We use an option only to get the + // stmt in place before we can reference it. + rows: Option>, +} + +impl<'a> QueryAdapterInner<'a> { + fn new(stmt: rusqlite::Statement<'a>, params: impl rusqlite::Params) -> Result>> { + let mut q = Box::pin(QueryAdapterInner { stmt, rows: None }); + unsafe { + //Soundness: we pin a QueryAdapterInner value containing + // both the stmt and the rows referencing the statement + // together. It is not possible to drop/move the stmt without + // bringing the referencing rows along with it. + let q_ref = Pin::get_unchecked_mut(Pin::as_mut(&mut q)); + let stmt_ref: *mut rusqlite::Statement<'a> = &mut q_ref.stmt; + q_ref.rows = Some((*stmt_ref).query(params)?) + } + Ok(q) + } + + fn next<'b>(self: Pin<&'b mut Self>) -> Result>> { + let this = self.project(); + let rows: &mut rusqlite::Rows<'a> = this.rows.as_mut().unwrap(); + Ok(rows.next()?) + } + + fn current(self: Pin<&Self>) -> Option<&rusqlite::Row> { + let this = self.project_ref(); + this.rows.as_ref().unwrap().get() + } +} + +struct QueryAdapter<'a> { + inner: Pin>>, +} +impl<'a> QueryAdapter<'a> { + fn new(stmt: rusqlite::Statement<'a>, params: impl rusqlite::Params) -> Result { + Ok(QueryAdapter { + inner: QueryAdapterInner::new(stmt, params)?, + }) + } +} + +impl<'a> BackendRows for QueryAdapter<'a> { + fn next<'b>(&'b mut self) -> Result> { + Ok(self + .inner + .as_mut() + .next()? + .map(|row| row as &dyn BackendRow)) + } + fn current<'b>(&'b self) -> Option<&'b (dyn BackendRow + 'b)> { + self.inner + .as_ref() + .current() + .map(|row| row as &dyn BackendRow) + } +} + +impl BackendRow for rusqlite::Row<'_> { + fn get(&self, idx: usize, ty: SqlType) -> Result { + sql_valref_from_rusqlite(self.get_ref(idx)?, &ty) + } + fn len(&self) -> usize { + self.as_ref().column_count() + } +} + +fn sql_for_expr( + expr: query::Expr, + values: &mut Vec, + pls: &mut SQLitePlaceholderSource, + w: &mut W, +) where + W: Write, +{ + helper::sql_for_expr(expr, sql_for_expr, values, pls, w) +} + +fn sql_val_from_rusqlite(val: rusqlite::types::ValueRef, col: &Column) -> Result { + sql_valref_from_rusqlite(val, col.ty()).map(|v| v.into()) +} + +fn sql_valref_from_rusqlite<'a>( + val: rusqlite::types::ValueRef<'a>, + ty: &SqlType, +) -> Result> { + if let rusqlite::types::ValueRef::Null = val { + return Ok(SqlValRef::Null); + } + Ok(match ty { + SqlType::Bool => SqlValRef::Bool(val.as_i64()? != 0), + SqlType::Int => SqlValRef::Int(val.as_i64()? as i32), + SqlType::BigInt => SqlValRef::BigInt(val.as_i64()?), + SqlType::Real => SqlValRef::Real(val.as_f64()?), + SqlType::Text => SqlValRef::Text(val.as_str()?), + #[cfg(feature = "datetime")] + SqlType::Timestamp => SqlValRef::Timestamp(NaiveDateTime::parse_from_str( + val.as_str()?, + SQLITE_DT_FORMAT, + )?), + SqlType::Blob => SqlValRef::Blob(val.as_blob()?), + SqlType::Custom(v) => { + return Err(Error::IncompatibleCustomT(v.deref().clone(), BACKEND_NAME)) + } + }) +} + +fn sql_for_op(current: &mut ADB, op: &Operation) -> Result { + match op { + Operation::AddTable(table) => Ok(create_table(table, false)), + Operation::AddTableIfNotExists(table) => Ok(create_table(table, true)), + Operation::RemoveTable(name) => Ok(drop_table(name)), + Operation::AddColumn(tbl, col) => add_column(tbl, col), + Operation::RemoveColumn(tbl, name) => Ok(remove_column(current, tbl, name)), + Operation::ChangeColumn(tbl, old, new) => Ok(change_column(current, tbl, old, Some(new))), + } +} + +fn create_table(table: &ATable, allow_exists: bool) -> String { + let coldefs = table + .columns + .iter() + .map(define_column) + .collect::>() + .join(",\n"); + let modifier = if allow_exists { "IF NOT EXISTS " } else { "" }; + format!("CREATE TABLE {}{} (\n{}\n);", modifier, table.name, coldefs) +} + +fn define_column(col: &AColumn) -> String { + let mut constraints: Vec = Vec::new(); + if !col.nullable() { + constraints.push("NOT NULL".to_string()); + } + if col.is_pk() { + constraints.push("PRIMARY KEY".to_string()); + } + if col.is_auto() && !col.is_pk() { + // integer primary key is automatically an alias for ROWID, + // and we only allow auto on integer types + constraints.push("AUTOINCREMENT".to_string()); + } + if col.unique() { + constraints.push("UNIQUE".to_string()); + } + format!( + "{} {} {}", + &col.name(), + col_sqltype(col), + constraints.join(" ") + ) +} + +fn col_sqltype(col: &AColumn) -> Cow { + match col.typeid() { + Ok(TypeIdentifier::Ty(ty)) => Cow::Borrowed(sqltype(&ty)), + Ok(TypeIdentifier::Name(name)) => Cow::Owned(name), + // sqlite doesn't actually require that the column type be + // specified + Err(_) => Cow::Borrowed(""), + } +} + +fn sqltype(ty: &SqlType) -> &'static str { + match ty { + SqlType::Bool => "INTEGER", + SqlType::Int => "INTEGER", + SqlType::BigInt => "INTEGER", + SqlType::Real => "REAL", + SqlType::Text => "TEXT", + #[cfg(feature = "datetime")] + SqlType::Timestamp => "TEXT", + SqlType::Blob => "BLOB", + SqlType::Custom(_) => panic!("Custom types not supported by sqlite backend"), + } +} + +fn drop_table(name: &str) -> String { + format!("DROP TABLE {};", name) +} + +fn add_column(tbl_name: &str, col: &AColumn) -> Result { + let default: SqlVal = helper::column_default(col)?; + Ok(format!( + "ALTER TABLE {} ADD COLUMN {} DEFAULT {};", + tbl_name, + define_column(col), + helper::sql_literal_value(default)? + )) +} + +fn remove_column(current: &mut ADB, tbl_name: &str, name: &str) -> String { + let old = current + .get_table(tbl_name) + .and_then(|table| table.column(name)) + .cloned(); + match old { + Some(col) => change_column(current, tbl_name, &col, None), + None => { + crate::warn!( + "Cannot remove column {} that does not exist from table {}", + name, + tbl_name + ); + "".to_string() + } + } +} + +fn copy_table(old: &ATable, new: &ATable) -> String { + let column_names = new + .columns + .iter() + .map(|col| col.name()) + .collect::>() + .join(", "); + format!( + "INSERT INTO {} SELECT {} FROM {};", + &new.name, column_names, &old.name + ) +} + +fn tmp_table_name(name: &str) -> String { + format!("{}__butane_tmp", name) +} + +fn change_column( + current: &mut ADB, + tbl_name: &str, + old: &AColumn, + new: Option<&AColumn>, +) -> String { + let table = current.get_table(tbl_name); + if table.is_none() { + crate::warn!( + "Cannot alter column {} from table {} that does not exist", + &old.name(), + tbl_name + ); + return "".to_string(); + } + let old_table = table.unwrap(); + let mut new_table = old_table.clone(); + new_table.name = tmp_table_name(&new_table.name); + match new { + Some(col) => new_table.replace_column(col.clone()), + None => new_table.remove_column(old.name()), + } + let stmts: [&str; 4] = [ + &create_table(&new_table, false), + ©_table(old_table, &new_table), + &drop_table(&old_table.name), + &format!("ALTER TABLE {} RENAME TO {};", &new_table.name, tbl_name), + ]; + let result = stmts.join("\n"); + new_table.name = old_table.name.clone(); + current.replace_table(new_table); + result +} + +pub fn sql_insert_or_update(table: &str, columns: &[Column], w: &mut impl Write) { + write!(w, "INSERT OR REPLACE ").unwrap(); + write!(w, "INTO {} (", table).unwrap(); + helper::list_columns(columns, w); + write!(w, ") VALUES (").unwrap(); + columns.iter().fold("", |sep, _| { + write!(w, "{}?", sep).unwrap(); + ", " + }); + write!(w, ")").unwrap(); +} + +struct SQLitePlaceholderSource {} +impl SQLitePlaceholderSource { + fn new() -> Self { + SQLitePlaceholderSource {} + } +} +impl helper::PlaceholderSource for SQLitePlaceholderSource { + fn next_placeholder(&mut self) -> Cow { + // sqlite placeholder is always a question mark. + Cow::Borrowed("?") + } +} diff --git a/butane_core/src/many.rs b/butane_core/src/many.rs index d9de07b5..67fc3c80 100644 --- a/butane_core/src/many.rs +++ b/butane_core/src/many.rs @@ -130,19 +130,15 @@ where /// Loads the values referred to by this foreign key from the /// database if necessary and returns a reference to them. pub async fn load(&self, conn: &impl ConnectionMethods) -> Result> { - let vals: Option<&Vec> = self.all_values.get(); - if let Some(vals) = vals { - // We already cached the value - return Ok(vals.iter()); - } - - //////////// - // load the value from the database - - let mut vals: Vec = match &self.owner { - None => Vec::new(), //if we don't have an owner then there are no values - Some(owner) => { - T::query() + let vals: Result<&Vec> = self + .all_values + .get_or_try_init(|| async { + //if we don't have an owner then there are no values + let owner: &SqlVal = match &self.owner { + Some(o) => o, + None => return Ok(Vec::new()), + }; + let mut vals = T::query() .filter(BoolExpr::Subquery { col: T::PKCOL, tbl2: self.item_table.clone(), @@ -150,24 +146,20 @@ where expr: Box::new(BoolExpr::Eq("owner", Expr::Val(owner.clone()))), }) .load(conn) - .await? - } - }; - // Now add in the values for things not saved to the db yet (if any) - if !self.new_values.is_empty() { - vals.append( - &mut T::query() - .filter(BoolExpr::In(T::PKCOL, self.new_values.clone())) - .load(conn) - .await?, - ); - } - - // cache what we loaded (and added to) - Ok(match self.all_values.try_insert(vals) { - Ok(v) => v.iter(), - Err((existing, v)) => existing.iter(), - }) + .await?; + // Now add in the values for things not saved to the db yet + if !self.new_values.is_empty() { + vals.append( + &mut T::query() + .filter(BoolExpr::In(T::PKCOL, self.new_values.clone())) + .load(conn) + .await?, + ); + } + Ok(vals) + }) + .await; + vals.map(|v| v.iter()) } pub fn columns(&self) -> [Column; 2] { [ From 7900468cf52037c7beffd5c6b727cfd95012f458 Mon Sep 17 00:00:00 2001 From: Ari Breitkreuz Date: Sun, 12 Feb 2023 13:52:49 +0100 Subject: [PATCH 13/78] compiles --- Cargo.lock | 28 +- butane_core/Cargo.toml | 3 +- butane_core/src/db/connmethods.rs | 20 +- butane_core/src/db/macros.rs | 26 +- butane_core/src/db/mod.rs | 40 +- butane_core/src/db/pg.rs | 159 ++--- butane_core/src/db/sqlite.rs | 618 -------------------- butane_core/src/fkey.rs | 27 +- butane_core/src/lib.rs | 8 +- butane_core/src/many.rs | 25 +- butane_core/src/migrations/fsmigrations.rs | 12 +- butane_core/src/migrations/memmigrations.rs | 5 +- butane_core/src/migrations/migration.rs | 25 +- butane_core/src/migrations/mod.rs | 14 +- butane_core/src/query/mod.rs | 4 +- butane_core/src/sqlval.rs | 2 +- 16 files changed, 202 insertions(+), 814 deletions(-) delete mode 100644 butane_core/src/db/sqlite.rs diff --git a/Cargo.lock b/Cargo.lock index da52a0f9..3e0295f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -146,12 +146,12 @@ dependencies = [ "log", "once_cell", "paste", - "postgres", "proc-macro2 1.0.47", "quote 1.0.21", "r2d2", "rusqlite", "serde_json", + "tokio-postgres", "uuid", ] @@ -183,18 +183,18 @@ dependencies = [ name = "butane_core" version = "0.5.0" dependencies = [ + "async-trait", "bytes", "cfg-if", "chrono", "fallible-iterator", "fallible-streaming-iterator", "fs2", + "futures-util", "hex", "log", "native-tls", - "once_cell", "pin-project", - "postgres", "postgres-native-tls", "proc-macro2 1.0.47", "quote 1.0.21", @@ -205,6 +205,8 @@ dependencies = [ "serde_json", "syn 1.0.103", "thiserror", + "tokio", + "tokio-postgres", "uuid", ] @@ -1027,20 +1029,6 @@ version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" -[[package]] -name = "postgres" -version = "0.19.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960c214283ef8f0027974c03e9014517ced5db12f021a9abb66185a5751fab0a" -dependencies = [ - "bytes", - "fallible-iterator", - "futures-util", - "log", - "tokio", - "tokio-postgres", -] - [[package]] name = "postgres-native-tls" version = "0.5.0" @@ -1504,9 +1492,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.21.2" +version = "1.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" +checksum = "597a12a59981d9e3c38d216785b0c37399f6e415e8d0712047620f189371b0bb" dependencies = [ "autocfg", "bytes", @@ -1515,7 +1503,7 @@ dependencies = [ "mio", "pin-project-lite", "socket2", - "winapi", + "windows-sys 0.42.0", ] [[package]] diff --git a/butane_core/Cargo.toml b/butane_core/Cargo.toml index 4586821e..50aef87a 100644 --- a/butane_core/Cargo.toml +++ b/butane_core/Cargo.toml @@ -25,7 +25,6 @@ fallible-iterator = "0.2" fallible-streaming-iterator = "0.1" fs2 = "0.4" # for file locks hex = "0.4" -once_cell="1.5" log = { version="0.4", optional=true } native-tls={ version = "0.2", optional = true } tokio-postgres={ version = "0.7", features=["with-chrono-0_4"], optional = true} @@ -43,3 +42,5 @@ thiserror = "1.0" chrono = { version = "0.4", features=["serde"], optional = true } uuid = {workspace=true, optional=true} async-trait = "0.1" +tokio = { version = "1.24", features=["rt", "sync"] } +futures-util = "0.3" diff --git a/butane_core/src/db/connmethods.rs b/butane_core/src/db/connmethods.rs index 5cffddbe..be37df71 100644 --- a/butane_core/src/db/connmethods.rs +++ b/butane_core/src/db/connmethods.rs @@ -13,7 +13,7 @@ use async_trait::async_trait; /// implemented by both database connections and transactions. #[async_trait] pub trait ConnectionMethods: Sync { - fn execute(&self, sql: &str) -> Result<()>; + async fn execute(&self, sql: &str) -> Result<()>; async fn query<'a, 'b, 'c: 'a>( &'c self, table: &str, @@ -23,7 +23,7 @@ pub trait ConnectionMethods: Sync { offset: Option, sort: Option<&[Order]>, ) -> Result>; - fn insert_returning_pk( + async fn insert_returning_pk( &self, table: &str, columns: &[Column], @@ -31,30 +31,30 @@ pub trait ConnectionMethods: Sync { values: &[SqlValRef<'_>], ) -> Result; /// Like `insert_returning_pk` but with no return value - fn insert_only(&self, table: &str, columns: &[Column], values: &[SqlValRef<'_>]) -> Result<()>; + async fn insert_only(&self, table: &str, columns: &[Column], values: &[SqlValRef<'_>]) -> Result<()>; /// Insert unless there's a conflict on the primary key column, in which case update - fn insert_or_replace( + async fn insert_or_replace( &self, table: &str, columns: &[Column], pkcol: &Column, values: &[SqlValRef<'_>], ) -> Result<()>; - fn update( + async fn update<'a>( &self, table: &str, pkcol: Column, - pk: SqlValRef, + pk: SqlValRef<'a>, columns: &[Column], values: &[SqlValRef<'_>], ) -> Result<()>; - fn delete(&self, table: &str, pkcol: &'static str, pk: SqlVal) -> Result<()> { - self.delete_where(table, BoolExpr::Eq(pkcol, Expr::Val(pk)))?; + async fn delete(&self, table: &str, pkcol: &'static str, pk: SqlVal) -> Result<()> { + self.delete_where(table, BoolExpr::Eq(pkcol, Expr::Val(pk))).await?; Ok(()) } - fn delete_where(&self, table: &str, expr: BoolExpr) -> Result; + async fn delete_where(&self, table: &str, expr: BoolExpr) -> Result; /// Tests if a table exists in the database. - fn has_table(&self, table: &str) -> Result; + async fn has_table(&self, table: &str) -> Result; } /// Represents a database column. Most users do not need to use this diff --git a/butane_core/src/db/macros.rs b/butane_core/src/db/macros.rs index a64fb9f7..1af8edbe 100644 --- a/butane_core/src/db/macros.rs +++ b/butane_core/src/db/macros.rs @@ -3,8 +3,8 @@ macro_rules! connection_method_wrapper { ($ty:path) => { #[async_trait::async_trait] impl ConnectionMethods for $ty { - fn execute(&self, sql: &str) -> Result<()> { - ConnectionMethods::execute(self.wrapped_connection_methods()?, sql) + async fn execute(&self, sql: &str) -> Result<()> { + ConnectionMethods::execute(self.wrapped_connection_methods()?, sql).await } async fn query<'a, 'b, 'c: 'a>( &'c self, @@ -18,7 +18,7 @@ macro_rules! connection_method_wrapper { self.wrapped_connection_methods()? .query(table, columns, expr, limit, offset, sort).await } - fn insert_returning_pk( + async fn insert_returning_pk( &self, table: &str, columns: &[Column], @@ -27,8 +27,9 @@ macro_rules! connection_method_wrapper { ) -> Result { self.wrapped_connection_methods()? .insert_returning_pk(table, columns, pkcol, values) + .await } - fn insert_only( + async fn insert_only( &self, table: &str, columns: &[Column], @@ -36,8 +37,9 @@ macro_rules! connection_method_wrapper { ) -> Result<()> { self.wrapped_connection_methods()? .insert_only(table, columns, values) + .await } - fn insert_or_replace( + async fn insert_or_replace( &self, table: &str, columns: &[Column], @@ -46,23 +48,25 @@ macro_rules! connection_method_wrapper { ) -> Result<()> { self.wrapped_connection_methods()? .insert_or_replace(table, columns, pkcol, values) + .await } - fn update( + async fn update<'a>( &self, table: &str, pkcol: Column, - pk: SqlValRef, + pk: SqlValRef<'a>, columns: &[Column], values: &[SqlValRef<'_>], ) -> Result<()> { self.wrapped_connection_methods()? .update(table, pkcol, pk, columns, values) + .await } - fn delete_where(&self, table: &str, expr: BoolExpr) -> Result { - self.wrapped_connection_methods()?.delete_where(table, expr) + async fn delete_where(&self, table: &str, expr: BoolExpr) -> Result { + self.wrapped_connection_methods()?.delete_where(table, expr).await } - fn has_table(&self, table: &str) -> Result { - self.wrapped_connection_methods()?.has_table(table) + async fn has_table(&self, table: &str) -> Result { + self.wrapped_connection_methods()?.has_table(table).await } } }; diff --git a/butane_core/src/db/mod.rs b/butane_core/src/db/mod.rs index 8f306a1e..3c5fbba6 100644 --- a/butane_core/src/db/mod.rs +++ b/butane_core/src/db/mod.rs @@ -27,8 +27,6 @@ mod helper; mod macros; #[cfg(feature = "pg")] pub mod pg; -#[cfg(feature = "sqlite")] -pub mod sqlite; #[cfg(feature = "r2d2")] mod r2; @@ -47,7 +45,7 @@ pub use connmethods::{ pub trait BackendConnection: ConnectionMethods + Send + 'static { /// Begin a database transaction. The transaction object must be /// used in place of this connection until it is committed and aborted. - fn transaction(&mut self) -> Result; + async fn transaction(&mut self) -> Result; /// Retrieve the backend backend this connection fn backend(&self) -> Box; fn backend_name(&self) -> &'static str; @@ -62,8 +60,8 @@ pub struct Connection { conn: Box, } impl Connection { - pub fn execute(&mut self, sql: impl AsRef) -> Result<()> { - self.conn.execute(sql.as_ref()) + pub async fn execute(&mut self, sql: impl AsRef) -> Result<()> { + self.conn.execute(sql.as_ref()).await } // For use with connection_method_wrapper macro #[allow(clippy::unnecessary_wraps)] @@ -71,9 +69,10 @@ impl Connection { Ok(self.conn.as_ref()) } } +#[async_trait] impl BackendConnection for Connection { - fn transaction(&mut self) -> Result { - self.conn.transaction() + async fn transaction(&mut self) -> Result { + self.conn.transaction().await } fn backend(&self) -> Box { self.conn.backend() @@ -131,12 +130,14 @@ fn conn_complete_if_dir(path: &Path) -> Cow { } /// Database backend. A boxed implementation can be returned by name via [get_backend][crate::db::get_backend]. -pub trait Backend { +#[async_trait] +pub trait Backend: Sync { fn name(&self) -> &'static str; fn create_migration_sql(&self, current: &adb::ADB, ops: Vec) -> Result; - fn connect(&self, conn_str: &str) -> Result; + async fn connect(&self, conn_str: &str) -> Result; } +#[async_trait] impl Backend for Box { fn name(&self) -> &'static str { self.deref().name() @@ -144,16 +145,14 @@ impl Backend for Box { fn create_migration_sql(&self, current: &adb::ADB, ops: Vec) -> Result { self.deref().create_migration_sql(current, ops) } - fn connect(&self, conn_str: &str) -> Result { - self.deref().connect(conn_str) + async fn connect(&self, conn_str: &str) -> Result { + self.deref().connect(conn_str).await } } /// Find a backend by name. pub fn get_backend(name: &str) -> Option> { match name { - #[cfg(feature = "sqlite")] - sqlite::BACKEND_NAME => Some(Box::new(sqlite::SQLiteBackend::new())), #[cfg(feature = "pg")] pg::BACKEND_NAME => Some(Box::new(pg::PgBackend::new())), _ => None, @@ -162,19 +161,20 @@ pub fn get_backend(name: &str) -> Option> { /// Connect to a database. For non-boxed connections, see individual /// [Backend][crate::db::Backend] implementations. -pub fn connect(spec: &ConnectionSpec) -> Result { +pub async fn connect(spec: &ConnectionSpec) -> Result { get_backend(&spec.backend_name) .ok_or_else(|| Error::UnknownBackend(spec.backend_name.clone()))? .connect(&spec.conn_str) + .await } #[async_trait] -trait BackendTransaction<'c>: ConnectionMethods { +trait BackendTransaction<'c>: ConnectionMethods + Send { /// Commit the transaction Unfortunately because we use this as a /// trait object, we can't consume self. It should be understood /// that no methods should be called after commit. This trait is /// not public, and that behavior is enforced by Transaction - fn commit(&mut self) -> Result<()>; + async fn commit(&mut self) -> Result<()>; /// Roll back the transaction. Same comment about consuming self as above. async fn rollback(&mut self) -> Result<()>; @@ -197,12 +197,12 @@ impl<'c> Transaction<'c> { Transaction { trans } } /// Commit the transaction - pub fn commit(mut self) -> Result<()> { - self.trans.deref_mut().commit() + pub async fn commit(mut self) -> Result<()> { + self.trans.commit().await } /// Roll back the transaction. Equivalent to dropping it. - pub fn rollback(mut self) -> Result<()> { - self.trans.deref_mut().rollback() + pub async fn rollback(mut self) -> Result<()> { + self.trans.deref_mut().rollback().await } // For use with connection_method_wrapper macro #[allow(clippy::unnecessary_wraps)] diff --git a/butane_core/src/db/pg.rs b/butane_core/src/db/pg.rs index 2f8c91d2..d1b8fe8c 100644 --- a/butane_core/src/db/pg.rs +++ b/butane_core/src/db/pg.rs @@ -9,12 +9,12 @@ use crate::{Result, SqlType, SqlVal, SqlValRef}; use bytes::BufMut; #[cfg(feature = "datetime")] use chrono::NaiveDateTime; -use tokio_postgres::fallible_iterator::FallibleIterator; use tokio_postgres::GenericClient; use tokio_postgres as postgres; -use std::cell::RefCell; use std::fmt::Write; use async_trait::async_trait; +use tokio; +use futures_util::stream::StreamExt; /// The name of the postgres backend. pub const BACKEND_NAME: &str = "pg"; @@ -28,10 +28,12 @@ impl PgBackend { } } impl PgBackend { - fn connect(&self, params: &str) -> Result { - PgConnection::open(params) + async fn connect(&self, params: &str) -> Result { + PgConnection::open(params).await } } + +#[async_trait] impl Backend for PgBackend { fn name(&self) -> &'static str { BACKEND_NAME @@ -46,24 +48,28 @@ impl Backend for PgBackend { .join("\n")) } - fn connect(&self, path: &str) -> Result { + async fn connect(&self, path: &str) -> Result { Ok(Connection { - conn: Box::new(self.connect(path)?), + conn: Box::new(self.connect(path).await?), }) } } +// type PgConnHandle = tokio::task::JoinHandle>; + /// Pg database connection. pub struct PgConnection { - conn: RefCell, + client: postgres::Client, + // Save the handle to the task running the connection to keep it alive + _conn_handle: tokio::task::JoinHandle<()>, } + impl PgConnection { - fn open(params: &str) -> Result { - Ok(PgConnection { - conn: RefCell::new(Self::connect(params)?), - }) + async fn open(params: &str) -> Result { + let (client, _conn_handle) = Self::connect(params).await?; + Ok( Self { client, _conn_handle }) } - fn connect(params: &str) -> Result { + async fn connect(params: &str) -> Result<(postgres::Client, tokio::task::JoinHandle<()>)> { cfg_if::cfg_if! { if #[cfg(feature = "tls")] { let connector = native_tls::TlsConnector::new()?; @@ -72,18 +78,27 @@ impl PgConnection { let connector = postgres::NoTls; } } - Ok(postgres::Client::connect(params, connector)?) + let (client, conn) = postgres::connect(params, connector).await?; + let conn_handle = tokio::spawn(async move { + if let Err(_e) = conn.await { + // TODO don't panic + panic!() + } + }); + Ok((client, conn_handle)) } } impl PgConnectionLike for PgConnection { type Client = postgres::Client; - fn cell(&self) -> Result<&RefCell> { - Ok(&self.conn) + fn client(&self) -> Result<&Self::Client> { + Ok(&self.client) } } + +#[async_trait] impl BackendConnection for PgConnection { - fn transaction(&mut self) -> Result> { - let trans: postgres::Transaction<'_> = self.conn.get_mut().transaction()?; + async fn transaction(&mut self) -> Result> { + let trans: postgres::Transaction<'_> = self.client.transaction().await?; let trans = Box::new(PgTransaction::new(trans)); Ok(Transaction::new(trans)) } @@ -94,7 +109,7 @@ impl BackendConnection for PgConnection { BACKEND_NAME } fn is_closed(&self) -> bool { - self.conn.borrow().is_closed() + self.client.is_closed() } } @@ -111,20 +126,20 @@ fn sqlvalref_for_pg_query<'a>(v: &'a SqlValRef<'a>) -> &'a dyn postgres::types:: /// Shared functionality between connection and /// transaction. Implementation detail. Semver exempt. pub trait PgConnectionLike { - type Client: postgres::GenericClient; - fn cell(&self) -> Result<&RefCell>; + type Client: postgres::GenericClient + Send + Sync; + fn client(&self) -> Result<&Self::Client>; } #[async_trait] impl ConnectionMethods for T where - T: PgConnectionLike, + T: PgConnectionLike + std::marker::Sync, { - fn execute(&self, sql: &str) -> Result<()> { + async fn execute(&self, sql: &str) -> Result<()> { if cfg!(feature = "log") { debug!("execute sql {}", sql); } - self.cell()?.try_borrow_mut()?.batch_execute(sql.as_ref())?; + self.client()?.batch_execute(sql.as_ref()).await?; Ok(()) } @@ -169,23 +184,22 @@ where let types: Vec = values.iter().map(pgtype_for_val).collect(); let stmt = self - .cell()? - .try_borrow_mut()? + .client()? .prepare_typed(&sqlquery, types.as_ref()).await?; - // todo avoid intermediate vec? - let rowvec: Vec = self - .cell()? - .try_borrow_mut()? - .query_raw(&stmt, values.iter().map(sqlval_for_pg_query))? - .map_err(Error::Postgres) - .map(|r| { - check_columns(&r, columns)?; - Ok(r) - }) - .collect()?; + let mut rowvec = Vec::::new(); + let rowstream = self + .client()? + .query_raw(&stmt, values.iter().map(sqlval_for_pg_query)).await + .map_err(Error::Postgres)?; + let mut rowstream = Box::pin(rowstream); + while let Some(r) = rowstream.next().await { + let r = r?; + check_columns(&r, columns)?; + rowvec.push(r); + } Ok(Box::new(VecRows::new(rowvec))) } - fn insert_returning_pk( + async fn insert_returning_pk( &self, table: &str, columns: &[Column], @@ -205,16 +219,13 @@ where } // use query instead of execute so we can get our result back - let pk: Option = self - .cell()? - .try_borrow_mut()? - .query_raw(sql.as_str(), values.iter().map(sqlvalref_for_pg_query))? - .map_err(Error::Postgres) - .map(|r| sql_val_from_postgres(&r, 0, pkcol)) - .nth(0)?; - pk.ok_or_else(|| Error::Internal("could not get pk".to_string())) - } - fn insert_only(&self, table: &str, columns: &[Column], values: &[SqlValRef<'_>]) -> Result<()> { + let pk_stream = self.client()? + .query_raw(sql.as_str(), values.iter().map(sqlvalref_for_pg_query)).await + .map_err(Error::Postgres)? + .map(|r| r.map(|x| sql_val_from_postgres(&x, 0, pkcol))); + Box::pin(pk_stream).next().await.ok_or(Error::Internal(("could not get pk").to_string()))?? + } + async fn insert_only(&self, table: &str, columns: &[Column], values: &[SqlValRef<'_>]) -> Result<()> { let mut sql = String::new(); helper::sql_insert_with_placeholders( table, @@ -223,31 +234,29 @@ where &mut sql, ); let params: Vec<&DynToSqlPg> = values.iter().map(|v| v as &DynToSqlPg).collect(); - self.cell()? - .try_borrow_mut()? - .execute(sql.as_str(), params.as_slice())?; + self.client()? + .execute(sql.as_str(), params.as_slice()).await?; Ok(()) } - fn insert_or_replace<'a>( + async fn insert_or_replace( &self, table: &str, columns: &[Column], pkcol: &Column, - values: &[SqlValRef<'a>], + values: &[SqlValRef<'_>], ) -> Result<()> { let mut sql = String::new(); sql_insert_or_replace_with_placeholders(table, columns, pkcol, &mut sql); let params: Vec<&DynToSqlPg> = values.iter().map(|v| v as &DynToSqlPg).collect(); - self.cell()? - .try_borrow_mut()? - .execute(sql.as_str(), params.as_slice())?; + self.client()? + .execute(sql.as_str(), params.as_slice()).await?; Ok(()) } - fn update( + async fn update<'a>( &self, table: &str, pkcol: Column, - pk: SqlValRef, + pk: SqlValRef<'a>, columns: &[Column], values: &[SqlValRef<'_>], ) -> Result<()> { @@ -267,12 +276,10 @@ where if cfg!(feature = "log") { debug!("update sql {}", sql); } - self.cell()? - .try_borrow_mut()? - .execute(sql.as_str(), params.as_slice())?; + self.client()?.execute(sql.as_str(), params.as_slice()).await?; Ok(()) } - fn delete_where(&self, table: &str, expr: BoolExpr) -> Result { + async fn delete_where(&self, table: &str, expr: BoolExpr) -> Result { let mut sql = String::new(); let mut values: Vec = Vec::new(); write!(&mut sql, "DELETE FROM {} WHERE ", table).unwrap(); @@ -284,35 +291,33 @@ where ); let params: Vec<&DynToSqlPg> = values.iter().map(|v| v as &DynToSqlPg).collect(); let cnt = self - .cell()? - .try_borrow_mut()? - .execute(sql.as_str(), params.as_slice())?; + .client()? + .execute(sql.as_str(), params.as_slice()).await?; Ok(cnt as usize) } - fn has_table(&self, table: &str) -> Result { + async fn has_table(&self, table: &str) -> Result { // future improvement, should be schema-aware let stmt = self - .cell()? - .try_borrow_mut()? - .prepare("SELECT table_name FROM information_schema.tables WHERE table_name=$1;")?; - let rows = self.cell()?.try_borrow_mut()?.query(&stmt, &[&table])?; + .client()? + .prepare("SELECT table_name FROM information_schema.tables WHERE table_name=$1;").await?; + let rows = self.client()?.query(&stmt, &[&table]).await?; Ok(!rows.is_empty()) } } struct PgTransaction<'c> { - trans: Option>>, + trans: Option>, } impl<'c> PgTransaction<'c> { fn new(trans: postgres::Transaction<'c>) -> Self { PgTransaction { - trans: Some(RefCell::new(trans)), + trans: Some(trans), } } - fn get(&self) -> Result<&RefCell>> { + fn get(&self) -> Result<&postgres::Transaction<'c>> { match &self.trans { + Some(x) => Ok(x), None => Err(Self::already_consumed()), - Some(trans) => Ok(trans), } } fn already_consumed() -> Error { @@ -321,24 +326,24 @@ impl<'c> PgTransaction<'c> { } impl<'c> PgConnectionLike for PgTransaction<'c> { type Client = postgres::Transaction<'c>; - fn cell(&self) -> Result<&RefCell> { + fn client(&self) -> Result<&Self::Client> { self.get() } } #[async_trait] impl<'c> BackendTransaction<'c> for PgTransaction<'c> { - fn commit(&mut self) -> Result<()> { + async fn commit(&mut self) -> Result<()> { match self.trans.take() { None => Err(Self::already_consumed()), - Some(trans) => Ok(trans.into_inner().commit()?), + Some(trans) => Ok(trans.commit().await?), } } async fn rollback(&mut self) -> Result<()> { match self.trans.take() { None => Err(Self::already_consumed()), - Some(trans) => Ok(trans.into_inner().rollback().await?), + Some(trans) => Ok(trans.rollback().await?), } } // Workaround for https://github.com/rust-lang/rfcs/issues/2765 diff --git a/butane_core/src/db/sqlite.rs b/butane_core/src/db/sqlite.rs deleted file mode 100644 index 7f4a11d1..00000000 --- a/butane_core/src/db/sqlite.rs +++ /dev/null @@ -1,618 +0,0 @@ -//! SQLite database backend -use super::helper; -use super::*; -use crate::db::connmethods::BackendRows; -use crate::debug; -use crate::migrations::adb::{AColumn, ATable, Operation, TypeIdentifier, ADB}; -use crate::query; -use crate::query::Order; -use crate::{Result, SqlType, SqlVal, SqlValRef}; -#[cfg(feature = "datetime")] -use chrono::naive::NaiveDateTime; -use fallible_streaming_iterator::FallibleStreamingIterator; -use pin_project::pin_project; -use std::borrow::Cow; -use std::fmt::Write; -use std::pin::Pin; -use async_trait::async_trait; - -#[cfg(feature = "datetime")] -const SQLITE_DT_FORMAT: &str = "%Y-%m-%d %H:%M:%S"; - -/// The name of the sqlite backend. -pub const BACKEND_NAME: &str = "sqlite"; - -/// SQLite [Backend][crate::db::Backend] implementation. -#[derive(Default)] -pub struct SQLiteBackend {} -impl SQLiteBackend { - pub fn new() -> SQLiteBackend { - SQLiteBackend {} - } -} -impl SQLiteBackend { - fn connect(&self, path: &str) -> Result { - SQLiteConnection::open(Path::new(path)) - } -} -impl Backend for SQLiteBackend { - fn name(&self) -> &'static str { - BACKEND_NAME - } - - fn create_migration_sql(&self, current: &ADB, ops: Vec) -> Result { - let mut current: ADB = (*current).clone(); - Ok(ops - .into_iter() - .map(|o| { - let sql = sql_for_op(&mut current, &o); - current.transform_with(o); - sql - }) - .collect::>>()? - .join("\n")) - } - - fn connect(&self, path: &str) -> Result { - Ok(Connection { - conn: Box::new(self.connect(path)?), - }) - } -} - -/// SQLite database connection. -pub struct SQLiteConnection { - conn: std::sync::Mutex, -} -impl SQLiteConnection { - fn open(path: impl AsRef) -> Result { - rusqlite::Connection::open(path) - .map(|conn| SQLiteConnection { conn }) - .map_err(|e| e.into()) - } - - // For use with connection_method_wrapper macro - #[allow(clippy::unnecessary_wraps)] - fn wrapped_connection_methods(&self) -> Result<&rusqlite::Connection> { - Ok(&self.conn) - } -} -connection_method_wrapper!(SQLiteConnection); - -impl BackendConnection for SQLiteConnection { - fn transaction(&mut self) -> Result> { - let trans: rusqlite::Transaction<'_> = self.conn.transaction()?; - let trans = Box::new(SqliteTransaction::new(trans)); - Ok(Transaction::new(trans)) - } - fn backend(&self) -> Box { - Box::new(SQLiteBackend {}) - } - fn backend_name(&self) -> &'static str { - "sqlite" - } - fn is_closed(&self) -> bool { - false - } -} - -#[async_trait] -impl ConnectionMethods for rusqlite::Connection { - fn execute(&self, sql: &str) -> Result<()> { - if cfg!(feature = "log") { - debug!("execute sql {}", sql); - } - self.execute_batch(sql.as_ref())?; - Ok(()) - } - - async fn query<'a, 'b, 'c: 'a>( - &'c self, - table: &str, - columns: &'b [Column], - expr: Option, - limit: Option, - offset: Option, - order: Option<&[Order]>, - ) -> Result> { - let mut sqlquery = String::new(); - helper::sql_select(columns, table, &mut sqlquery); - let mut values: Vec = Vec::new(); - if let Some(expr) = expr { - sqlquery.write_str(" WHERE ").unwrap(); - sql_for_expr( - query::Expr::Condition(Box::new(expr)), - &mut values, - &mut SQLitePlaceholderSource::new(), - &mut sqlquery, - ); - } - - if let Some(order) = order { - helper::sql_order(order, &mut sqlquery) - } - - if let Some(limit) = limit { - helper::sql_limit(limit, &mut sqlquery) - } - - if let Some(offset) = offset { - if limit.is_none() { - // Sqlite only supports offset in conjunction with - // limit, so add a max limit if we don't have one - // already. - helper::sql_limit(i32::MAX, &mut sqlquery) - } - helper::sql_offset(offset, &mut sqlquery) - } - - debug!("query sql {}", sqlquery); - - let stmt = self.prepare(&sqlquery)?; - let adapter = QueryAdapter::new(stmt, rusqlite::params_from_iter(values))?; - Ok(Box::new(adapter)) - } - fn insert_returning_pk( - &self, - table: &str, - columns: &[Column], - pkcol: &Column, - values: &[SqlValRef<'_>], - ) -> Result { - let mut sql = String::new(); - helper::sql_insert_with_placeholders( - table, - columns, - &mut SQLitePlaceholderSource::new(), - &mut sql, - ); - if cfg!(feature = "log") { - debug!("insert sql {}", sql); - } - self.execute(&sql, rusqlite::params_from_iter(values))?; - let pk: SqlVal = self.query_row_and_then( - &format!( - "SELECT {} FROM {} WHERE ROWID = last_insert_rowid()", - pkcol.name(), - table - ), - [], - |row| sql_val_from_rusqlite(row.get_ref_unwrap(0), pkcol), - )?; - Ok(pk) - } - fn insert_only(&self, table: &str, columns: &[Column], values: &[SqlValRef<'_>]) -> Result<()> { - let mut sql = String::new(); - helper::sql_insert_with_placeholders( - table, - columns, - &mut SQLitePlaceholderSource::new(), - &mut sql, - ); - if cfg!(feature = "log") { - debug!("insert sql {}", sql); - } - self.execute(&sql, rusqlite::params_from_iter(values))?; - Ok(()) - } - fn insert_or_replace( - &self, - table: &str, - columns: &[Column], - _pkcol: &Column, - values: &[SqlValRef], - ) -> Result<()> { - let mut sql = String::new(); - sql_insert_or_update(table, columns, &mut sql); - self.execute(&sql, rusqlite::params_from_iter(values))?; - Ok(()) - } - fn update( - &self, - table: &str, - pkcol: Column, - pk: SqlValRef, - columns: &[Column], - values: &[SqlValRef<'_>], - ) -> Result<()> { - let mut sql = String::new(); - helper::sql_update_with_placeholders( - table, - pkcol, - columns, - &mut SQLitePlaceholderSource::new(), - &mut sql, - ); - let placeholder_values = [values, &[pk]].concat(); - if cfg!(feature = "log") { - debug!("update sql {}", sql); - } - self.execute(&sql, rusqlite::params_from_iter(placeholder_values))?; - Ok(()) - } - fn delete_where(&self, table: &str, expr: BoolExpr) -> Result { - let mut sql = String::new(); - let mut values: Vec = Vec::new(); - write!(&mut sql, "DELETE FROM {} WHERE ", table).unwrap(); - sql_for_expr( - query::Expr::Condition(Box::new(expr)), - &mut values, - &mut SQLitePlaceholderSource::new(), - &mut sql, - ); - let cnt = self.execute(&sql, rusqlite::params_from_iter(values))?; - Ok(cnt) - } - fn has_table(&self, table: &str) -> Result { - let mut stmt = - self.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?;")?; - let mut rows = stmt.query([table])?; - Ok(rows.next()?.is_some()) - } -} - -struct SqliteTransaction<'c> { - trans: std::sync::Mutex>>, -} -impl<'c> SqliteTransaction<'c> { - fn new(trans: rusqlite::Transaction<'c>) -> Self { - SqliteTransaction { trans: Some(trans) } - } - fn get(&self) -> Result<&rusqlite::Transaction<'c>> { - match &self.trans { - None => Err(Self::already_consumed()), - Some(trans) => Ok(trans), - } - } - fn wrapped_connection_methods(&self) -> Result<&rusqlite::Connection> { - Ok(self.get()?.deref()) - } - fn already_consumed() -> Error { - Error::Internal("transaction has already been consumed".to_string()) - } -} -connection_method_wrapper!(SqliteTransaction<'_>); - -#[async_trait] -impl<'c> BackendTransaction<'c> for SqliteTransaction<'c> { - fn commit(&mut self) -> Result<()> { - match self.trans.take().take() { - None => Err(Self::already_consumed()), - Some(trans) => Ok(trans.commit()?), - } - } - async fn rollback(&mut self) -> Result<()> { - match self.trans.take() { - None => Err(Self::already_consumed()), - Some(trans) => Ok(trans.rollback()?), - } - } - // Workaround for https://github.com/rust-lang/rfcs/issues/2765 - fn connection_methods(&self) -> &dyn ConnectionMethods { - self - } - fn connection_methods_mut(&mut self) -> &mut dyn ConnectionMethods { - self - } -} - -impl rusqlite::ToSql for SqlVal { - fn to_sql(&self) -> rusqlite::Result> { - Ok(sqlvalref_to_sqlite(&self.as_ref())) - } -} - -impl<'a> rusqlite::ToSql for SqlValRef<'a> { - fn to_sql<'b>(&'b self) -> rusqlite::Result> { - Ok(sqlvalref_to_sqlite(self)) - } -} - -fn sqlvalref_to_sqlite<'a, 'b>(valref: &'b SqlValRef<'a>) -> rusqlite::types::ToSqlOutput<'a> { - use rusqlite::types::{ToSqlOutput::Borrowed, ToSqlOutput::Owned, Value, ValueRef}; - use SqlValRef::*; - match valref { - Bool(b) => Owned(Value::Integer(*b as i64)), - Int(i) => Owned(Value::Integer(*i as i64)), - BigInt(i) => Owned(Value::Integer(*i)), - Real(r) => Owned(Value::Real(*r)), - Text(t) => Borrowed(ValueRef::Text(t.as_bytes())), - Blob(b) => Borrowed(ValueRef::Blob(b)), - #[cfg(feature = "datetime")] - Timestamp(dt) => { - let f = dt.format(SQLITE_DT_FORMAT); - Owned(Value::Text(f.to_string())) - } - Null => Owned(Value::Null), - Custom(_) => panic!("Custom types not supported in sqlite"), - } -} - -#[pin_project] -struct QueryAdapterInner<'a> { - stmt: rusqlite::Statement<'a>, - // will always be Some when the constructor has finished. We use an option only to get the - // stmt in place before we can reference it. - rows: Option>, -} - -impl<'a> QueryAdapterInner<'a> { - fn new(stmt: rusqlite::Statement<'a>, params: impl rusqlite::Params) -> Result>> { - let mut q = Box::pin(QueryAdapterInner { stmt, rows: None }); - unsafe { - //Soundness: we pin a QueryAdapterInner value containing - // both the stmt and the rows referencing the statement - // together. It is not possible to drop/move the stmt without - // bringing the referencing rows along with it. - let q_ref = Pin::get_unchecked_mut(Pin::as_mut(&mut q)); - let stmt_ref: *mut rusqlite::Statement<'a> = &mut q_ref.stmt; - q_ref.rows = Some((*stmt_ref).query(params)?) - } - Ok(q) - } - - fn next<'b>(self: Pin<&'b mut Self>) -> Result>> { - let this = self.project(); - let rows: &mut rusqlite::Rows<'a> = this.rows.as_mut().unwrap(); - Ok(rows.next()?) - } - - fn current(self: Pin<&Self>) -> Option<&rusqlite::Row> { - let this = self.project_ref(); - this.rows.as_ref().unwrap().get() - } -} - -struct QueryAdapter<'a> { - inner: Pin>>, -} -impl<'a> QueryAdapter<'a> { - fn new(stmt: rusqlite::Statement<'a>, params: impl rusqlite::Params) -> Result { - Ok(QueryAdapter { - inner: QueryAdapterInner::new(stmt, params)?, - }) - } -} - -impl<'a> BackendRows for QueryAdapter<'a> { - fn next<'b>(&'b mut self) -> Result> { - Ok(self - .inner - .as_mut() - .next()? - .map(|row| row as &dyn BackendRow)) - } - fn current<'b>(&'b self) -> Option<&'b (dyn BackendRow + 'b)> { - self.inner - .as_ref() - .current() - .map(|row| row as &dyn BackendRow) - } -} - -impl BackendRow for rusqlite::Row<'_> { - fn get(&self, idx: usize, ty: SqlType) -> Result { - sql_valref_from_rusqlite(self.get_ref(idx)?, &ty) - } - fn len(&self) -> usize { - self.as_ref().column_count() - } -} - -fn sql_for_expr( - expr: query::Expr, - values: &mut Vec, - pls: &mut SQLitePlaceholderSource, - w: &mut W, -) where - W: Write, -{ - helper::sql_for_expr(expr, sql_for_expr, values, pls, w) -} - -fn sql_val_from_rusqlite(val: rusqlite::types::ValueRef, col: &Column) -> Result { - sql_valref_from_rusqlite(val, col.ty()).map(|v| v.into()) -} - -fn sql_valref_from_rusqlite<'a>( - val: rusqlite::types::ValueRef<'a>, - ty: &SqlType, -) -> Result> { - if let rusqlite::types::ValueRef::Null = val { - return Ok(SqlValRef::Null); - } - Ok(match ty { - SqlType::Bool => SqlValRef::Bool(val.as_i64()? != 0), - SqlType::Int => SqlValRef::Int(val.as_i64()? as i32), - SqlType::BigInt => SqlValRef::BigInt(val.as_i64()?), - SqlType::Real => SqlValRef::Real(val.as_f64()?), - SqlType::Text => SqlValRef::Text(val.as_str()?), - #[cfg(feature = "datetime")] - SqlType::Timestamp => SqlValRef::Timestamp(NaiveDateTime::parse_from_str( - val.as_str()?, - SQLITE_DT_FORMAT, - )?), - SqlType::Blob => SqlValRef::Blob(val.as_blob()?), - SqlType::Custom(v) => { - return Err(Error::IncompatibleCustomT(v.deref().clone(), BACKEND_NAME)) - } - }) -} - -fn sql_for_op(current: &mut ADB, op: &Operation) -> Result { - match op { - Operation::AddTable(table) => Ok(create_table(table, false)), - Operation::AddTableIfNotExists(table) => Ok(create_table(table, true)), - Operation::RemoveTable(name) => Ok(drop_table(name)), - Operation::AddColumn(tbl, col) => add_column(tbl, col), - Operation::RemoveColumn(tbl, name) => Ok(remove_column(current, tbl, name)), - Operation::ChangeColumn(tbl, old, new) => Ok(change_column(current, tbl, old, Some(new))), - } -} - -fn create_table(table: &ATable, allow_exists: bool) -> String { - let coldefs = table - .columns - .iter() - .map(define_column) - .collect::>() - .join(",\n"); - let modifier = if allow_exists { "IF NOT EXISTS " } else { "" }; - format!("CREATE TABLE {}{} (\n{}\n);", modifier, table.name, coldefs) -} - -fn define_column(col: &AColumn) -> String { - let mut constraints: Vec = Vec::new(); - if !col.nullable() { - constraints.push("NOT NULL".to_string()); - } - if col.is_pk() { - constraints.push("PRIMARY KEY".to_string()); - } - if col.is_auto() && !col.is_pk() { - // integer primary key is automatically an alias for ROWID, - // and we only allow auto on integer types - constraints.push("AUTOINCREMENT".to_string()); - } - if col.unique() { - constraints.push("UNIQUE".to_string()); - } - format!( - "{} {} {}", - &col.name(), - col_sqltype(col), - constraints.join(" ") - ) -} - -fn col_sqltype(col: &AColumn) -> Cow { - match col.typeid() { - Ok(TypeIdentifier::Ty(ty)) => Cow::Borrowed(sqltype(&ty)), - Ok(TypeIdentifier::Name(name)) => Cow::Owned(name), - // sqlite doesn't actually require that the column type be - // specified - Err(_) => Cow::Borrowed(""), - } -} - -fn sqltype(ty: &SqlType) -> &'static str { - match ty { - SqlType::Bool => "INTEGER", - SqlType::Int => "INTEGER", - SqlType::BigInt => "INTEGER", - SqlType::Real => "REAL", - SqlType::Text => "TEXT", - #[cfg(feature = "datetime")] - SqlType::Timestamp => "TEXT", - SqlType::Blob => "BLOB", - SqlType::Custom(_) => panic!("Custom types not supported by sqlite backend"), - } -} - -fn drop_table(name: &str) -> String { - format!("DROP TABLE {};", name) -} - -fn add_column(tbl_name: &str, col: &AColumn) -> Result { - let default: SqlVal = helper::column_default(col)?; - Ok(format!( - "ALTER TABLE {} ADD COLUMN {} DEFAULT {};", - tbl_name, - define_column(col), - helper::sql_literal_value(default)? - )) -} - -fn remove_column(current: &mut ADB, tbl_name: &str, name: &str) -> String { - let old = current - .get_table(tbl_name) - .and_then(|table| table.column(name)) - .cloned(); - match old { - Some(col) => change_column(current, tbl_name, &col, None), - None => { - crate::warn!( - "Cannot remove column {} that does not exist from table {}", - name, - tbl_name - ); - "".to_string() - } - } -} - -fn copy_table(old: &ATable, new: &ATable) -> String { - let column_names = new - .columns - .iter() - .map(|col| col.name()) - .collect::>() - .join(", "); - format!( - "INSERT INTO {} SELECT {} FROM {};", - &new.name, column_names, &old.name - ) -} - -fn tmp_table_name(name: &str) -> String { - format!("{}__butane_tmp", name) -} - -fn change_column( - current: &mut ADB, - tbl_name: &str, - old: &AColumn, - new: Option<&AColumn>, -) -> String { - let table = current.get_table(tbl_name); - if table.is_none() { - crate::warn!( - "Cannot alter column {} from table {} that does not exist", - &old.name(), - tbl_name - ); - return "".to_string(); - } - let old_table = table.unwrap(); - let mut new_table = old_table.clone(); - new_table.name = tmp_table_name(&new_table.name); - match new { - Some(col) => new_table.replace_column(col.clone()), - None => new_table.remove_column(old.name()), - } - let stmts: [&str; 4] = [ - &create_table(&new_table, false), - ©_table(old_table, &new_table), - &drop_table(&old_table.name), - &format!("ALTER TABLE {} RENAME TO {};", &new_table.name, tbl_name), - ]; - let result = stmts.join("\n"); - new_table.name = old_table.name.clone(); - current.replace_table(new_table); - result -} - -pub fn sql_insert_or_update(table: &str, columns: &[Column], w: &mut impl Write) { - write!(w, "INSERT OR REPLACE ").unwrap(); - write!(w, "INTO {} (", table).unwrap(); - helper::list_columns(columns, w); - write!(w, ") VALUES (").unwrap(); - columns.iter().fold("", |sep, _| { - write!(w, "{}?", sep).unwrap(); - ", " - }); - write!(w, ")").unwrap(); -} - -struct SQLitePlaceholderSource {} -impl SQLitePlaceholderSource { - fn new() -> Self { - SQLitePlaceholderSource {} - } -} -impl helper::PlaceholderSource for SQLitePlaceholderSource { - fn next_placeholder(&mut self) -> Cow { - // sqlite placeholder is always a question mark. - Cow::Borrowed("?") - } -} diff --git a/butane_core/src/fkey.rs b/butane_core/src/fkey.rs index 834e12a8..c48e23d4 100644 --- a/butane_core/src/fkey.rs +++ b/butane_core/src/fkey.rs @@ -1,6 +1,6 @@ use crate::db::ConnectionMethods; use crate::*; -use once_cell::unsync::OnceCell; +use tokio::sync::OnceCell; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::borrow::Cow; use std::fmt::{Debug, Formatter}; @@ -55,17 +55,6 @@ impl ForeignKey { } } - /// Loads the value referred to by this foreign key from the - /// database if necessary and returns a reference to it. - pub fn load(&self, conn: &impl ConnectionMethods) -> Result<&T> { - self.val - .get_or_try_init(|| { - let pk = self.valpk.get().unwrap(); - T::get(conn, &T::PKType::from_sql_ref(pk.as_ref())?).map(Box::new) - }) - .map(|v| v.as_ref()) - } - fn new_raw() -> Self { ForeignKey { val: OnceCell::new(), @@ -85,6 +74,20 @@ impl ForeignKey { } } +impl ForeignKey { + /// Loads the value referred to by this foreign key from the + /// database if necessary and returns a reference to it. + pub async fn load(&self, conn: &impl ConnectionMethods) -> Result<&T> { + self.val + .get_or_try_init(|| async { + let pk = self.valpk.get().unwrap(); + T::get(conn, &T::PKType::from_sql_ref(pk.as_ref())?).await.map(Box::new) + }) + .await + .map(|v| v.as_ref()) + } +} + impl From for ForeignKey { fn from(obj: T) -> Self { let ret = Self::new_raw(); diff --git a/butane_core/src/lib.rs b/butane_core/src/lib.rs index 2548f436..748b750a 100644 --- a/butane_core/src/lib.rs +++ b/butane_core/src/lib.rs @@ -71,7 +71,7 @@ pub trait DataResult: Sized { /// Rather than implementing this type manually, use the /// `#[model]` attribute. #[async_trait] -pub trait DataObject: DataResult { +pub trait DataObject: DataResult + Sync { /// The type of the primary key field. type PKType: PrimaryKeyType; type Fields: Default; @@ -85,7 +85,7 @@ pub trait DataObject: DataResult { /// Get the primary key fn pk(&self) -> &Self::PKType; /// Find this object in the database based on primary key. - async fn get(conn: &impl ConnectionMethods, id: impl Borrow) -> Result + async fn get(conn: &impl ConnectionMethods, id: impl Borrow + Send + Sync) -> Result where Self: Sized, Self::PKType: Sync, @@ -103,9 +103,9 @@ pub trait DataObject: DataResult { } /// Save the object to the database. - fn save(&mut self, conn: &impl ConnectionMethods) -> Result<()>; + async fn save(&mut self, conn: &impl ConnectionMethods) -> Result<()>; /// Delete the object from the database. - fn delete(&self, conn: &impl ConnectionMethods) -> Result<()>; + async fn delete(&self, conn: &impl ConnectionMethods) -> Result<()>; } pub trait ModelTyped { diff --git a/butane_core/src/many.rs b/butane_core/src/many.rs index c6969f69..58e404a8 100644 --- a/butane_core/src/many.rs +++ b/butane_core/src/many.rs @@ -1,12 +1,13 @@ use crate::db::{Column, ConnectionMethods}; use crate::query::{BoolExpr, Expr}; use crate::{DataObject, Error, FieldType, Result, SqlType, SqlVal, ToSql}; -use once_cell::unsync::OnceCell; +use tokio::sync::OnceCell; use serde::{Deserialize, Serialize}; use std::borrow::Cow; fn default_oc() -> OnceCell> { - OnceCell::default() + // Same as impl Default for once_cell::unsync::OnceCell + OnceCell::new() } /// Used to implement a many-to-many relationship between models. @@ -102,7 +103,7 @@ where } /// Used by macro-generated code. You do not need to call this directly. - pub fn save(&mut self, conn: &impl ConnectionMethods) -> Result<()> { + pub async fn save(&mut self, conn: &impl ConnectionMethods) -> Result<()> { let owner = self.owner.as_ref().ok_or(Error::NotInitialized)?; while !self.new_values.is_empty() { conn.insert_only( @@ -112,13 +113,13 @@ where owner.as_ref(), self.new_values.pop().unwrap().as_ref().clone(), ], - )?; + ).await?; } if !self.removed_values.is_empty() { conn.delete_where( &self.item_table, BoolExpr::In("has", std::mem::take(&mut self.removed_values)), - )?; + ).await?; } self.new_values.clear(); Ok(()) @@ -126,31 +127,31 @@ where /// Loads the values referred to by this foreign key from the /// database if necessary and returns a reference to them. - pub fn load(&self, conn: &impl ConnectionMethods) -> Result> { - let vals: Result<&Vec> = self.all_values.get_or_try_init(|| { + pub async fn load(&self, conn: &impl ConnectionMethods) -> Result> { + let vals: Result<&Vec> = self.all_values.get_or_try_init(|| async { //if we don't have an owner then there are no values let owner: &SqlVal = match &self.owner { Some(o) => o, None => return Ok(Vec::new()), }; - let mut vals = T::query() + let mut vals = T::query().await .filter(BoolExpr::Subquery { col: T::PKCOL, tbl2: self.item_table.clone(), tbl2_col: "has", expr: Box::new(BoolExpr::Eq("owner", Expr::Val(owner.clone()))), }) - .load(conn)?; + .load(conn).await?; // Now add in the values for things not saved to the db yet if !self.new_values.is_empty() { vals.append( - &mut T::query() + &mut T::query().await .filter(BoolExpr::In(T::PKCOL, self.new_values.clone())) - .load(conn)?, + .load(conn).await?, ); } Ok(vals) - }); + }).await; vals.map(|v| v.iter()) } pub fn columns(&self) -> [Column; 2] { diff --git a/butane_core/src/migrations/fsmigrations.rs b/butane_core/src/migrations/fsmigrations.rs index 328ac66d..a52d8dac 100644 --- a/butane_core/src/migrations/fsmigrations.rs +++ b/butane_core/src/migrations/fsmigrations.rs @@ -10,7 +10,6 @@ use std::fs::{File, OpenOptions}; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; -use std::rc::Rc; type SqlTypeMap = BTreeMap; const TYPES_FILENAME: &str = "types.json"; @@ -43,7 +42,7 @@ impl MigrationsState { /// A migration stored in the filesystem pub struct FsMigration { - fs: Rc, + fs: std::sync::Arc, root: PathBuf, } @@ -236,13 +235,13 @@ impl Eq for FsMigration {} /// A collection of migrations stored in the filesystem. pub struct FsMigrations { - fs: Rc, + fs: std::sync::Arc, root: PathBuf, current: FsMigration, } impl FsMigrations { pub fn new(root: PathBuf) -> Self { - let fs = Rc::new(OsFilesystem {}); + let fs = std::sync::Arc::new(OsFilesystem {}); let current = FsMigration { fs: fs.clone(), root: root.join("current"), @@ -297,6 +296,7 @@ impl Migrations for FsMigrations { } } +#[async_trait::async_trait] impl MigrationsMut for FsMigrations { fn current(&mut self) -> &mut Self::M { &mut self.current @@ -320,7 +320,7 @@ impl MigrationsMut for FsMigrations { Ok(()) } - fn clear_migrations(&mut self, conn: &impl ConnectionMethods) -> Result<()> { + async fn clear_migrations(&mut self, conn: &impl ConnectionMethods) -> Result<()> { for entry in std::fs::read_dir(&self.root)? { let entry = entry?; if matches!(entry.path().file_name(), Some(name) if name == "current") { @@ -332,7 +332,7 @@ impl MigrationsMut for FsMigrations { std::fs::remove_file(entry.path())?; } } - conn.delete_where(super::ButaneMigration::TABLE, crate::query::BoolExpr::True)?; + conn.delete_where(super::ButaneMigration::TABLE, crate::query::BoolExpr::True).await?; Ok(()) } } diff --git a/butane_core/src/migrations/memmigrations.rs b/butane_core/src/migrations/memmigrations.rs index 030d4c11..6f5fd2d1 100644 --- a/butane_core/src/migrations/memmigrations.rs +++ b/butane_core/src/migrations/memmigrations.rs @@ -128,6 +128,7 @@ impl Migrations for MemMigrations { } } +#[async_trait::async_trait] impl MigrationsMut for MemMigrations { fn current(&mut self) -> &mut Self::M { &mut self.current @@ -152,10 +153,10 @@ impl MigrationsMut for MemMigrations { Ok(()) } - fn clear_migrations(&mut self, conn: &impl ConnectionMethods) -> Result<()> { + async fn clear_migrations(&mut self, conn: &impl ConnectionMethods) -> Result<()> { self.migrations.clear(); self.latest = None; - conn.delete_where(ButaneMigration::TABLE, BoolExpr::True)?; + conn.delete_where(ButaneMigration::TABLE, BoolExpr::True).await?; Ok(()) } } diff --git a/butane_core/src/migrations/migration.rs b/butane_core/src/migrations/migration.rs index b73b5d39..d15dbbc5 100644 --- a/butane_core/src/migrations/migration.rs +++ b/butane_core/src/migrations/migration.rs @@ -13,6 +13,7 @@ use std::cmp::PartialEq; /// /// A Migration cannot be constructed directly, only retrieved from /// [Migrations][crate::migrations::Migrations]. +#[async_trait::async_trait] pub trait Migration: PartialEq { /// Retrieves the full abstract database state describing all tables fn db(&self) -> Result; @@ -37,46 +38,46 @@ pub trait Migration: PartialEq { /// Apply the migration to a database connection. The connection /// must be for the same type of database as this and the database /// must be in the state of the migration prior to this one - fn apply(&self, conn: &mut impl db::BackendConnection) -> Result<()> { + async fn apply(&self, conn: &mut impl db::BackendConnection) -> Result<()> { let backend_name = conn.backend_name(); - let tx = conn.transaction()?; + let tx = conn.transaction().await?; let sql = self .up_sql(backend_name)? .ok_or_else(|| Error::UnknownBackend(backend_name.to_string()))?; - tx.execute(&sql)?; - self.mark_applied(&tx)?; - tx.commit() + tx.execute(&sql).await?; + self.mark_applied(&tx).await?; + tx.commit().await } /// Mark the migration as being applied without doing any /// work. Use carefully -- the caller must ensure that the /// database schema already matches that expected by this /// migration. - fn mark_applied(&self, conn: &impl db::ConnectionMethods) -> Result<()> { + async fn mark_applied(&self, conn: &impl db::ConnectionMethods) -> Result<()> { conn.insert_only( ButaneMigration::TABLE, ButaneMigration::COLUMNS, &[self.name().as_ref().to_sql_ref()], - ) + ).await } /// Un-apply (downgrade) the migration to a database /// connection. The connection must be for the same type of /// database as this and this must be the latest migration applied /// to the database. - fn downgrade(&self, conn: &mut impl db::BackendConnection) -> Result<()> { + async fn downgrade(&self, conn: &mut impl db::BackendConnection) -> Result<()> { let backend_name = conn.backend_name(); - let tx = conn.transaction()?; + let tx = conn.transaction().await?; let sql = self .down_sql(backend_name)? .ok_or_else(|| Error::UnknownBackend(backend_name.to_string()))?; - tx.execute(&sql)?; + tx.execute(&sql).await?; let nameval = self.name().as_ref().to_sql(); tx.delete_where( ButaneMigration::TABLE, BoolExpr::Eq(ButaneMigration::PKCOL, Expr::Val(nameval)), - )?; - tx.commit() + ).await?; + tx.commit().await } } diff --git a/butane_core/src/migrations/mod.rs b/butane_core/src/migrations/mod.rs index 928c423b..21c53020 100644 --- a/butane_core/src/migrations/mod.rs +++ b/butane_core/src/migrations/mod.rs @@ -78,7 +78,7 @@ pub trait Migrations { /// Get the last migration that has been applied to the database or None /// if no migrations have been applied async fn last_applied_migration(&self, conn: &impl ConnectionMethods) -> Result> { - if !conn.has_table(ButaneMigration::TABLE)? { + if !conn.has_table(ButaneMigration::TABLE).await? { return Ok(None); } let migrations: Vec = conn @@ -108,6 +108,7 @@ pub trait Migrations { } } +#[async_trait] pub trait MigrationsMut: Migrations where Self::M: MigrationMut, @@ -126,7 +127,7 @@ where /// any storage backing it) and deleting the record of their /// existence/application from the database. The database schema /// is not modified, nor is any other data removed. Use carefully. - fn clear_migrations(&mut self, conn: &impl ConnectionMethods) -> Result<()>; + async fn clear_migrations(&mut self, conn: &impl ConnectionMethods) -> Result<()>; /// Get a pseudo-migration representing the current state as /// determined by the last build of models. This does not @@ -256,6 +257,7 @@ impl DataResult for ButaneMigration { } } +#[async_trait] impl DataObject for ButaneMigration { type PKType = String; type Fields = (); // we don't need Fields as we never filter @@ -265,7 +267,7 @@ impl DataObject for ButaneMigration { fn pk(&self) -> &String { &self.name } - fn save(&mut self, conn: &impl ConnectionMethods) -> Result<()> { + async fn save(&mut self, conn: &impl ConnectionMethods) -> Result<()> { let mut values: Vec> = Vec::with_capacity(2usize); values.push(self.name.to_sql_ref()); conn.insert_or_replace( @@ -273,9 +275,9 @@ impl DataObject for ButaneMigration { ::COLUMNS, &Column::new(Self::PKCOL, SqlType::Text), &values, - ) + ).await } - fn delete(&self, conn: &impl ConnectionMethods) -> Result<()> { - conn.delete(Self::TABLE, Self::PKCOL, self.pk().to_sql()) + async fn delete(&self, conn: &impl ConnectionMethods) -> Result<()> { + conn.delete(Self::TABLE, Self::PKCOL, self.pk().to_sql()).await } } diff --git a/butane_core/src/query/mod.rs b/butane_core/src/query/mod.rs index 1b64e4ac..f0157e4c 100644 --- a/butane_core/src/query/mod.rs +++ b/butane_core/src/query/mod.rs @@ -198,7 +198,7 @@ impl Query { } /// Executes the query against `conn` and deletes all matching objects. - pub fn delete(self, conn: &impl ConnectionMethods) -> Result { - conn.delete_where(&self.table, self.filter.unwrap_or(BoolExpr::True)) + pub async fn delete(self, conn: &impl ConnectionMethods) -> Result { + conn.delete_where(&self.table, self.filter.unwrap_or(BoolExpr::True)).await } } diff --git a/butane_core/src/sqlval.rs b/butane_core/src/sqlval.rs index 2efd50e2..990dcbdc 100644 --- a/butane_core/src/sqlval.rs +++ b/butane_core/src/sqlval.rs @@ -269,7 +269,7 @@ pub trait FieldType: ToSql + FromSql { } /// Marker trait for a type suitable for being a primary key -pub trait PrimaryKeyType: FieldType + Clone + PartialEq {} +pub trait PrimaryKeyType: FieldType + Clone + PartialEq + Sync{} /// Trait for referencing the primary key for a given model. Used to /// implement ForeignKey equality tests. From deed29bad179016400f89a706ec0ba8c5224dce8 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Sun, 7 May 2023 17:50:18 -0400 Subject: [PATCH 14/78] Update versions --- Cargo.lock | 659 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 411 insertions(+), 248 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a8bd4593..dc14e6a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,9 +30,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.7.20" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" dependencies = [ "memchr", ] @@ -57,19 +57,28 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.68" +version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] [[package]] name = "async-trait" -version = "0.1.64" +version = "0.1.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd7fce9ba8c3c042128ce72d8b2ddbf3a05747efb67ea0313c635e10bda47a2" +checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ - "proc-macro2 1.0.50", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 2.0.15", ] [[package]] @@ -78,7 +87,7 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.19", "libc", "winapi", ] @@ -104,12 +113,6 @@ dependencies = [ "rustc-demangle", ] -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - [[package]] name = "base64" version = "0.21.0" @@ -124,18 +127,18 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "block-buffer" -version = "0.10.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "bumpalo" -version = "3.12.0" +version = "3.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +checksum = "9b1ce199063694f33ffb7dd4e0ee620741495c32833cde5aa08f02a0bf96f0c8" [[package]] name = "butane" @@ -152,12 +155,12 @@ dependencies = [ "log", "once_cell", "paste", - "proc-macro2 1.0.50", - "quote 1.0.23", + "proc-macro2 1.0.56", + "quote 1.0.26", "r2d2", "rusqlite", "serde_json", - "tokio-postgres 0.7.7 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-postgres 0.7.8 (registry+https://github.com/rust-lang/crates.io-index)", "uuid", ] @@ -169,7 +172,7 @@ dependencies = [ "butane", "chrono", "clap", - "quote 1.0.23", + "quote 1.0.26", "serde", "serde_json", ] @@ -179,9 +182,9 @@ name = "butane_codegen" version = "0.5.0" dependencies = [ "butane_core", - "proc-macro2 1.0.50", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", "uuid", ] @@ -202,17 +205,17 @@ dependencies = [ "native-tls", "pin-project", "postgres-native-tls", - "proc-macro2 1.0.50", - "quote 1.0.23", + "proc-macro2 1.0.56", + "quote 1.0.26", "r2d2", "regex", "rusqlite", "serde", "serde_json", - "syn 1.0.107", + "syn 1.0.109", "thiserror", "tokio", - "tokio-postgres 0.7.7 (git+https://github.com/sfackler/rust-postgres)", + "tokio-postgres 0.7.8 (git+https://github.com/sfackler/rust-postgres)", "uuid", ] @@ -242,9 +245,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.23" +version = "0.4.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" dependencies = [ "iana-time-zone", "js-sys", @@ -293,15 +296,15 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "cpufeatures" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" dependencies = [ "libc", ] @@ -318,9 +321,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.89" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc831ee6a32dd495436e317595e639a587aa9907bef96fe6e6abc290ab6204e9" +checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93" dependencies = [ "cc", "cxxbridge-flags", @@ -330,34 +333,34 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.89" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94331d54f1b1a8895cd81049f7eaaaef9d05a7dcb4d1fd08bf3ff0806246789d" +checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b" dependencies = [ "cc", "codespan-reporting", "once_cell", - "proc-macro2 1.0.50", - "quote 1.0.23", + "proc-macro2 1.0.56", + "quote 1.0.26", "scratch", - "syn 1.0.107", + "syn 2.0.15", ] [[package]] name = "cxxbridge-flags" -version = "1.0.89" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48dcd35ba14ca9b40d6e4b4b39961f23d835dbb8eed74565ded361d93e1feb8a" +checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb" [[package]] name = "cxxbridge-macro" -version = "1.0.89" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bbeb29798b407ccd82a3324ade1a7286e0d29851475990b612670f6f5124d2" +checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" dependencies = [ - "proc-macro2 1.0.50", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 2.0.15", ] [[package]] @@ -419,6 +422,27 @@ dependencies = [ "termcolor", ] +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "example" version = "0.1.0" @@ -455,9 +479,9 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" dependencies = [ - "proc-macro2 1.0.50", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", "synstructure", ] @@ -475,9 +499,9 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fastrand" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" dependencies = [ "instant", ] @@ -515,9 +539,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" dependencies = [ "futures-channel", "futures-core", @@ -530,9 +554,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" dependencies = [ "futures-core", "futures-sink", @@ -540,15 +564,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" [[package]] name = "futures-executor" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" dependencies = [ "futures-core", "futures-task", @@ -557,38 +581,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" [[package]] name = "futures-macro" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ - "proc-macro2 1.0.50", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 2.0.15", ] [[package]] name = "futures-sink" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" [[package]] name = "futures-task" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" [[package]] name = "futures-util" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ "futures-channel", "futures-core", @@ -604,9 +628,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.6" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -614,18 +638,20 @@ dependencies = [ [[package]] name = "geo-types" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e26879b63ac36ca5492918dc16f8c1e604b0f70f884fffbd3533f89953ab1991" +checksum = "a5f0b3068e1537a4b861ec3734f4aa9c317d537cf0845bf6fb6221973499d26c" dependencies = [ + "approx", "num-traits", + "serde", ] [[package]] name = "getrandom" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" dependencies = [ "cfg-if", "libc", @@ -641,9 +667,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "221996f774192f0f718773def8201c4ae31f02616a54ccfc2d358bb0e5cefdec" +checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" [[package]] name = "hashbrown" @@ -672,6 +698,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + [[package]] name = "hex" version = "0.4.3" @@ -695,16 +727,16 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "iana-time-zone" -version = "0.1.53" +version = "0.1.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "winapi", + "windows", ] [[package]] @@ -732,11 +764,22 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "io-lifetimes" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" +dependencies = [ + "hermit-abi 0.3.1", + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "itoa" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "js-sys" @@ -755,9 +798,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.139" +version = "0.2.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc207893e85c5d6be840e969b496b53d94cec8be2d501b214f50daa97fa8024" + +[[package]] +name = "libm" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" [[package]] name = "libsqlite3-sys" @@ -779,6 +828,12 @@ dependencies = [ "cc", ] +[[package]] +name = "linux-raw-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ece97ea872ece730aed82664c424eb4c8291e1ff2480247ccf7409044bc6479f" + [[package]] name = "lock_api" version = "0.4.9" @@ -824,14 +879,14 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" +checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" dependencies = [ "libc", "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.42.0", + "windows-sys 0.45.0", ] [[package]] @@ -869,6 +924,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -882,15 +938,15 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.17.0" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" [[package]] name = "openssl" -version = "0.10.45" +version = "0.10.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b102428fd03bc5edf97f62620f7298614c45cedf287c271e7ed450bbaf83f2e1" +checksum = "01b8574602df80f7b85fdfc5392fa884a4e3b3f4f35402c070ab34c3d3f78d56" dependencies = [ "bitflags", "cfg-if", @@ -903,13 +959,13 @@ dependencies = [ [[package]] name = "openssl-macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ - "proc-macro2 1.0.50", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 2.0.15", ] [[package]] @@ -920,11 +976,10 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.80" +version = "0.9.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23bbbf7854cd45b83958ebe919f0e8e516793727652e27fda10a8384cfc790b7" +checksum = "8e17f59264b2809d77ae94f0e1ebabc434773f370d6ca667bd223ea10e06cc7e" dependencies = [ - "autocfg", "cc", "libc", "pkg-config", @@ -949,7 +1004,7 @@ checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "smallvec", "windows-sys 0.45.0", ] @@ -1012,9 +1067,9 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" dependencies = [ - "proc-macro2 1.0.50", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", ] [[package]] @@ -1031,9 +1086,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "postgres-native-tls" @@ -1045,16 +1100,16 @@ dependencies = [ "native-tls", "tokio", "tokio-native-tls", - "tokio-postgres 0.7.7 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-postgres 0.7.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "postgres-protocol" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "878c6cbf956e03af9aa8204b407b9cbf47c072164800aa918c516cd4b056c50c" +checksum = "78b7fa9f396f51dffd61546fd8573ee20592287996568e6175ceb0f8699ad75d" dependencies = [ - "base64 0.13.1", + "base64", "byteorder", "bytes", "fallible-iterator", @@ -1068,10 +1123,10 @@ dependencies = [ [[package]] name = "postgres-protocol" -version = "0.6.4" -source = "git+https://github.com/sfackler/rust-postgres#f4d8d603f1b9d3edd9b93889465f3db5bba0fd40" +version = "0.6.5" +source = "git+https://github.com/sfackler/rust-postgres#22469d6b3852da3b6c66f571af1a2982f19930c4" dependencies = [ - "base64 0.21.0", + "base64", "byteorder", "bytes", "fallible-iterator", @@ -1085,25 +1140,25 @@ dependencies = [ [[package]] name = "postgres-types" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73d946ec7d256b04dfadc4e6a3292324e6f417124750fc5c0950f981b703a0f1" +checksum = "f028f05971fe20f512bcc679e2c10227e57809a3af86a7606304435bc8896cd6" dependencies = [ "bytes", "fallible-iterator", "geo-types", - "postgres-protocol 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)", + "postgres-protocol 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "postgres-types" -version = "0.2.4" -source = "git+https://github.com/sfackler/rust-postgres#f4d8d603f1b9d3edd9b93889465f3db5bba0fd40" +version = "0.2.5" +source = "git+https://github.com/sfackler/rust-postgres#22469d6b3852da3b6c66f571af1a2982f19930c4" dependencies = [ "bytes", "chrono", "fallible-iterator", - "postgres-protocol 0.6.4 (git+https://github.com/sfackler/rust-postgres)", + "postgres-protocol 0.6.5 (git+https://github.com/sfackler/rust-postgres)", ] [[package]] @@ -1129,9 +1184,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.50" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2" +checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" dependencies = [ "unicode-ident", ] @@ -1147,11 +1202,11 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.23" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" dependencies = [ - "proc-macro2 1.0.50", + "proc-macro2 1.0.56", ] [[package]] @@ -1204,11 +1259,20 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" -version = "1.7.1" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" dependencies = [ "aho-corasick", "memchr", @@ -1217,18 +1281,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.28" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" - -[[package]] -name = "remove_dir_all" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -dependencies = [ - "winapi", -] +checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" [[package]] name = "rusqlite" @@ -1246,15 +1301,29 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.21" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustix" +version = "0.37.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" +checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", +] [[package]] name = "ryu" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" [[package]] name = "schannel" @@ -1267,9 +1336,9 @@ dependencies = [ [[package]] name = "scheduled-thread-pool" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "977a7519bff143a44f842fd07e80ad1329295bd71686457f18e496736f4bf9bf" +checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" dependencies = [ "parking_lot", ] @@ -1282,9 +1351,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "scratch" -version = "1.0.3" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" +checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" [[package]] name = "security-framework" @@ -1311,29 +1380,29 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.152" +version = "1.0.162" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +checksum = "71b2f6e1ab5c2b98c05f0f35b236b22e8df7ead6ffbf51d7808da7f8817e7ab6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.152" +version = "1.0.162" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +checksum = "a2a0814352fd64b58489904a44ea8d90cb1a91dcb6b4f5ebabc32c8318e93cb6" dependencies = [ - "proc-macro2 1.0.50", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 2.0.15", ] [[package]] name = "serde_json" -version = "1.0.91" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" +checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" dependencies = [ "itoa", "ryu", @@ -1359,9 +1428,9 @@ checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" [[package]] name = "slab" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" dependencies = [ "autocfg", ] @@ -1374,14 +1443,24 @@ checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] name = "socket2" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" dependencies = [ "libc", "winapi", ] +[[package]] +name = "socket2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d283f86695ae989d1e18440a943880967156325ba025f05049946bff47bcc2b" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "stringprep" version = "0.1.2" @@ -1423,12 +1502,23 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.107" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2 1.0.56", + "quote 1.0.26", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" dependencies = [ - "proc-macro2 1.0.50", - "quote 1.0.23", + "proc-macro2 1.0.56", + "quote 1.0.26", "unicode-ident", ] @@ -1438,24 +1528,23 @@ version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" dependencies = [ - "proc-macro2 1.0.50", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", "unicode-xid 0.2.4", ] [[package]] name = "tempfile" -version = "3.3.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" dependencies = [ "cfg-if", "fastrand", - "libc", - "redox_syscall", - "remove_dir_all", - "winapi", + "redox_syscall 0.3.5", + "rustix", + "windows-sys 0.45.0", ] [[package]] @@ -1478,22 +1567,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.38" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.38" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ - "proc-macro2 1.0.50", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 2.0.15", ] [[package]] @@ -1524,25 +1613,24 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.25.0" +version = "1.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af" +checksum = "c3c786bf8134e5a3a166db9b29ab8f48134739014a3eca7bc6bfa95d673b136f" dependencies = [ "autocfg", "bytes", "libc", - "memchr", "mio", "pin-project-lite", - "socket2", - "windows-sys 0.42.0", + "socket2 0.4.9", + "windows-sys 0.48.0", ] [[package]] name = "tokio-native-tls" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ "native-tls", "tokio", @@ -1550,9 +1638,9 @@ dependencies = [ [[package]] name = "tokio-postgres" -version = "0.7.7" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29a12c1b3e0704ae7dfc25562629798b29c72e6b1d0a681b6f29ab4ae5e7f7bf" +checksum = "6e89f6234aa8fd43779746012fcf53603cdb91fdd8399aa0de868c2d56b6dde1" dependencies = [ "async-trait", "byteorder", @@ -1565,17 +1653,17 @@ dependencies = [ "percent-encoding", "phf", "pin-project-lite", - "postgres-protocol 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)", - "postgres-types 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", - "socket2", + "postgres-protocol 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", + "postgres-types 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", + "socket2 0.5.2", "tokio", "tokio-util", ] [[package]] name = "tokio-postgres" -version = "0.7.7" -source = "git+https://github.com/sfackler/rust-postgres#f4d8d603f1b9d3edd9b93889465f3db5bba0fd40" +version = "0.7.8" +source = "git+https://github.com/sfackler/rust-postgres#22469d6b3852da3b6c66f571af1a2982f19930c4" dependencies = [ "async-trait", "byteorder", @@ -1588,18 +1676,18 @@ dependencies = [ "percent-encoding", "phf", "pin-project-lite", - "postgres-protocol 0.6.4 (git+https://github.com/sfackler/rust-postgres)", - "postgres-types 0.2.4 (git+https://github.com/sfackler/rust-postgres)", - "socket2", + "postgres-protocol 0.6.5 (git+https://github.com/sfackler/rust-postgres)", + "postgres-types 0.2.5 (git+https://github.com/sfackler/rust-postgres)", + "socket2 0.5.2", "tokio", "tokio-util", ] [[package]] name = "tokio-util" -version = "0.7.4" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" dependencies = [ "bytes", "futures-core", @@ -1637,15 +1725,15 @@ checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" [[package]] name = "unicode-bidi" -version = "0.3.10" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54675592c1dbefd78cbd98db9bacd89886e1ca50692a0692baefffdeb92dd58" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.6" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" [[package]] name = "unicode-normalization" @@ -1676,9 +1764,9 @@ checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "uuid" -version = "1.3.0" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" +checksum = "4dad5567ad0cf5b760e5665964bec1b47dfd077ba8a2544b513f3556d3d239a2" dependencies = [ "getrandom", ] @@ -1732,9 +1820,9 @@ dependencies = [ "bumpalo", "log", "once_cell", - "proc-macro2 1.0.50", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", "wasm-bindgen-shared", ] @@ -1744,7 +1832,7 @@ version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" dependencies = [ - "quote 1.0.23", + "quote 1.0.26", "wasm-bindgen-macro-support", ] @@ -1754,9 +1842,9 @@ version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" dependencies = [ - "proc-macro2 1.0.50", - "quote 1.0.23", - "syn 1.0.107", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1798,19 +1886,28 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.0", +] + [[package]] name = "windows-sys" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] @@ -1819,62 +1916,128 @@ version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "windows-targets", + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] name = "windows-targets" -version = "0.42.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.1" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" -version = "0.42.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" [[package]] name = "windows_i686_gnu" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" -version = "0.42.1" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" -version = "0.42.1" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.1" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" -version = "0.42.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" From 29c8da915094b7e131ef5628176d77e7e85eeec9 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Tue, 9 May 2023 21:57:44 -0400 Subject: [PATCH 15/78] Work on lifetimes --- butane_core/src/db/pg.rs | 57 +++++++++++++++++++--------------------- butane_core/src/lib.rs | 8 +++--- 2 files changed, 32 insertions(+), 33 deletions(-) diff --git a/butane_core/src/db/pg.rs b/butane_core/src/db/pg.rs index 02b688fb..aa3cc45e 100644 --- a/butane_core/src/db/pg.rs +++ b/butane_core/src/db/pg.rs @@ -141,7 +141,9 @@ where if cfg!(feature = "log") { debug!("execute sql {}", sql); } - self.client()?.batch_execute(sql.as_ref()).await?; + // Note, let binding exists only so that the self.client() reference is not held across the await + let future = self.client()?.batch_execute(sql.as_ref()); + future.await?; Ok(()) } @@ -185,16 +187,13 @@ where eprintln!("query sql {}", sqlquery); let types: Vec = values.iter().map(pgtype_for_val).collect(); - let stmt = self - .client()? - .prepare_typed(&sqlquery, types.as_ref()) - .await?; + let future = self.client()?.prepare_typed(&sqlquery, types.as_ref()); + let stmt = future.await?; let mut rowvec = Vec::::new(); - let rowstream = self + let future = self .client()? - .query_raw(&stmt, values.iter().map(sqlval_for_pg_query)) - .await - .map_err(Error::Postgres)?; + .query_raw(&stmt, values.iter().map(sqlval_for_pg_query)); + let rowstream = future.await.map_err(Error::Postgres)?; let mut rowstream = Box::pin(rowstream); while let Some(r) = rowstream.next().await { let r = r?; @@ -223,9 +222,10 @@ where } // use query instead of execute so we can get our result back - let pk_stream = self + let future = self .client()? - .query_raw(sql.as_str(), values.iter().map(sqlvalref_for_pg_query)) + .query_raw(sql.as_str(), values.iter().map(sqlvalref_for_pg_query)); + let pk_stream = future .await .map_err(Error::Postgres)? .map(|r| r.map(|x| sql_val_from_postgres(&x, 0, pkcol))); @@ -248,9 +248,8 @@ where &mut sql, ); let params: Vec<&DynToSqlPg> = values.iter().map(|v| v as &DynToSqlPg).collect(); - self.client()? - .execute(sql.as_str(), params.as_slice()) - .await?; + let future = self.client()?.execute(sql.as_str(), params.as_slice()); + future.await?; Ok(()) } async fn insert_or_replace( @@ -263,9 +262,8 @@ where let mut sql = String::new(); sql_insert_or_replace_with_placeholders(table, columns, pkcol, &mut sql); let params: Vec<&DynToSqlPg> = values.iter().map(|v| v as &DynToSqlPg).collect(); - self.client()? - .execute(sql.as_str(), params.as_slice()) - .await?; + let future = self.client()?.execute(sql.as_str(), params.as_slice()); + future.await?; Ok(()) } async fn update<'a>( @@ -292,9 +290,8 @@ where if cfg!(feature = "log") { debug!("update sql {}", sql); } - self.client()? - .execute(sql.as_str(), params.as_slice()) - .await?; + let future = self.client()?.execute(sql.as_str(), params.as_slice()); + future.await?; Ok(()) } async fn delete_where(&self, table: &str, expr: BoolExpr) -> Result { @@ -308,19 +305,19 @@ where &mut sql, ); let params: Vec<&DynToSqlPg> = values.iter().map(|v| v as &DynToSqlPg).collect(); - let cnt = self - .client()? - .execute(sql.as_str(), params.as_slice()) - .await?; + let future = self.client()?.execute(sql.as_str(), params.as_slice()); + let cnt = future.await?; Ok(cnt as usize) } async fn has_table(&self, table: &str) -> Result { // future improvement, should be schema-aware - let stmt = self + let future = self .client()? - .prepare("SELECT table_name FROM information_schema.tables WHERE table_name=$1;") - .await?; - let rows = self.client()?.query(&stmt, &[&table]).await?; + .prepare("SELECT table_name FROM information_schema.tables WHERE table_name=$1;"); + let stmt = future.await?; + let tableref: &[&(dyn postgres::types::ToSql + Sync)] = &[&table]; + let future = self.client()?.query(&stmt, tableref); + let rows = future.await?; Ok(!rows.is_empty()) } } @@ -486,7 +483,7 @@ impl<'a> postgres::types::FromSql<'a> for SqlValRef<'a> { } } -fn check_columns(row: &postgres::RowStream, cols: &[Column]) -> Result<()> { +fn check_columns(row: &postgres::Row, cols: &[Column]) -> Result<()> { if cols.len() != row.len() { Err(Error::Internal(format!( "postgres returns columns {} doesn't match requested columns {}", @@ -518,7 +515,7 @@ fn sql_for_expr( helper::sql_for_expr(expr, sql_for_expr, values, pls, w) } -fn sql_val_from_postgres(row: &postgres::RowStream, idx: I, col: &Column) -> Result +fn sql_val_from_postgres(row: &postgres::Row, idx: I, col: &Column) -> Result where I: postgres::row::RowIndex + std::fmt::Display, { diff --git a/butane_core/src/lib.rs b/butane_core/src/lib.rs index 1870f57e..3b7369ab 100644 --- a/butane_core/src/lib.rs +++ b/butane_core/src/lib.rs @@ -85,12 +85,15 @@ pub trait DataObject: DataResult + Sync { /// Get the primary key fn pk(&self) -> &Self::PKType; /// Find this object in the database based on primary key. - async fn get(conn: &impl ConnectionMethods, id: impl Borrow + Send + Sync) -> Result + async fn get( + conn: &impl ConnectionMethods, + id: impl Borrow + Send + Sync, + ) -> Result where Self: Sized, Self::PKType: Sync, { - let query = ::query().await; + let query = ::query(); query .filter(query::BoolExpr::Eq( Self::PKCOL, @@ -102,7 +105,6 @@ pub trait DataObject: DataResult + Sync { .into_iter() .nth(0) .ok_or(Error::NoSuchObject) - .await } /// Save the object to the database. From 5a0fbab4f08166f00ce5f439ac9136d8980e2d97 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Fri, 19 May 2023 21:03:12 -0400 Subject: [PATCH 16/78] Work on macros --- Cargo.lock | 1 + butane/Cargo.toml | 1 + butane/src/lib.rs | 8 ++ butane_core/Cargo.toml | 2 +- butane_core/src/codegen/dbobj.rs | 8 +- butane_core/src/db/connmethods.rs | 122 +++++++++++++++++------------- butane_core/src/db/macros.rs | 3 + butane_core/src/db/sqlite.rs | 2 +- 8 files changed, 88 insertions(+), 59 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dc14e6a9..0bb0dedf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -144,6 +144,7 @@ checksum = "9b1ce199063694f33ffb7dd4e0ee620741495c32833cde5aa08f02a0bf96f0c8" name = "butane" version = "0.5.0" dependencies = [ + "async-trait", "butane_codegen", "butane_core", "cfg-if", diff --git a/butane/Cargo.toml b/butane/Cargo.toml index f3783cbc..3a98b6ef 100644 --- a/butane/Cargo.toml +++ b/butane/Cargo.toml @@ -24,6 +24,7 @@ tls = ["butane_core/tls"] uuid = ["butane_core/uuid", "butane_codegen/uuid"] [dependencies] +async-trait = "0.1" butane_codegen = { path = "../butane_codegen", version = "0.5" } butane_core = { path = "../butane_core", version = "0.5" } diff --git a/butane/src/lib.rs b/butane/src/lib.rs index 4972f09e..b9b0d002 100644 --- a/butane/src/lib.rs +++ b/butane/src/lib.rs @@ -169,3 +169,11 @@ pub mod prelude { pub use crate::DataResult; pub use butane_core::db::BackendConnection; } + +pub mod internal { + //! Internals which will we be used in macro-generated code. + //! + //! Do not use directly. + + pub use async_trait::async_trait; +} diff --git a/butane_core/Cargo.toml b/butane_core/Cargo.toml index c6cdcdd1..eb2ab4fb 100644 --- a/butane_core/Cargo.toml +++ b/butane_core/Cargo.toml @@ -19,6 +19,7 @@ pg = ["tokio-postgres", "bytes"] [dependencies] +async-trait = "0.1" bytes = { version="1.0", optional=true} cfg-if = "1.0" fallible-iterator = "0.2" @@ -44,6 +45,5 @@ syn = { version = "1.0", features = ["full", "extra-traits"] } thiserror = "1.0" chrono = { version = "0.4", features=["serde"], optional = true } uuid = {workspace=true, optional=true} -async-trait = "0.1" tokio = { version = "1.24", features=["rt", "sync"] } diff --git a/butane_core/src/codegen/dbobj.rs b/butane_core/src/codegen/dbobj.rs index 46eff1cb..eb1365e6 100644 --- a/butane_core/src/codegen/dbobj.rs +++ b/butane_core/src/codegen/dbobj.rs @@ -54,7 +54,9 @@ pub fn impl_dbobject(ast_struct: &ItemStruct, config: &Config) -> TokenStream2 { let dataresult = impl_dataresult(ast_struct, tyname); quote!( - #dataresult + #dataresult + + #[butane::internal::async_trait] impl butane::DataObject for #tyname { type PKType = #pktype; type Fields = #fields_type; @@ -64,7 +66,7 @@ pub fn impl_dbobject(ast_struct: &ItemStruct, config: &Config) -> TokenStream2 { fn pk(&self) -> &Self::PKType { &self.#pkident } - fn save(&mut self, conn: &impl butane::db::ConnectionMethods) -> butane::Result<()> { + async fn save(&mut self, conn: &impl butane::db::ConnectionMethods) -> butane::Result<()> { //future perf improvement use an array on the stack let mut values: Vec = Vec::with_capacity(#numdbfields); let pkcol = butane::db::Column::new( @@ -86,7 +88,7 @@ pub fn impl_dbobject(ast_struct: &ItemStruct, config: &Config) -> TokenStream2 { #many_save Ok(()) } - fn delete(&self, conn: &impl butane::db::ConnectionMethods) -> butane::Result<()> { + async fn delete(&self, conn: &impl butane::db::ConnectionMethods) -> butane::Result<()> { use butane::ToSql; use butane::prelude::DataObject; conn.delete(Self::TABLE, Self::PKCOL, self.pk().to_sql()) diff --git a/butane_core/src/db/connmethods.rs b/butane_core/src/db/connmethods.rs index c7ab8213..37af34b5 100644 --- a/butane_core/src/db/connmethods.rs +++ b/butane_core/src/db/connmethods.rs @@ -7,60 +7,74 @@ use async_trait::async_trait; use std::ops::{Deref, DerefMut}; use std::vec::Vec; -/// Methods available on a database connection. Most users do not need -/// to call these methods directly and will instead use methods on -/// [DataObject][crate::DataObject] or the `query!` macro. This trait is -/// implemented by both database connections and transactions. -#[async_trait] -pub trait ConnectionMethods: Sync { - async fn execute(&self, sql: &str) -> Result<()>; - async fn query<'a, 'b, 'c: 'a>( - &'c self, - table: &str, - columns: &'b [Column], - expr: Option, - limit: Option, - offset: Option, - sort: Option<&[Order]>, - ) -> Result>; - async fn insert_returning_pk( - &self, - table: &str, - columns: &[Column], - pkcol: &Column, - values: &[SqlValRef<'_>], - ) -> Result; - /// Like `insert_returning_pk` but with no return value - async fn insert_only( - &self, - table: &str, - columns: &[Column], - values: &[SqlValRef<'_>], - ) -> Result<()>; - /// Insert unless there's a conflict on the primary key column, in which case update - async fn insert_or_replace( - &self, - table: &str, - columns: &[Column], - pkcol: &Column, - values: &[SqlValRef<'_>], - ) -> Result<()>; - async fn update<'a>( - &self, - table: &str, - pkcol: Column, - pk: SqlValRef<'a>, - columns: &[Column], - values: &[SqlValRef<'_>], - ) -> Result<()>; - async fn delete(&self, table: &str, pkcol: &'static str, pk: SqlVal) -> Result<()> { - self.delete_where(table, BoolExpr::Eq(pkcol, Expr::Val(pk))) - .await?; - Ok(()) - } - async fn delete_where(&self, table: &str, expr: BoolExpr) -> Result; - /// Tests if a table exists in the database. - async fn has_table(&self, table: &str) -> Result; +macro_rules! connection_methods_trait { + (async) => { + connection_methods_trait!(async #[async_trait]); + }; + (sync) => { + connection_methods_trait!(); + }; + ($($async:tt)? $(#[$attr:tt])?) => { + /// Methods available on a database connection. Most users do not need + /// to call these methods directly and will instead use methods on + /// [DataObject][crate::DataObject] or the `query!` macro. This trait is + /// implemented by both database connections and transactions. + $(#[$attr])? + pub trait ConnectionMethods: Sync { + $($async)? fn execute(&self, sql: &str) -> Result<()>; + $($async)? fn query<'a, 'b, 'c: 'a>( + &'c self, + table: &str, + columns: &'b [Column], + expr: Option, + limit: Option, + offset: Option, + sort: Option<&[Order]>, + ) -> Result>; + $($async)? fn insert_returning_pk( + &self, + table: &str, + columns: &[Column], + pkcol: &Column, + values: &[SqlValRef<'_>], + ) -> Result; + /// Like `insert_returning_pk` but with no return value + $($async)? fn insert_only( + &self, + table: &str, + columns: &[Column], + values: &[SqlValRef<'_>], + ) -> Result<()>; + /// Insert unless there's a conflict on the primary key column, in which case update + $($async)? fn insert_or_replace( + &self, + table: &str, + columns: &[Column], + pkcol: &Column, + values: &[SqlValRef<'_>], + ) -> Result<()>; + $($async)? fn update<'a>( + &self, + table: &str, + pkcol: Column, + pk: SqlValRef<'a>, + columns: &[Column], + values: &[SqlValRef<'_>], + ) -> Result<()>; + $($async)? fn delete(&self, table: &str, pkcol: &'static str, pk: SqlVal) -> Result<()>; + // self.delete_where(table, BoolExpr::Eq(pkcol, Expr::Val(pk))).await?; + $($async)? fn delete_where(&self, table: &str, expr: BoolExpr) -> Result; + /// Tests if a table exists in the database. + $($async)? fn has_table(&self, table: &str) -> Result; + } + }; +} + +connection_methods_trait!(async); + +mod sync { + use super::*; + connection_methods_trait!(sync); } /// Represents a database column. Most users do not need to use this diff --git a/butane_core/src/db/macros.rs b/butane_core/src/db/macros.rs index 414becaf..c1ededf8 100644 --- a/butane_core/src/db/macros.rs +++ b/butane_core/src/db/macros.rs @@ -63,6 +63,9 @@ macro_rules! connection_method_wrapper { .update(table, pkcol, pk, columns, values) .await } + async fn delete(&self, table: &str, expr: BoolExpr) -> Result { + self.wrapped_connection_methods()?.delete(table, expr).await + } async fn delete_where(&self, table: &str, expr: BoolExpr) -> Result { self.wrapped_connection_methods()? .delete_where(table, expr) diff --git a/butane_core/src/db/sqlite.rs b/butane_core/src/db/sqlite.rs index 3de62ef9..562ae095 100644 --- a/butane_core/src/db/sqlite.rs +++ b/butane_core/src/db/sqlite.rs @@ -262,7 +262,7 @@ fn rs_conn_has_table(conn: &rusqlite::Connection, table: &str) -> Result { } #[async_trait] -impl ConnectionMethods for Mutex { +impl Connectionmethods for Mutex { fn execute(&self, sql: &str) -> Result<()> { rs_conn_execute(self.lock()?.deref(), sql) } From 66c77859bb08d13ff93dfdeb6067222a2f927f5b Mon Sep 17 00:00:00 2001 From: James Oakley Date: Sat, 20 May 2023 10:35:05 -0400 Subject: [PATCH 17/78] Fix some merge issues --- Cargo.lock | 624 ++++++++++++++++------------------ Cargo.toml | 4 +- butane_core/Cargo.toml | 7 +- butane_test_helper/Cargo.toml | 2 +- 4 files changed, 296 insertions(+), 341 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8be4ad49..ac3d4441 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,20 +4,20 @@ version = 3 [[package]] name = "ahash" -version = "0.7.6" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" dependencies = [ - "getrandom", + "cfg-if", "once_cell", "version_check", ] [[package]] name = "aho-corasick" -version = "0.7.20" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" dependencies = [ "memchr", ] @@ -31,18 +31,77 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" + +[[package]] +name = "anstyle-parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys 0.48.0", +] + [[package]] name = "anyhow" -version = "1.0.68" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" + +[[package]] +name = "approx" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] [[package]] name = "assert_cmd" -version = "2.0.8" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9834fcc22e0874394a010230586367d4a3e9f11b560f469262678547e1d2575e" +checksum = "86d6b683edf8d1119fe420a94f8a7e389239666aa72e65495d91c00462510151" dependencies = [ + "anstyle", "bstr", "doc-comment", "predicates", @@ -53,23 +112,20 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.60" +version = "0.1.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d1d8ab452a3936018a687b20e6f7cf5363d713b732b8884001317b0e48aa3" +checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", - "syn 1.0.107", + "proc-macro2 1.0.58", + "quote 1.0.27", + "syn 2.0.16", ] [[package]] name = "atomic" -version = "0.5.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b88d82667eca772c4aa12f0f1348b3ae643424c8876448f3f7bd5787032e234c" -dependencies = [ - "autocfg", -] +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" [[package]] name = "autocfg" @@ -91,9 +147,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.2.1" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a6904aef64d73cf10ab17ebace7befb918b82164785cb89907993be7f83813" +checksum = "6776fc96284a0bb647b615056fc496d1fe1644a7ab01829818a6d91cae888b84" [[package]] name = "block-buffer" @@ -106,9 +162,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ffdb39cb703212f3c11973452c2861b972f757b021158f3516ba10f2fa8b2c1" +checksum = "c3d4260bcc2e8fc9df1eac4919a720effeb63a3f0952f5bf4944adfa18897f09" dependencies = [ "memchr", "once_cell", @@ -118,9 +174,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.12.1" +version = "3.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b1ce199063694f33ffb7dd4e0ee620741495c32833cde5aa08f02a0bf96f0c8" +checksum = "3c6ed94e98ecff0c12dd1b04c15ec0d7d9458ca8fe806cea6f12954efe74c63b" [[package]] name = "butane" @@ -139,15 +195,14 @@ dependencies = [ "log", "once_cell", "paste", - "postgres", - "proc-macro2 1.0.56", - "quote 1.0.26", + "proc-macro2 1.0.58", + "quote 1.0.27", "r2d2", "rand 0.8.5", "rusqlite", "serde", "serde_json", - "tokio-postgres 0.7.8 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-postgres", "uuid", ] @@ -159,7 +214,7 @@ dependencies = [ "butane", "chrono", "clap", - "quote 1.0.26", + "quote 1.0.27", "serde", "serde_json", ] @@ -169,15 +224,16 @@ name = "butane_codegen" version = "0.6.0" dependencies = [ "butane_core", - "proc-macro2 1.0.56", - "quote 1.0.26", - "syn 2.0.15", + "proc-macro2 1.0.58", + "quote 1.0.27", + "syn 2.0.16", ] [[package]] name = "butane_core" version = "0.6.0" dependencies = [ + "async-trait", "butane_test_helper", "bytes", "cfg-if", @@ -195,17 +251,19 @@ dependencies = [ "paste", "pin-project", "postgres-native-tls", - "proc-macro2 1.0.56", - "quote 1.0.26", + "proc-macro2 1.0.58", + "quote 1.0.27", "r2d2", "rand 0.8.5", "regex", "rusqlite", "serde", "serde_json", - "syn 2.0.15", + "syn 2.0.16", "tempdir", "thiserror", + "tokio", + "tokio-postgres", "uuid", ] @@ -216,7 +274,7 @@ dependencies = [ "butane_core", "libc", "once_cell", - "postgres", + "tokio-postgres", "uuid", ] @@ -228,15 +286,15 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" [[package]] name = "cc" -version = "1.0.78" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" [[package]] name = "cfg-if" @@ -246,9 +304,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.23" +version = "0.4.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" dependencies = [ "iana-time-zone", "num-integer", @@ -259,35 +317,37 @@ dependencies = [ [[package]] name = "clap" -version = "4.1.6" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0b0588d44d4d63a87dbd75c136c166bbfd9a86a31cb89e09906521c7d3f5e3" +checksum = "93aae7a4192245f70fe75dd9157fc7b4a5bf53e88d30bd4396f7d8f9284d5acc" dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f423e341edefb78c9caba2d9c7f7687d0e72e89df3ce3394554754393ac3990" +dependencies = [ + "anstream", + "anstyle", "bitflags 1.3.2", "clap_lex", - "is-terminal", "strsim 0.10.0", - "termcolor", ] [[package]] name = "clap_lex" -version = "0.3.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350b9cf31731f9957399229e9b2adc51eeabdfbe9d71d9a0552275fd12710d09" -dependencies = [ - "os_str_bytes", -] +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" [[package]] -name = "codespan-reporting" -version = "0.11.1" +name = "colorchoice" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "termcolor", - "unicode-width", -] +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "core-foundation" @@ -324,50 +384,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "cxx" -version = "1.0.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5add3fc1717409d029b20c5b6903fc0c0b02fa6741d820054f4a2efa5e5816fd" -dependencies = [ - "cc", - "cxxbridge-flags", - "cxxbridge-macro", - "link-cplusplus", -] - -[[package]] -name = "cxx-build" -version = "1.0.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c87959ba14bc6fbc61df77c3fcfe180fc32b93538c4f1031dd802ccb5f2ff0" -dependencies = [ - "cc", - "codespan-reporting", - "once_cell", - "proc-macro2 1.0.56", - "quote 1.0.26", - "scratch", - "syn 1.0.107", -] - -[[package]] -name = "cxxbridge-flags" -version = "1.0.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69a3e162fde4e594ed2b07d0f83c6c67b745e7f28ce58c6df5e6b6bef99dfb59" - -[[package]] -name = "cxxbridge-macro" -version = "1.0.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e7e2adeb6a0d4a282e581096b06e1791532b7d576dcde5ccd9382acf55db8e6" -dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", - "syn 1.0.107", -] - [[package]] name = "darling" version = "0.9.0" @@ -410,10 +426,10 @@ checksum = "ab8bfa2e259f8ee1ce5e97824a3c55ec4404a0d772ca7fa96bf19f0752a046eb" dependencies = [ "fnv", "ident_case", - "proc-macro2 1.0.56", - "quote 1.0.26", + "proc-macro2 1.0.58", + "quote 1.0.27", "strsim 0.10.0", - "syn 2.0.15", + "syn 2.0.16", ] [[package]] @@ -434,8 +450,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a" dependencies = [ "darling_core 0.20.1", - "quote 1.0.26", - "syn 2.0.15", + "quote 1.0.27", + "syn 2.0.16", ] [[package]] @@ -446,9 +462,9 @@ checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" [[package]] name = "digest" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", @@ -468,9 +484,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8da904daa6ce71bb51e03fd0529b08bad4b18bae89e176873c7bae9eb91a19ce" dependencies = [ "darling 0.20.1", - "proc-macro2 1.0.56", - "quote 1.0.26", - "syn 2.0.15", + "proc-macro2 1.0.58", + "quote 1.0.27", + "syn 2.0.16", ] [[package]] @@ -494,13 +510,13 @@ dependencies = [ [[package]] name = "errno" -version = "0.2.8" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" dependencies = [ "errno-dragonfly", "libc", - "winapi", + "windows-sys 0.48.0", ] [[package]] @@ -535,9 +551,9 @@ dependencies = [ [[package]] name = "fake" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429a9ab42c62b5768abe594aa7e405c63c83829643da6027671a36885fe8d55" +checksum = "0a44c765350db469b774425ff1c833890b16ceb9612fb5d7c4bbdf4a1b55f876" dependencies = [ "chrono", "dummy", @@ -658,9 +674,9 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", - "syn 1.0.107", + "proc-macro2 1.0.58", + "quote 1.0.27", + "syn 2.0.16", ] [[package]] @@ -705,9 +721,9 @@ dependencies = [ [[package]] name = "geo-types" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e26879b63ac36ca5492918dc16f8c1e604b0f70f884fffbd3533f89953ab1991" +checksum = "a5f0b3068e1537a4b861ec3734f4aa9c317d537cf0845bf6fb6221973499d26c" dependencies = [ "approx", "num-traits", @@ -734,31 +750,22 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.12.3" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ "ahash", ] [[package]] name = "hashlink" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa" +checksum = "0761a1b9491c4f2e3d66aa0f62d0fba0af9a0e2852e4d48ea506632a4b56e6aa" dependencies = [ "hashbrown", ] -[[package]] -name = "hermit-abi" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] - [[package]] name = "hermit-abi" version = "0.3.1" @@ -802,12 +809,11 @@ dependencies = [ [[package]] name = "iana-time-zone-haiku" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ - "cxx", - "cxx-build", + "cc", ] [[package]] @@ -827,24 +833,25 @@ dependencies = [ [[package]] name = "io-lifetimes" -version = "1.0.4" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7d6c6f8c91b4b9ed43484ad1a938e393caf35960fce7f82a040497207bd8e9e" +checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" dependencies = [ + "hermit-abi", "libc", - "windows-sys 0.42.0", + "windows-sys 0.48.0", ] [[package]] name = "is-terminal" -version = "0.4.2" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dfb6c8100ccc63462345b67d1bbc3679177c75ee4bf59bf29c8b1d110b8189" +checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" dependencies = [ "hermit-abi", "io-lifetimes", "rustix", - "windows-sys 0.42.0", + "windows-sys 0.48.0", ] [[package]] @@ -858,15 +865,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "js-sys" -version = "0.3.61" +version = "0.3.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790" dependencies = [ "wasm-bindgen", ] @@ -879,9 +886,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.139" +version = "0.2.144" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" + +[[package]] +name = "libm" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" [[package]] name = "libsqlite3-sys" @@ -894,20 +907,11 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "link-cplusplus" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" -dependencies = [ - "cc", -] - [[package]] name = "linux-raw-sys" -version = "0.1.4" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "lock_api" @@ -952,7 +956,7 @@ dependencies = [ "libc", "log", "wasi", - "windows-sys 0.42.0", + "windows-sys 0.45.0", ] [[package]] @@ -995,15 +999,15 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.17.0" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" [[package]] name = "openssl" -version = "0.10.45" +version = "0.10.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b102428fd03bc5edf97f62620f7298614c45cedf287c271e7ed450bbaf83f2e1" +checksum = "01b8574602df80f7b85fdfc5392fa884a4e3b3f4f35402c070ab34c3d3f78d56" dependencies = [ "bitflags 1.3.2", "cfg-if", @@ -1020,9 +1024,9 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", - "syn 1.0.107", + "proc-macro2 1.0.58", + "quote 1.0.27", + "syn 2.0.16", ] [[package]] @@ -1033,9 +1037,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.80" +version = "0.9.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23bbbf7854cd45b83958ebe919f0e8e516793727652e27fda10a8384cfc790b7" +checksum = "8e17f59264b2809d77ae94f0e1ebabc434773f370d6ca667bd223ea10e06cc7e" dependencies = [ "cc", "libc", @@ -1043,12 +1047,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "os_str_bytes" -version = "6.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" - [[package]] name = "parking_lot" version = "0.12.1" @@ -1061,9 +1059,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.5" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba" +checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" dependencies = [ "cfg-if", "libc", @@ -1074,9 +1072,9 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba" +checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" [[package]] name = "percent-encoding" @@ -1104,22 +1102,22 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.0.12" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +checksum = "c95a7476719eab1e366eaf73d0260af3021184f18177925b07f54b30089ceead" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.0.12" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07" dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", - "syn 1.0.107", + "proc-macro2 1.0.58", + "quote 1.0.27", + "syn 2.0.16", ] [[package]] @@ -1150,31 +1148,14 @@ dependencies = [ "native-tls", "tokio", "tokio-native-tls", - "tokio-postgres 0.7.8 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio-postgres", ] [[package]] name = "postgres-protocol" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "878c6cbf956e03af9aa8204b407b9cbf47c072164800aa918c516cd4b056c50c" -dependencies = [ - "base64", - "byteorder", - "bytes", - "fallible-iterator", - "hmac", - "md-5", - "memchr", - "rand 0.8.5", - "sha2", - "stringprep", -] - -[[package]] -name = "postgres-protocol" -version = "0.6.5" -source = "git+https://github.com/sfackler/rust-postgres#22469d6b3852da3b6c66f571af1a2982f19930c4" +checksum = "78b7fa9f396f51dffd61546fd8573ee20592287996568e6175ceb0f8699ad75d" dependencies = [ "base64", "byteorder", @@ -1195,6 +1176,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f028f05971fe20f512bcc679e2c10227e57809a3af86a7606304435bc8896cd6" dependencies = [ "bytes", + "chrono", "fallible-iterator", "geo-types", "postgres-protocol", @@ -1210,10 +1192,11 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "predicates" -version = "2.1.5" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd" +checksum = "09963355b9f467184c04017ced4a2ba2d75cbcb4e7462690d388233253d4b1a9" dependencies = [ + "anstyle", "difflib", "itertools", "predicates-core", @@ -1221,15 +1204,15 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f883590242d3c6fc5bf50299011695fa6590c2c70eac95ee1bdb9a733ad1a2" +checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" [[package]] name = "predicates-tree" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54ff541861505aabf6ea722d2131ee980b8276e10a1297b94e896dd8b621850d" +checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" dependencies = [ "predicates-core", "termtree", @@ -1246,9 +1229,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.56" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +checksum = "fa1fb82fc0c281dd9671101b66b771ebbe1eaf967b96ac8740dcba4b70005ca8" dependencies = [ "unicode-ident", ] @@ -1264,11 +1247,11 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" dependencies = [ - "proc-macro2 1.0.56", + "proc-macro2 1.0.58", ] [[package]] @@ -1358,11 +1341,20 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "regex" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" +checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" dependencies = [ "aho-corasick", "memchr", @@ -1377,9 +1369,9 @@ checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" [[package]] name = "regex-syntax" -version = "0.6.28" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" +checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" [[package]] name = "remove_dir_all" @@ -1396,7 +1388,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" dependencies = [ - "bitflags 2.2.1", + "bitflags 2.3.1", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -1407,23 +1399,23 @@ dependencies = [ [[package]] name = "rustix" -version = "0.36.6" +version = "0.37.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4feacf7db682c6c329c4ede12649cd36ecab0f3be5b7d74e6a20304725db4549" +checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" dependencies = [ "bitflags 1.3.2", "errno", "io-lifetimes", "libc", "linux-raw-sys", - "windows-sys 0.42.0", + "windows-sys 0.48.0", ] [[package]] name = "ryu" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" [[package]] name = "schannel" @@ -1449,17 +1441,11 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" -[[package]] -name = "scratch" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" - [[package]] name = "security-framework" -version = "2.8.2" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" +checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" dependencies = [ "bitflags 1.3.2", "core-foundation", @@ -1470,9 +1456,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4" +checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" dependencies = [ "core-foundation-sys", "libc", @@ -1480,29 +1466,29 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.152" +version = "1.0.163" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.152" +version = "1.0.163" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", - "syn 1.0.107", + "proc-macro2 1.0.58", + "quote 1.0.27", + "syn 2.0.16", ] [[package]] name = "serde_json" -version = "1.0.91" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" +checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" dependencies = [ "itoa", "ryu", @@ -1559,9 +1545,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d283f86695ae989d1e18440a943880967156325ba025f05049946bff47bcc2b" +checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" dependencies = [ "libc", "windows-sys 0.48.0", @@ -1591,9 +1577,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "subtle" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" @@ -1608,23 +1594,12 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" -dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.15" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01" dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", + "proc-macro2 1.0.58", + "quote 1.0.27", "unicode-ident", ] @@ -1662,28 +1637,28 @@ dependencies = [ [[package]] name = "termtree" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95059e91184749cb66be6dc994f67f182b6d897cb3df74a5bf66b5e709295fd8" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" [[package]] name = "thiserror" -version = "1.0.38" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.38" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", - "syn 1.0.107", + "proc-macro2 1.0.58", + "quote 1.0.27", + "syn 2.0.16", ] [[package]] @@ -1703,17 +1678,17 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.23.0" +version = "1.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46" +checksum = "0aa32867d44e6f2ce3385e89dceb990188b8bb0fb25b0cf576647a6f98ac5105" dependencies = [ "autocfg", "bytes", "libc", "mio", "pin-project-lite", - "socket2", - "windows-sys 0.42.0", + "socket2 0.4.9", + "windows-sys 0.48.0", ] [[package]] @@ -1743,32 +1718,9 @@ dependencies = [ "percent-encoding", "phf", "pin-project-lite", - "postgres-protocol 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", - "postgres-types 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", - "socket2 0.5.2", - "tokio", - "tokio-util", -] - -[[package]] -name = "tokio-postgres" -version = "0.7.8" -source = "git+https://github.com/sfackler/rust-postgres#22469d6b3852da3b6c66f571af1a2982f19930c4" -dependencies = [ - "async-trait", - "byteorder", - "bytes", - "fallible-iterator", - "futures-channel", - "futures-util", - "log", - "parking_lot", - "percent-encoding", - "phf", - "pin-project-lite", - "postgres-protocol 0.6.5 (git+https://github.com/sfackler/rust-postgres)", - "postgres-types 0.2.5 (git+https://github.com/sfackler/rust-postgres)", - "socket2 0.5.2", + "postgres-protocol", + "postgres-types", + "socket2 0.5.3", "tokio", "tokio-util", ] @@ -1800,9 +1752,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" dependencies = [ "once_cell", ] @@ -1821,9 +1773,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.6" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" [[package]] name = "unicode-normalization" @@ -1834,12 +1786,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-width" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" - [[package]] name = "unicode-xid" version = "0.1.0" @@ -1852,11 +1798,17 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "402bb19d8e03f1d1a7450e2bd613980869438e0666331be3e073089124aa1adc" +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "uuid" -version = "1.3.2" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dad5567ad0cf5b760e5665964bec1b47dfd077ba8a2544b513f3556d3d239a2" +checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2" dependencies = [ "atomic", "getrandom", @@ -1893,9 +1845,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.84" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -1903,47 +1855,47 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.84" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb" dependencies = [ "bumpalo", "log", "once_cell", - "proc-macro2 1.0.56", - "quote 1.0.26", - "syn 1.0.107", + "proc-macro2 1.0.58", + "quote 1.0.27", + "syn 2.0.16", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.84" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258" dependencies = [ - "quote 1.0.26", + "quote 1.0.27", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.84" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", - "syn 1.0.107", + "proc-macro2 1.0.58", + "quote 1.0.27", + "syn 2.0.16", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.84" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" +checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93" [[package]] name = "winapi" diff --git a/Cargo.toml b/Cargo.toml index df2d1032..f3272e28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ members = [ edition = "2021" [workspace.dependencies] +async-trait = "0.1" butane = { version = "0.6", path = "butane" } butane_core = { version = "0.6", path = "butane_core" } butane_codegen = { version = "0.6", path = "butane_codegen" } @@ -28,7 +29,6 @@ fake = "2.6" log = "0.4" once_cell = "1.5.2" paste = "1.0.11" -postgres = "0.19" proc-macro2 = { version = "1.0", default-features = false } quote = { version = "1.0", default-features = false } r2d2 = "0.8" @@ -37,6 +37,8 @@ rusqlite = "0.29" serde = { version = "1.0", default-features = false } serde_json = "1.0" syn = { version = "2", features = ["extra-traits", "full"] } +tokio-postgres = "0.7" +tokio = { version = "1"} uuid = "1.2" [workspace.metadata.release] diff --git a/butane_core/Cargo.toml b/butane_core/Cargo.toml index f428ef05..bdc5c45c 100644 --- a/butane_core/Cargo.toml +++ b/butane_core/Cargo.toml @@ -10,10 +10,10 @@ repository = "https://github.com/Electron100/butane" [features] -datetime = ["chrono", "postgres?/with-chrono-0_4"] +datetime = ["chrono", "tokio-postgres?/with-chrono-0_4"] debug = ["log"] fake = ["dep:fake", "rand"] -json = ["postgres?/with-serde_json-1", "rusqlite?/serde_json"] +json = ["tokio-postgres?/with-serde_json-1", "rusqlite?/serde_json"] sqlite = ["rusqlite"] sqlite-bundled = ["rusqlite/bundled"] tls = ["native-tls", "postgres-native-tls"] @@ -35,7 +35,8 @@ log = { optional = true, workspace = true } native-tls = { version = "0.2", optional = true } once_cell = { workspace = true } pin-project = "1" -postgres = { optional = true, workspace = true } +tokio = {workspace = true, features = ["rt", "sync"]} +tokio-postgres = { optional = true, workspace = true } postgres-native-tls = { version = "0.5", optional = true } proc-macro2 = { workspace = true } quote = { workspace = true } diff --git a/butane_test_helper/Cargo.toml b/butane_test_helper/Cargo.toml index 455a7ebc..aea978fe 100644 --- a/butane_test_helper/Cargo.toml +++ b/butane_test_helper/Cargo.toml @@ -15,7 +15,7 @@ documentation = "https://docs.rs/butane/" butane_core = { features = ["pg", "sqlite"], workspace = true } libc = "0.2" once_cell = { workspace = true } -postgres = { features = ["with-geo-types-0_7"], workspace = true } +tokio-postgres = { features = ["with-geo-types-0_7"], workspace = true } uuid = { features = ["v4"], workspace = true } [package.metadata.release] From b893dd7b8a2a2183ac388c462ef5f7efa55c143d Mon Sep 17 00:00:00 2001 From: James Oakley Date: Sat, 20 May 2023 10:36:16 -0400 Subject: [PATCH 18/78] Delete now explicit --- butane_core/src/db/connmethods.rs | 2 +- butane_core/src/db/macros.rs | 6 ++++-- butane_core/src/db/pg.rs | 6 ++++++ butane_core/src/db/sqlite.rs | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/butane_core/src/db/connmethods.rs b/butane_core/src/db/connmethods.rs index d7e969a4..9c2c01ba 100644 --- a/butane_core/src/db/connmethods.rs +++ b/butane_core/src/db/connmethods.rs @@ -1,7 +1,7 @@ //! Not expected to be called directly by most users. Used by code //! generated by `#[model]`, `query!`, and other macros. -use crate::query::{BoolExpr, Expr, Order}; +use crate::query::{BoolExpr, Order}; use crate::{Result, SqlType, SqlVal, SqlValRef}; use async_trait::async_trait; use std::ops::{Deref, DerefMut}; diff --git a/butane_core/src/db/macros.rs b/butane_core/src/db/macros.rs index c1ededf8..1ee61374 100644 --- a/butane_core/src/db/macros.rs +++ b/butane_core/src/db/macros.rs @@ -63,8 +63,10 @@ macro_rules! connection_method_wrapper { .update(table, pkcol, pk, columns, values) .await } - async fn delete(&self, table: &str, expr: BoolExpr) -> Result { - self.wrapped_connection_methods()?.delete(table, expr).await + async fn delete(&self, table: &str, pkcol: &'static str, pk: SqlVal) -> Result<()> { + self.wrapped_connection_methods()? + .delete(table, pkcol, pk) + .await } async fn delete_where(&self, table: &str, expr: BoolExpr) -> Result { self.wrapped_connection_methods()? diff --git a/butane_core/src/db/pg.rs b/butane_core/src/db/pg.rs index 6a97f036..f16f8089 100644 --- a/butane_core/src/db/pg.rs +++ b/butane_core/src/db/pg.rs @@ -4,6 +4,7 @@ use super::helper; use super::*; use crate::custom::{SqlTypeCustom, SqlValRefCustom}; use crate::migrations::adb::{AColumn, ATable, Operation, TypeIdentifier, ADB}; +use crate::query::Expr; use crate::{debug, query, warn}; use crate::{Result, SqlType, SqlVal, SqlValRef}; use async_trait::async_trait; @@ -318,6 +319,11 @@ where future.await?; Ok(()) } + async fn delete(&self, table: &str, pkcol: &'static str, pk: SqlVal) -> Result<()> { + self.delete_where(table, BoolExpr::Eq(pkcol, Expr::Val(pk))) + .await?; + Ok(()) + } async fn delete_where(&self, table: &str, expr: BoolExpr) -> Result { let mut sql = String::new(); let mut values: Vec = Vec::new(); diff --git a/butane_core/src/db/sqlite.rs b/butane_core/src/db/sqlite.rs index c48b2a8f..086fb0c9 100644 --- a/butane_core/src/db/sqlite.rs +++ b/butane_core/src/db/sqlite.rs @@ -263,7 +263,7 @@ fn rs_conn_has_table(conn: &rusqlite::Connection, table: &str) -> Result { } #[async_trait] -impl Connectionmethods for Mutex { +impl ConnectionMethods for Mutex { fn execute(&self, sql: &str) -> Result<()> { rs_conn_execute(self.lock()?.deref(), sql) } From b876e224541b5818c2f56888a47c41d9e8884bdd Mon Sep 17 00:00:00 2001 From: James Oakley Date: Sun, 21 May 2023 21:14:27 -0400 Subject: [PATCH 19/78] Lots of awawits --- Cargo.lock | 15 +++++++ butane/src/lib.rs | 1 + butane_cli/Cargo.toml | 1 + butane_cli/src/lib.rs | 44 +++++++++---------- butane_cli/src/main.rs | 29 ++++++------ butane_core/src/codegen/dbobj.rs | 8 ++-- butane_core/src/db/mod.rs | 2 + butane_core/src/db/pg.rs | 10 ----- butane_core/src/lib.rs | 7 ++- butane_core/src/query/mod.rs | 14 +++--- butane_test_helper/src/lib.rs | 29 +++++++----- example/Cargo.toml | 1 + example/src/main.rs | 44 +++++++++++-------- examples/getting_started/Cargo.toml | 1 + .../getting_started/src/bin/delete_post.rs | 6 ++- .../getting_started/src/bin/publish_post.rs | 11 +++-- .../getting_started/src/bin/show_posts.rs | 6 ++- .../getting_started/src/bin/write_post.rs | 11 ++--- examples/getting_started/src/lib.rs | 18 ++++---- 19 files changed, 150 insertions(+), 108 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ac3d4441..f758bcc8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -217,6 +217,7 @@ dependencies = [ "quote 1.0.27", "serde", "serde_json", + "tokio", ] [[package]] @@ -535,6 +536,7 @@ version = "0.1.0" dependencies = [ "assert_cmd", "butane", + "tokio", ] [[package]] @@ -746,6 +748,7 @@ name = "getting_started" version = "0.1.0" dependencies = [ "butane", + "tokio", ] [[package]] @@ -1688,9 +1691,21 @@ dependencies = [ "mio", "pin-project-lite", "socket2 0.4.9", + "tokio-macros", "windows-sys 0.48.0", ] +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2 1.0.58", + "quote 1.0.27", + "syn 2.0.16", +] + [[package]] name = "tokio-native-tls" version = "0.3.1" diff --git a/butane/src/lib.rs b/butane/src/lib.rs index 6976b89f..5b51cc88 100644 --- a/butane/src/lib.rs +++ b/butane/src/lib.rs @@ -153,6 +153,7 @@ macro_rules! find { butane::query!($dbobj, $filter) .limit(1) .load($conn) + .await .and_then(|mut results| results.pop().ok_or(butane::Error::NoSuchObject)) }; } diff --git a/butane_cli/Cargo.toml b/butane_cli/Cargo.toml index aa173112..98b0e708 100644 --- a/butane_cli/Cargo.toml +++ b/butane_cli/Cargo.toml @@ -27,3 +27,4 @@ clap = { version = "4.1" } quote = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +tokio = { workspace = true, features = ["macros"] } diff --git a/butane_cli/src/lib.rs b/butane_cli/src/lib.rs index 49835ada..00924f8c 100644 --- a/butane_cli/src/lib.rs +++ b/butane_cli/src/lib.rs @@ -44,14 +44,14 @@ pub fn default_name() -> String { Utc::now().format("%Y%m%d_%H%M%S%3f").to_string() } -pub fn init(name: &str, connstr: &str) -> Result<()> { +pub async fn init(name: &str, connstr: &str) -> Result<()> { if db::get_backend(name).is_none() { eprintln!("Unknown backend {name}"); std::process::exit(1); }; let spec = db::ConnectionSpec::new(name, connstr); - db::connect(&spec)?; // ensure we can + db::connect(&spec).await?; // ensure we can std::fs::create_dir_all(base_dir()?)?; spec.save(&base_dir()?)?; @@ -84,19 +84,19 @@ pub fn make_migration(name: Option<&String>) -> Result<()> { Ok(()) } -pub fn migrate() -> Result<()> { +pub async fn migrate() -> Result<()> { let spec = load_connspec()?; - let mut conn = db::connect(&spec)?; - let to_apply = get_migrations()?.unapplied_migrations(&conn)?; + let mut conn = db::connect(&spec).await?; + let to_apply = get_migrations()?.unapplied_migrations(&conn).await?; println!("{} migrations to apply", to_apply.len()); for m in to_apply { println!("Applying migration {}", m.name()); - m.apply(&mut conn)?; + m.apply(&mut conn).await?; } Ok(()) } -pub fn rollback_to(mut conn: Connection, to: &str) -> Result<()> { +pub async fn rollback_to(mut conn: Connection, to: &str) -> Result<()> { let ms = get_migrations()?; let to_migration = match ms.get_migration(to) { Some(m) => m, @@ -112,16 +112,16 @@ pub fn rollback_to(mut conn: Connection, to: &str) -> Result<()> { } for m in to_unapply.into_iter().rev() { println!("Rolling back migration {}", m.name()); - m.downgrade(&mut conn)?; + m.downgrade(&mut conn).await?; } Ok(()) } -pub fn rollback_latest(mut conn: Connection) -> Result<()> { +pub async fn rollback_latest(mut conn: Connection) -> Result<()> { match get_migrations()?.latest() { Some(m) => { println!("Rolling back migration {}", m.name()); - m.downgrade(&mut conn)?; + m.downgrade(&mut conn).await?; } None => { eprintln!("No migrations applied!"); @@ -177,11 +177,11 @@ pub fn load_connspec() -> Result { } } -pub fn list_migrations() -> Result<()> { +pub async fn list_migrations() -> Result<()> { let spec = load_connspec()?; - let conn = db::connect(&spec)?; + let conn = db::connect(&spec).await?; let ms = get_migrations()?; - let unapplied = ms.unapplied_migrations(&conn)?; + let unapplied = ms.unapplied_migrations(&conn).await?; let all = ms.all_migrations()?; for m in all { let m_state = match unapplied.contains(&m) { @@ -193,25 +193,25 @@ pub fn list_migrations() -> Result<()> { Ok(()) } -pub fn collapse_migrations(new_initial_name: Option<&String>) -> Result<()> { +pub async fn collapse_migrations(new_initial_name: Option<&String>) -> Result<()> { let name = match new_initial_name { Some(name) => format!("{}_{}", default_name(), name), None => default_name(), }; let spec = load_connspec()?; let backend = spec.get_backend()?; - let conn = db::connect(&spec)?; + let conn = db::connect(&spec).await?; let mut ms = get_migrations()?; - let latest = ms.last_applied_migration(&conn)?; + let latest = ms.last_applied_migration(&conn).await?; if latest.is_none() { eprintln!("There are no migrations to collapse"); std::process::exit(1); } let latest_db = latest.unwrap().db()?; - ms.clear_migrations(&conn)?; + ms.clear_migrations(&conn).await?; ms.create_migration_to(&backend, &name, None, latest_db)?; let new_migration = ms.latest().unwrap(); - new_migration.mark_applied(&conn)?; + new_migration.mark_applied(&conn).await?; let cli_state = CliState::load()?; if cli_state.embedded { // Update the embedding @@ -228,10 +228,10 @@ pub fn delete_table(name: &str) -> Result<()> { Ok(()) } -pub fn clear_data() -> Result<()> { +pub async fn clear_data() -> Result<()> { let spec = load_connspec()?; - let conn = db::connect(&spec)?; - let latest = match get_migrations()?.last_applied_migration(&conn)? { + let conn = db::connect(&spec).await?; + let latest = match get_migrations()?.last_applied_migration(&conn).await? { Some(m) => m, None => { eprintln!("No migrations have been applied, so no data is recognized."); @@ -240,7 +240,7 @@ pub fn clear_data() -> Result<()> { }; for table in latest.db()?.tables() { println!("Deleting data from {}", &table.name); - conn.delete_where(&table.name, BoolExpr::True)?; + conn.delete_where(&table.name, BoolExpr::True).await?; } Ok(()) } diff --git a/butane_cli/src/main.rs b/butane_cli/src/main.rs index db7e4af1..f2516f79 100644 --- a/butane_cli/src/main.rs +++ b/butane_cli/src/main.rs @@ -5,7 +5,8 @@ use butane_cli::{ migrate, Result, }; -fn main() { +#[tokio::main(flavor = "current_thread")] +async fn main() { let app = clap::Command::new("butane") .version(env!("CARGO_PKG_VERSION")) .author("James Oakley ") @@ -85,15 +86,17 @@ fn main() { .arg_required_else_help(true); let args = app.get_matches(); match args.subcommand() { - Some(("init", sub_args)) => handle_error(init(Some(sub_args))), + Some(("init", sub_args)) => handle_error(init(Some(sub_args)).await), Some(("makemigration", sub_args)) => handle_error(make_migration(Some(sub_args))), - Some(("migrate", _)) => handle_error(migrate()), - Some(("rollback", sub_args)) => handle_error(rollback(Some(sub_args))), + Some(("migrate", _)) => handle_error(migrate().await), + Some(("rollback", sub_args)) => handle_error(rollback(Some(sub_args)).await), Some(("embed", _)) => handle_error(embed()), - Some(("list", _)) => handle_error(list_migrations()), - Some(("collapse", sub_args)) => handle_error(collapse_migrations(sub_args.get_one("NAME"))), + Some(("list", _)) => handle_error(list_migrations().await), + Some(("collapse", sub_args)) => { + handle_error(collapse_migrations(sub_args.get_one("NAME")).await) + } Some(("clear", sub_args)) => match sub_args.subcommand() { - Some(("data", _)) => handle_error(clear_data()), + Some(("data", _)) => handle_error(clear_data().await), _ => eprintln!("Unknown clear command. Try: clear data"), }, Some(("delete", sub_args)) => match sub_args.subcommand() { @@ -108,11 +111,11 @@ fn main() { } } -fn init(args: Option<&ArgMatches>) -> Result<()> { +async fn init(args: Option<&ArgMatches>) -> Result<()> { let args = args.unwrap(); let name: &String = args.get_one("BACKEND").unwrap(); let connstr: &String = args.get_one("CONNECTION").unwrap(); - butane_cli::init(name, connstr) + butane_cli::init(name, connstr).await } fn make_migration(args: Option<&ArgMatches>) -> Result<()> { @@ -120,12 +123,12 @@ fn make_migration(args: Option<&ArgMatches>) -> Result<()> { butane_cli::make_migration(name_arg) } -fn rollback(args: Option<&ArgMatches>) -> Result<()> { +async fn rollback(args: Option<&ArgMatches>) -> Result<()> { let spec = butane_cli::load_connspec()?; - let conn = butane::db::connect(&spec)?; + let conn = butane::db::connect(&spec).await?; match args.and_then(|a| a.get_one::("NAME")) { - Some(to) => butane_cli::rollback_to(conn, to), - None => butane_cli::rollback_latest(conn), + Some(to) => butane_cli::rollback_to(conn, to).await, + None => butane_cli::rollback_latest(conn).await, } } diff --git a/butane_core/src/codegen/dbobj.rs b/butane_core/src/codegen/dbobj.rs index d41059ad..1635245e 100644 --- a/butane_core/src/codegen/dbobj.rs +++ b/butane_core/src/codegen/dbobj.rs @@ -45,7 +45,7 @@ pub fn impl_dbobject(ast_struct: &ItemStruct, config: &Config) -> TokenStream2 { // Save needs to ensure_initialized quote!( self.#ident.ensure_init(#many_table_lit, butane::ToSql::to_sql(self.pk()), #pksqltype); - self.#ident.save(conn)?; + self.#ident.save(conn).await?; ) }).collect(); @@ -78,11 +78,11 @@ pub fn impl_dbobject(ast_struct: &ItemStruct, config: &Config) -> TokenStream2 { conn.update(Self::TABLE, pkcol, butane::ToSql::to_sql_ref(self.pk()), - &[#save_cols], &values)?; + &[#save_cols], &values).await?; } } else { #(#values)* - let pk = conn.insert_returning_pk(Self::TABLE, &[#insert_cols], &pkcol, &values)?; + let pk = conn.insert_returning_pk(Self::TABLE, &[#insert_cols], &pkcol, &values).await?; #(#post_insert)* } #many_save @@ -91,7 +91,7 @@ pub fn impl_dbobject(ast_struct: &ItemStruct, config: &Config) -> TokenStream2 { async fn delete(&self, conn: &impl butane::db::ConnectionMethods) -> butane::Result<()> { use butane::ToSql; use butane::prelude::DataObject; - conn.delete(Self::TABLE, Self::PKCOL, self.pk().to_sql()) + conn.delete(Self::TABLE, Self::PKCOL, self.pk().to_sql()).await } } impl butane::ToSql for #tyname { diff --git a/butane_core/src/db/mod.rs b/butane_core/src/db/mod.rs index b1b1bdb3..4f9a045c 100644 --- a/butane_core/src/db/mod.rs +++ b/butane_core/src/db/mod.rs @@ -28,6 +28,8 @@ mod helper; mod macros; #[cfg(feature = "pg")] pub mod pg; +//#[cfg(feature = "sqlite")] +//pub mod sqlite; #[cfg(feature = "r2d2")] pub mod r2; diff --git a/butane_core/src/db/pg.rs b/butane_core/src/db/pg.rs index f16f8089..747538f8 100644 --- a/butane_core/src/db/pg.rs +++ b/butane_core/src/db/pg.rs @@ -129,16 +129,6 @@ impl Debug for PgConnection { d.finish() } } -impl Debug for PgConnection { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut d = f.debug_struct("PgConnection"); - #[cfg(feature = "debug")] - d.field("params", &self.params); - // postgres::Client doesnt expose any internal state - d.field("conn", &!self.is_closed()); - d.finish() - } -} type DynToSqlPg<'a> = (dyn postgres::types::ToSql + Sync + 'a); diff --git a/butane_core/src/lib.rs b/butane_core/src/lib.rs index cb07aaec..86b46a31 100644 --- a/butane_core/src/lib.rs +++ b/butane_core/src/lib.rs @@ -104,11 +104,14 @@ pub trait DataObject: DataResult + Sync { Self: Sized, Self::PKType: Sync, { - Self::try_get(conn, id)?.ok_or(Error::NoSuchObject) + Self::try_get(conn, id).await?.ok_or(Error::NoSuchObject) } /// Find this object in the database based on primary key. /// Returns `None` if the primary key does not exist. - async fn try_get(conn: &impl ConnectionMethods, id: impl Borrow + Send + Sync) -> Result> + async fn try_get( + conn: &impl ConnectionMethods, + id: impl Borrow + Send + Sync, + ) -> Result> where Self: Sized, { diff --git a/butane_core/src/query/mod.rs b/butane_core/src/query/mod.rs index 5d56976d..815d6eb0 100644 --- a/butane_core/src/query/mod.rs +++ b/butane_core/src/query/mod.rs @@ -178,7 +178,7 @@ impl Query { self.order(column, OrderDirection::Descending) } - fn fetch( + async fn fetch( self, conn: &impl ConnectionMethods, limit: Option, @@ -196,21 +196,23 @@ impl Query { self.offset, sort, ) + .await } /// Executes the query against `conn` and returns the first result (if any). - pub fn load_first(self, conn: &impl ConnectionMethods) -> Result> { - self.fetch(conn, Some(1))?.mapped(T::from_row).nth(0) + pub async fn load_first(self, conn: &impl ConnectionMethods) -> Result> { + self.fetch(conn, Some(1)).await?.mapped(T::from_row).nth(0) } /// Executes the query against `conn`. - pub fn load(self, conn: &impl ConnectionMethods) -> Result> { + pub async fn load(self, conn: &impl ConnectionMethods) -> Result> { let limit = self.limit.to_owned(); - self.fetch(conn, limit)?.mapped(T::from_row).collect() + self.fetch(conn, limit).await?.mapped(T::from_row).collect() } /// Executes the query against `conn` and deletes all matching objects. pub async fn delete(self, conn: &impl ConnectionMethods) -> Result { - conn.delete_where(&self.table, self.filter.unwrap_or(BoolExpr::True)).await + conn.delete_where(&self.table, self.filter.unwrap_or(BoolExpr::True)) + .await } } diff --git a/butane_test_helper/src/lib.rs b/butane_test_helper/src/lib.rs index 83a35272..5dc21970 100644 --- a/butane_test_helper/src/lib.rs +++ b/butane_test_helper/src/lib.rs @@ -1,4 +1,6 @@ -use butane_core::db::{connect, get_backend, pg, sqlite, Backend, Connection, ConnectionSpec}; +use butane_core::db::{connect, get_backend, pg, Backend, Connection, ConnectionSpec}; +//todo +//use butane_core::db::sqlite; use butane_core::migrations::{self, MemMigrations, Migration, Migrations, MigrationsMut}; use once_cell::sync::Lazy; use std::io::{BufRead, BufReader, Read, Write}; @@ -8,14 +10,14 @@ use std::process::{ChildStderr, Command, Stdio}; use std::sync::Mutex; use uuid::Uuid; -pub fn pg_connection() -> (Connection, PgSetupData) { +pub async fn pg_connection() -> (Connection, PgSetupData) { let backend = get_backend(pg::BACKEND_NAME).unwrap(); - let data = pg_setup(); - (backend.connect(&pg_connstr(&data)).unwrap(), data) + let data = pg_setup().await; + (backend.connect(&pg_connstr(&data)).await.unwrap(), data) } -pub fn pg_connspec() -> (ConnectionSpec, PgSetupData) { - let data = pg_setup(); +pub async fn pg_connspec() -> (ConnectionSpec, PgSetupData) { + let data = pg_setup().await; ( ConnectionSpec::new(pg::BACKEND_NAME, pg_connstr(&data)), data, @@ -116,7 +118,7 @@ extern "C" fn proc_teardown() { static TMP_SERVER: Lazy>> = Lazy::new(|| Mutex::new(Some(create_tmp_server()))); -pub fn pg_setup() -> PgSetupData { +pub async fn pg_setup() -> PgSetupData { eprintln!("pg_setup"); // By default we set up a temporary, local postgres server just // for this test. This can be overridden by the environment @@ -133,8 +135,9 @@ pub fn pg_setup() -> PgSetupData { let new_dbname = format!("butane_test_{}", Uuid::new_v4().simple()); eprintln!("new db is `{}`", &new_dbname); - let mut conn = connect(&ConnectionSpec::new("pg", &connstr)).unwrap(); + let mut conn = connect(&ConnectionSpec::new("pg", &connstr)).await.unwrap(); conn.execute(format!("CREATE DATABASE {new_dbname};")) + .await .unwrap(); let connstr = format!("{connstr} dbname={new_dbname}"); @@ -148,7 +151,7 @@ pub fn pg_connstr(data: &PgSetupData) -> String { data.connstr.clone() } -pub fn setup_db(backend: Box, conn: &mut Connection, migrate: bool) { +pub async fn setup_db(backend: Box, conn: &mut Connection, migrate: bool) { let mut root = std::env::current_dir().unwrap(); root.push(".butane/migrations"); let mut disk_migrations = migrations::from_root(&root); @@ -172,14 +175,15 @@ pub fn setup_db(backend: Box, conn: &mut Connection, migrate: bool) "expected to create migration" ); println!("created current migration"); - let to_apply = mem_migrations.unapplied_migrations(conn).unwrap(); + let to_apply = mem_migrations.unapplied_migrations(conn).await.unwrap(); for m in to_apply { println!("Applying migration {}", m.name()); - m.apply(conn).unwrap(); + m.apply(conn).await.unwrap(); } } -pub fn sqlite_connection() -> Connection { +// todo +/*pub fn sqlite_connection() -> Connection { let backend = get_backend(sqlite::BACKEND_NAME).unwrap(); backend.connect(":memory:").unwrap() } @@ -190,6 +194,7 @@ pub fn sqlite_connspec() -> ConnectionSpec { pub fn sqlite_setup() {} pub fn sqlite_teardown(_: ()) {} +*/ #[macro_export] macro_rules! maketest { diff --git a/example/Cargo.toml b/example/Cargo.toml index 9269ccda..8bb4198f 100644 --- a/example/Cargo.toml +++ b/example/Cargo.toml @@ -8,6 +8,7 @@ build = "build.rs" [dependencies] butane = { features = ["sqlite"], workspace = true } +tokio = { workspace = true, features = ["macros"] } [dev-dependencies] assert_cmd = "2.0" diff --git a/example/src/main.rs b/example/src/main.rs index bd9d6e7e..2d9c913f 100644 --- a/example/src/main.rs +++ b/example/src/main.rs @@ -54,52 +54,60 @@ struct Tag { tag: String, } -fn query() -> Result<()> { - let conn = establish_connection()?; +async fn query() -> Result<()> { + let conn = establish_connection().await?; let mut blog = Blog { name: "Bears".into(), ..Default::default() }; - blog.save(&conn).unwrap(); + blog.save(&conn).await.unwrap(); let mut tag = Tag { tag: "dinosaurs".into(), ..Default::default() }; - tag.save(&conn).unwrap(); + tag.save(&conn).await.unwrap(); let mut post = Post::new(&blog, "Grizzly".into(), "lorem ipsum".into()); post.published = true; post.tags.add(&tag)?; - post.save(&conn).unwrap(); + post.save(&conn).await.unwrap(); - let _specific_post = Post::get(&conn, 1)?; - let published_posts = query!(Post, published == true).limit(5).load(&conn)?; + let _specific_post = Post::get(&conn, 1).await?; + let published_posts = query!(Post, published == true).limit(5).load(&conn).await?; assert!(!published_posts.is_empty()); - let unliked_posts = query!(Post, published == true && likes < 5).load(&conn)?; + let unliked_posts = query!(Post, published == true && likes < 5) + .load(&conn) + .await?; assert!(!unliked_posts.is_empty()); - let _blog: &Blog = unliked_posts.first().unwrap().blog.load(&conn)?; - let tagged_posts = query!(Post, tags.contains("dinosaurs")).load(&conn)?; + let _blog: &Blog = unliked_posts.first().unwrap().blog.load(&conn).await?; + let tagged_posts = query!(Post, tags.contains("dinosaurs")).load(&conn).await?; assert!(!tagged_posts.is_empty()); - let tagged_posts = query!(Post, tags.contains(tag == "dinosaurs")).load(&conn)?; + let tagged_posts = query!(Post, tags.contains(tag == "dinosaurs")) + .load(&conn) + .await?; assert!(!tagged_posts.is_empty()); let blog: Blog = find!(Blog, name == "Bears", &conn).unwrap(); - let posts_in_blog = query!(Post, blog == { &blog }).load(&conn)?; + let posts_in_blog = query!(Post, blog == { &blog }).load(&conn).await?; assert!(!posts_in_blog.is_empty()); - let posts_in_blog = query!(Post, blog == { blog }).load(&conn)?; + let posts_in_blog = query!(Post, blog == { blog }).load(&conn).await?; assert!(!posts_in_blog.is_empty()); - let posts_in_blog = query!(Post, blog.matches(name == "Bears")).load(&conn)?; + let posts_in_blog = query!(Post, blog.matches(name == "Bears")) + .load(&conn) + .await?; assert!(!posts_in_blog.is_empty()); Ok(()) } -fn establish_connection() -> Result { +async fn establish_connection() -> Result { let mut cwd = std::env::current_dir()?; cwd.push(".butane"); let spec = ConnectionSpec::load(cwd)?; - let conn = butane::db::connect(&spec)?; + let conn = butane::db::connect(&spec).await?; Ok(conn) } -fn main() -> Result<()> { - query() + +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<()> { + query().await } diff --git a/examples/getting_started/Cargo.toml b/examples/getting_started/Cargo.toml index 2b391ae9..01115f11 100644 --- a/examples/getting_started/Cargo.toml +++ b/examples/getting_started/Cargo.toml @@ -22,6 +22,7 @@ doc = false [dependencies] butane = { features = ["default", "sqlite"], workspace = true } +tokio = { workspace = true, features = ["macros"] } [package.metadata.release] diff --git a/examples/getting_started/src/bin/delete_post.rs b/examples/getting_started/src/bin/delete_post.rs index dc979464..2e1369cd 100644 --- a/examples/getting_started/src/bin/delete_post.rs +++ b/examples/getting_started/src/bin/delete_post.rs @@ -3,13 +3,15 @@ use butane::query; use getting_started::*; use std::env::args; -fn main() { +#[tokio::main(flavor = "current_thread")] +async fn main() { let target = args().nth(1).expect("Expected a target to match against"); let pattern = format!("%{target}%"); - let conn = establish_connection(); + let conn = establish_connection().await; let cnt = query!(Post, title.like({ pattern })) .delete(&conn) + .await .expect("error deleting posts"); println!("Deleted {cnt} posts"); } diff --git a/examples/getting_started/src/bin/publish_post.rs b/examples/getting_started/src/bin/publish_post.rs index d6bca977..69dd6d9e 100644 --- a/examples/getting_started/src/bin/publish_post.rs +++ b/examples/getting_started/src/bin/publish_post.rs @@ -4,17 +4,20 @@ use butane::prelude::*; use getting_started::*; use std::env::args; -fn main() { +#[tokio::main(flavor = "current_thread")] +async fn main() { let id = args() .nth(1) .expect("publish_post requires a post id") .parse::() .expect("Invalid ID"); - let conn = establish_connection(); + let conn = establish_connection().await; - let mut post = Post::get(&conn, id).expect(&format!("Unable to find post {id}")); + let mut post = Post::get(&conn, id) + .await + .expect(&format!("Unable to find post {id}")); // Just a normal Rust assignment, no fancy set methods post.published = true; - post.save(&conn).unwrap(); + post.save(&conn).await.unwrap(); println!("Published post {}", post.title); } diff --git a/examples/getting_started/src/bin/show_posts.rs b/examples/getting_started/src/bin/show_posts.rs index 1f0103c3..4f1bba46 100644 --- a/examples/getting_started/src/bin/show_posts.rs +++ b/examples/getting_started/src/bin/show_posts.rs @@ -2,11 +2,13 @@ use butane::query; use getting_started::models::*; use getting_started::*; -fn main() { - let conn = establish_connection(); +#[tokio::main(flavor = "current_thread")] +async fn main() { + let conn = establish_connection().await; let results = query!(Post, published == true) .limit(5) .load(&conn) + .await .expect("Error loading posts"); println!("Displaying {} posts", results.len()); for post in results { diff --git a/examples/getting_started/src/bin/write_post.rs b/examples/getting_started/src/bin/write_post.rs index 6bdf6259..c438f410 100644 --- a/examples/getting_started/src/bin/write_post.rs +++ b/examples/getting_started/src/bin/write_post.rs @@ -1,15 +1,16 @@ use getting_started::*; use std::io::{stdin, Read}; -fn main() { - let conn = establish_connection(); +#[tokio::main(flavor = "current_thread")] +async fn main() { + let conn = establish_connection().await; - let blog = match existing_blog(&conn) { + let blog = match existing_blog(&conn).await { Some(blog) => blog, None => { println!("Enter blog name"); let name = readline(); - create_blog(&conn, name) + create_blog(&conn, name).await } }; @@ -19,7 +20,7 @@ fn main() { let mut body = String::new(); stdin().read_to_string(&mut body).unwrap(); - let post = create_post(&conn, &blog, title, body); + let post = create_post(&conn, &blog, title, body).await; println!( "\nSaved unpublished post {} with id {}", post.title, post.id diff --git a/examples/getting_started/src/lib.rs b/examples/getting_started/src/lib.rs index fbfcf5f7..5db27255 100644 --- a/examples/getting_started/src/lib.rs +++ b/examples/getting_started/src/lib.rs @@ -4,22 +4,24 @@ use butane::db::{Connection, ConnectionSpec}; use butane::prelude::*; use models::{Blog, Post}; -pub fn establish_connection() -> Connection { - butane::db::connect(&ConnectionSpec::load(".butane/connection.json").unwrap()).unwrap() +pub async fn establish_connection() -> Connection { + butane::db::connect(&ConnectionSpec::load(".butane/connection.json").unwrap()) + .await + .unwrap() } -pub fn create_blog(conn: &Connection, name: impl Into) -> Blog { +pub async fn create_blog(conn: &Connection, name: impl Into) -> Blog { let mut blog = Blog::new(name); - blog.save(conn).unwrap(); + blog.save(conn).await.unwrap(); blog } -pub fn create_post(conn: &Connection, blog: &Blog, title: String, body: String) -> Post { +pub async fn create_post(conn: &Connection, blog: &Blog, title: String, body: String) -> Post { let mut new_post = Post::new(blog, title, body); - new_post.save(conn).unwrap(); + new_post.save(conn).await.unwrap(); new_post } -pub fn existing_blog(conn: &Connection) -> Option { - Blog::query().load_first(conn).unwrap() +pub async fn existing_blog(conn: &Connection) -> Option { + Blog::query().load_first(conn).await.unwrap() } From f3b719342738796d2334ef3ed3c2dc94940adb04 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Mon, 29 May 2023 14:41:36 -0400 Subject: [PATCH 20/78] Test passing, sqlite disabled --- Cargo.lock | 50 ++++++++- Cargo.toml | 1 + butane/Cargo.toml | 10 +- butane/src/lib.rs | 14 ++- butane/tests/basic.rs | 152 +++++++++++++++------------- butane/tests/common/blog.rs | 22 ++-- butane/tests/custom_enum_derived.rs | 13 +-- butane/tests/custom_pg.rs | 6 +- butane/tests/custom_type.rs | 13 +-- butane/tests/fake.rs | 14 +-- butane/tests/json.rs | 50 ++++----- butane/tests/many.rs | 64 ++++++------ butane/tests/migration-tests.rs | 68 +++++++------ butane/tests/nullable.rs | 30 +++--- butane/tests/query.rs | 107 +++++++++++--------- butane/tests/r2d2.rs | 1 + butane/tests/uuid.rs | 10 +- butane_cli/Cargo.toml | 6 +- butane_core/Cargo.toml | 8 +- butane_core/src/db/mod.rs | 10 +- butane_core/src/db/pg.rs | 1 + butane_core/tests/connection.rs | 16 +-- butane_core/tests/transactions.rs | 18 ++-- butane_test_helper/Cargo.toml | 3 +- butane_test_helper/src/lib.rs | 22 ++-- example/Cargo.toml | 3 +- example/tests/test_cli.rs | 3 + examples/getting_started/Cargo.toml | 3 +- 28 files changed, 411 insertions(+), 307 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0eec92bc..a4e0359b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,6 +110,28 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2 1.0.58", + "quote 1.0.27", + "syn 2.0.16", +] + [[package]] name = "async-trait" version = "0.1.68" @@ -202,7 +224,9 @@ dependencies = [ "rusqlite", "serde", "serde_json", + "tokio", "tokio-postgres", + "tokio-test", "uuid", ] @@ -254,7 +278,6 @@ dependencies = [ "postgres-native-tls", "proc-macro2 1.0.58", "quote 1.0.27", - "r2d2", "rand 0.8.5", "regex", "rusqlite", @@ -905,7 +928,6 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" dependencies = [ - "cc", "pkg-config", "vcpkg", ] @@ -1740,6 +1762,30 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-test" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53474327ae5e166530d17f2d956afcb4f8a004de581b3cae10f12006bc8163e3" +dependencies = [ + "async-stream", + "bytes", + "futures-core", + "tokio", + "tokio-stream", +] + [[package]] name = "tokio-util" version = "0.7.8" diff --git a/Cargo.toml b/Cargo.toml index 111f825f..2f77ddf0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ serde_json = "1.0" syn = { version = "2", features = ["extra-traits", "full"] } tokio-postgres = "0.7" tokio = { version = "1"} +tokio-test = { version = "0.4"} uuid = "1.2" [workspace.metadata.release] diff --git a/butane/Cargo.toml b/butane/Cargo.toml index 0a3c53fe..aa36ca27 100644 --- a/butane/Cargo.toml +++ b/butane/Cargo.toml @@ -16,13 +16,15 @@ build = "build.rs" default = ["datetime", "json", "uuid"] fake = ["butane_core/fake"] json = ["butane_codegen/json", "butane_core/json"] -sqlite = ["butane_core/sqlite"] -sqlite-bundled = ["butane_core/sqlite-bundled"] +# todo re-enable sqlite +#sqlite = ["butane_core/sqlite"] +#sqlite-bundled = ["butane_core/sqlite-bundled"] pg = ["butane_core/pg"] datetime = ["butane_codegen/datetime", "butane_core/datetime"] debug = ["butane_core/debug"] log = ["butane_core/log"] -r2d2 = ["butane_core/r2d2"] +# todo re-enable r2d2 +#r2d2 = ["butane_core/r2d2"] tls = ["butane_core/tls"] uuid = ["butane_codegen/uuid", "butane_core/uuid"] @@ -44,7 +46,9 @@ log_for_test = { package = "log", version = "0.4" } quote = { workspace = true } proc-macro2 = { workspace = true } once_cell = { workspace = true } +tokio = { workspace = true } tokio-postgres = { features = ["with-geo-types-0_7"], workspace = true } +tokio-test = { workspace = true } rand = { workspace = true } r2d2_for_test = { package = "r2d2", version = "0.8" } rusqlite = { workspace = true } diff --git a/butane/src/lib.rs b/butane/src/lib.rs index e763f75c..2efefb56 100644 --- a/butane/src/lib.rs +++ b/butane/src/lib.rs @@ -60,10 +60,12 @@ pub mod db { /// rank: i32, /// nationality: String /// } -/// let e: BoolExpr = filter!(Contestant, nationality == "US" && rank < 42); -/// let first_place = 1; -/// let e2 = filter!(Contestant, rank == { first_place }); -/// let e3 = filter!(Contestant, name.like("A%")); +/// # tokio_test::block_on(async { +/// let e: BoolExpr = filter!(Contestant, nationality == "US" && rank < 42); +/// let first_place = 1; +/// let e2 = filter!(Contestant, rank == { first_place }); +/// let e3 = filter!(Contestant, name.like("A%")); +/// # }) ///``` /// /// [`BoolExpr`]: crate::query::BoolExpr @@ -140,8 +142,10 @@ macro_rules! colname { /// nationality: String /// } /// -/// let conn = butane::db::connect(&ConnectionSpec::new("sqlite", "foo.db")).unwrap(); +/// # tokio_test::block_on(async { +/// let conn = butane::db::connect(&ConnectionSpec::new("sqlite", "foo.db")).await.unwrap(); /// let alice: Result = find!(Contestant, name == "Alice", &conn); +/// # }) ///``` /// /// [`filter]: crate::filter diff --git a/butane/tests/basic.rs b/butane/tests/basic.rs index d810fd23..44892349 100644 --- a/butane/tests/basic.rs +++ b/butane/tests/basic.rs @@ -3,9 +3,9 @@ use butane::{butane_type, find, model, query}; use butane::{colname, prelude::*}; use butane::{ForeignKey, ObjectState}; use chrono::{naive::NaiveDateTime, offset::Utc, DateTime}; -use serde::Serialize; #[cfg(feature = "sqlite")] use rusqlite; +use serde::Serialize; #[cfg(feature = "pg")] use tokio_postgres as postgres; @@ -109,47 +109,47 @@ struct TimeHolder { pub utc2: chrono::DateTime, } -fn basic_crud(conn: Connection) { +async fn basic_crud(conn: Connection) { //create let mut foo = Foo::new(1); foo.bam = 0.1; foo.bar = 42; foo.baz = "hello world".to_string(); foo.blobbity = [1u8, 2u8, 3u8].to_vec(); - foo.save(&conn).unwrap(); + foo.save(&conn).await.unwrap(); // read - let mut foo2 = Foo::get(&conn, 1).unwrap(); + let mut foo2 = Foo::get(&conn, 1).await.unwrap(); assert_eq!(foo, foo2); - assert_eq!(Some(foo), Foo::try_get(&conn, 1).unwrap()); + assert_eq!(Some(foo), Foo::try_get(&conn, 1).await.unwrap()); // update foo2.bam = 0.2; foo2.bar = 43; - foo2.save(&conn).unwrap(); - let foo3 = Foo::get(&conn, 1).unwrap(); + foo2.save(&conn).await.unwrap(); + let foo3 = Foo::get(&conn, 1).await.unwrap(); assert_eq!(foo2, foo3); // delete - assert!(foo3.delete(&conn).is_ok()); - if let Some(butane::Error::NoSuchObject) = Foo::get(&conn, 1).err() { + assert!(foo3.delete(&conn).await.is_ok()); + if let Some(butane::Error::NoSuchObject) = Foo::get(&conn, 1).await.err() { } else { panic!("Expected NoSuchObject"); } - assert_eq!(None, Foo::try_get(&conn, 1).unwrap()); + assert_eq!(None, Foo::try_get(&conn, 1).await.unwrap()); } testall!(basic_crud); -fn basic_find(conn: Connection) { +async fn basic_find(conn: Connection) { //create let mut foo1 = Foo::new(1); foo1.bar = 42; foo1.baz = "hello world".to_string(); - foo1.save(&conn).unwrap(); + foo1.save(&conn).await.unwrap(); let mut foo2 = Foo::new(2); foo2.bar = 43; foo2.baz = "hello world".to_string(); - foo2.save(&conn).unwrap(); + foo2.save(&conn).await.unwrap(); // find let found: Foo = find!(Foo, bar == 43, &conn).unwrap(); @@ -157,72 +157,75 @@ fn basic_find(conn: Connection) { } testall!(basic_find); -fn basic_query(conn: Connection) { +async fn basic_query(conn: Connection) { //create let mut foo1 = Foo::new(1); foo1.bar = 42; foo1.baz = "hello world".to_string(); - foo1.save(&conn).unwrap(); + foo1.save(&conn).await.unwrap(); let mut foo2 = Foo::new(2); foo2.bar = 43; foo2.baz = "hello world".to_string(); - foo2.save(&conn).unwrap(); + foo2.save(&conn).await.unwrap(); // query finds 1 - let mut found = query!(Foo, bar == 42).load(&conn).unwrap(); + let mut found = query!(Foo, bar == 42).load(&conn).await.unwrap(); assert_eq!(found.len(), 1); assert_eq!(found.pop().unwrap(), foo1); // query finds both - let found = query!(Foo, bar < 44).load(&conn).unwrap(); + let found = query!(Foo, bar < 44).load(&conn).await.unwrap(); assert_eq!(found.len(), 2); } testall!(basic_query); -fn basic_query_delete(conn: Connection) { +async fn basic_query_delete(conn: Connection) { //create let mut foo1 = Foo::new(1); foo1.bar = 42; foo1.baz = "hello world".to_string(); - foo1.save(&conn).unwrap(); + foo1.save(&conn).await.unwrap(); let mut foo2 = Foo::new(2); foo2.bar = 43; foo2.baz = "hello world".to_string(); - foo2.save(&conn).unwrap(); + foo2.save(&conn).await.unwrap(); let mut foo3 = Foo::new(3); foo3.bar = 44; foo3.baz = "goodbye world".to_string(); - foo3.save(&conn).unwrap(); + foo3.save(&conn).await.unwrap(); // delete just the last one - let cnt = query!(Foo, baz == "goodbye world").delete(&conn).unwrap(); + let cnt = query!(Foo, baz == "goodbye world") + .delete(&conn) + .await + .unwrap(); assert_eq!(cnt, 1); // delete the other two - let cnt = query!(Foo, baz.like("hello%")).delete(&conn).unwrap(); + let cnt = query!(Foo, baz.like("hello%")).delete(&conn).await.unwrap(); assert_eq!(cnt, 2); } testall!(basic_query_delete); -fn string_pk(conn: Connection) { +async fn string_pk(conn: Connection) { let mut foo = Foo::new(1); - foo.save(&conn).unwrap(); + foo.save(&conn).await.unwrap(); let mut bar = Bar::new("tarzan", foo); - bar.save(&conn).unwrap(); + bar.save(&conn).await.unwrap(); - let bar2 = Bar::get(&conn, "tarzan".to_string()).unwrap(); + let bar2 = Bar::get(&conn, "tarzan".to_string()).await.unwrap(); assert_eq!(bar, bar2); } testall!(string_pk); -fn foreign_key(conn: Connection) { +async fn foreign_key(conn: Connection) { let mut foo = Foo::new(1); - foo.save(&conn).unwrap(); + foo.save(&conn).await.unwrap(); let mut bar = Bar::new("tarzan", foo.clone()); - bar.save(&conn).unwrap(); - let bar2 = Bar::get(&conn, "tarzan".to_string()).unwrap(); + bar.save(&conn).await.unwrap(); + let bar2 = Bar::get(&conn, "tarzan".to_string()).await.unwrap(); - let foo2: &Foo = bar2.foo.load(&conn).unwrap(); + let foo2: &Foo = bar2.foo.load(&conn).await.unwrap(); assert_eq!(&foo, foo2); let foo3: &Foo = bar2.foo.get().unwrap(); @@ -230,53 +233,53 @@ fn foreign_key(conn: Connection) { } testall!(foreign_key); -fn auto_pk(conn: Connection) { +async fn auto_pk(conn: Connection) { let mut baz1 = Baz::new("baz1"); - baz1.save(&conn).unwrap(); + baz1.save(&conn).await.unwrap(); let mut baz2 = Baz::new("baz2"); - baz2.save(&conn).unwrap(); + baz2.save(&conn).await.unwrap(); let mut baz3 = Baz::new("baz3"); - baz3.save(&conn).unwrap(); + baz3.save(&conn).await.unwrap(); assert!(baz1.id < baz2.id); assert!(baz2.id < baz3.id); } testall!(auto_pk); -fn only_pk(conn: Connection) { +async fn only_pk(conn: Connection) { let mut obj = HasOnlyPk::new(1); - obj.save(&conn).unwrap(); + obj.save(&conn).await.unwrap(); // verify we can still save the object even though it has no // fields to modify - obj.save(&conn).unwrap(); + obj.save(&conn).await.unwrap(); } testall!(only_pk); -fn basic_committed_transaction(mut conn: Connection) { - let tr = conn.transaction().unwrap(); +async fn basic_committed_transaction(mut conn: Connection) { + let tr = conn.transaction().await.unwrap(); // Create an object with a transaction and commit it let mut foo = Foo::new(1); foo.bar = 42; - foo.save(&tr).unwrap(); - tr.commit().unwrap(); + foo.save(&tr).await.unwrap(); + tr.commit().await.unwrap(); // Find the object - let foo2 = Foo::get(&conn, 1).unwrap(); + let foo2 = Foo::get(&conn, 1).await.unwrap(); assert_eq!(foo, foo2); } testall!(basic_committed_transaction); -fn basic_dropped_transaction(mut conn: Connection) { +async fn basic_dropped_transaction(mut conn: Connection) { // Create an object with a transaction but never commit it { - let tr = conn.transaction().unwrap(); + let tr = conn.transaction().await.unwrap(); let mut foo = Foo::new(1); foo.bar = 42; - foo.save(&tr).unwrap(); + foo.save(&tr).await.unwrap(); } // Find the object - match Foo::get(&conn, 1) { + match Foo::get(&conn, 1).await { Ok(_) => panic!("object should not exist"), Err(butane::Error::NoSuchObject) => (), Err(e) => panic!("Unexpected error {e}"), @@ -284,17 +287,17 @@ fn basic_dropped_transaction(mut conn: Connection) { } testall!(basic_dropped_transaction); -fn basic_rollback_transaction(mut conn: Connection) { - let tr = conn.transaction().unwrap(); +async fn basic_rollback_transaction(mut conn: Connection) { + let tr = conn.transaction().await.unwrap(); // Create an object with a transaction but then roll back the transaction let mut foo = Foo::new(1); foo.bar = 42; - foo.save(&tr).unwrap(); - tr.rollback().unwrap(); + foo.save(&tr).await.unwrap(); + tr.rollback().await.unwrap(); // Find the object - match Foo::get(&conn, 1) { + match Foo::get(&conn, 1).await { Ok(_) => panic!("object should not exist"), Err(butane::Error::NoSuchObject) => (), Err(e) => panic!("Unexpected error {e}"), @@ -302,14 +305,14 @@ fn basic_rollback_transaction(mut conn: Connection) { } testall!(basic_rollback_transaction); -fn basic_unique_field_error_on_non_unique(conn: Connection) { +async fn basic_unique_field_error_on_non_unique(conn: Connection) { let mut foo1 = Foo::new(1); foo1.bar = 42; - foo1.save(&conn).unwrap(); + foo1.save(&conn).await.unwrap(); let mut foo2 = Foo::new(2); foo2.bar = foo1.bar; - let e = foo2.save(&conn).unwrap_err(); + let e = foo2.save(&conn).await.unwrap_err(); // Make sure the error is one we expect assert!(match e { #[cfg(feature = "sqlite")] @@ -326,22 +329,22 @@ fn basic_unique_field_error_on_non_unique(conn: Connection) { } testall!(basic_unique_field_error_on_non_unique); -fn fkey_same_type(conn: Connection) { +async fn fkey_same_type(conn: Connection) { let mut o1 = SelfReferential::new(1); let mut o2 = SelfReferential::new(2); - o2.save(&conn).unwrap(); + o2.save(&conn).await.unwrap(); o1.reference = Some(ForeignKey::from_pk(o2.id)); - o1.save(&conn).unwrap(); + o1.save(&conn).await.unwrap(); - let o1 = SelfReferential::get(&conn, 1).unwrap(); + let o1 = SelfReferential::get(&conn, 1).await.unwrap(); assert!(o1.reference.is_some()); - let inner: SelfReferential = o1.reference.unwrap().load(&conn).unwrap().clone(); + let inner: SelfReferential = o1.reference.unwrap().load(&conn).await.unwrap().clone(); assert_eq!(inner, o2); assert!(inner.reference.is_none()); } testall!(fkey_same_type); -fn basic_time(conn: Connection) { +async fn basic_time(conn: Connection) { let now = Utc::now(); let mut time = TimeHolder { id: 1, @@ -350,48 +353,52 @@ fn basic_time(conn: Connection) { utc2: now, state: ObjectState::default(), }; - time.save(&conn).unwrap(); + time.save(&conn).await.unwrap(); - let time2 = TimeHolder::get(&conn, 1).unwrap(); + let time2 = TimeHolder::get(&conn, 1).await.unwrap(); // Note, we don't just compare the objects directly because we // lose some precision when we go to the database. assert_eq!(time.naive.timestamp(), time2.naive.timestamp()); } testall!(basic_time); -fn basic_load_first(conn: Connection) { +async fn basic_load_first(conn: Connection) { //create let mut foo1 = Foo::new(1); foo1.bar = 42; foo1.baz = "hello world".to_string(); - foo1.save(&conn).unwrap(); + foo1.save(&conn).await.unwrap(); let mut foo2 = Foo::new(2); foo2.bar = 43; foo2.baz = "hello world".to_string(); - foo2.save(&conn).unwrap(); + foo2.save(&conn).await.unwrap(); // query finds first - let found = query!(Foo, baz.like("hello%")).load_first(&conn).unwrap(); + let found = query!(Foo, baz.like("hello%")) + .load_first(&conn) + .await + .unwrap(); assert_eq!(found, Some(foo1)); } testall!(basic_load_first); -fn basic_load_first_ordered(conn: Connection) { +async fn basic_load_first_ordered(conn: Connection) { //create let mut foo1 = Foo::new(1); foo1.bar = 42; foo1.baz = "hello world".to_string(); - foo1.save(&conn).unwrap(); + foo1.save(&conn).await.unwrap(); let mut foo2 = Foo::new(2); foo2.bar = 43; foo2.baz = "hello world".to_string(); - foo2.save(&conn).unwrap(); + foo2.save(&conn).await.unwrap(); // query finds first, ascending order let found_asc = query!(Foo, baz.like("hello%")) .order_asc(colname!(Foo, bar)) .load_first(&conn) + .await .unwrap(); assert_eq!(found_asc, Some(foo1)); @@ -400,6 +407,7 @@ fn basic_load_first_ordered(conn: Connection) { let found_desc = query!(Foo, baz.like("hello%")) .order_desc(colname!(Foo, bar)) .load_first(&conn) + .await .unwrap(); assert_eq!(found_desc, Some(foo2)); diff --git a/butane/tests/common/blog.rs b/butane/tests/common/blog.rs index 6d03c5f5..8749776e 100644 --- a/butane/tests/common/blog.rs +++ b/butane/tests/common/blog.rs @@ -76,9 +76,9 @@ impl Tag { } } -pub fn create_tag(conn: &Connection, name: &str) -> Tag { +pub async fn create_tag(conn: &Connection, name: &str) -> Tag { let mut tag = Tag::new(name); - tag.save(conn).unwrap(); + tag.save(conn).await.unwrap(); tag } @@ -86,14 +86,14 @@ pub fn create_tag(conn: &Connection, name: &str) -> Tag { /// 1. "Cats" /// 2. "Mountains" #[allow(dead_code)] // only used by some test files -pub fn setup_blog(conn: &Connection) { +pub async fn setup_blog(conn: &Connection) { let mut cats_blog = Blog::new(1, "Cats"); - cats_blog.save(conn).unwrap(); + cats_blog.save(conn).await.unwrap(); let mut mountains_blog = Blog::new(2, "Mountains"); - mountains_blog.save(conn).unwrap(); + mountains_blog.save(conn).await.unwrap(); - let tag_asia = create_tag(conn, "asia"); - let tag_danger = create_tag(conn, "danger"); + let tag_asia = create_tag(conn, "asia").await; + let tag_danger = create_tag(conn, "danger").await; let mut post = Post::new( 1, @@ -106,7 +106,7 @@ pub fn setup_blog(conn: &Connection) { post.likes = 4; post.tags.add(&tag_danger).unwrap(); post.tags.add(&tag_asia).unwrap(); - post.save(conn).unwrap(); + post.save(conn).await.unwrap(); let mut post = Post::new( 2, @@ -116,7 +116,7 @@ pub fn setup_blog(conn: &Connection) { ); post.published = true; post.likes = 20; - post.save(conn).unwrap(); + post.save(conn).await.unwrap(); let mut post = Post::new( 3, @@ -127,7 +127,7 @@ pub fn setup_blog(conn: &Connection) { post.published = true; post.likes = 10; post.tags.add(&tag_danger).unwrap(); - post.save(conn).unwrap(); + post.save(conn).await.unwrap(); let mut post = Post::new( 4, @@ -137,5 +137,5 @@ pub fn setup_blog(conn: &Connection) { ); post.published = false; post.tags.add(&tag_danger).unwrap(); - post.save(conn).unwrap(); + post.save(conn).await.unwrap(); } diff --git a/butane/tests/custom_enum_derived.rs b/butane/tests/custom_enum_derived.rs index e015a7e1..1ab0b7bf 100644 --- a/butane/tests/custom_enum_derived.rs +++ b/butane/tests/custom_enum_derived.rs @@ -29,27 +29,28 @@ impl HasCustomField2 { } } -fn roundtrip_custom_type(conn: Connection) { +async fn roundtrip_custom_type(conn: Connection) { //create let mut obj = HasCustomField2::new(1, Whatsit::Foo); - obj.save(&conn).unwrap(); + obj.save(&conn).await.unwrap(); // read - let obj2 = HasCustomField2::get(&conn, 1).unwrap(); + let obj2 = HasCustomField2::get(&conn, 1).await.unwrap(); assert_eq!(obj, obj2); } testall!(roundtrip_custom_type); -fn query_custom_type(conn: Connection) { +async fn query_custom_type(conn: Connection) { //create let mut obj_foo = HasCustomField2::new(1, Whatsit::Foo); - obj_foo.save(&conn).unwrap(); + obj_foo.save(&conn).await.unwrap(); let mut obj_bar = HasCustomField2::new(2, Whatsit::Bar); - obj_bar.save(&conn).unwrap(); + obj_bar.save(&conn).await.unwrap(); // query let results = query!(HasCustomField2, frob == { Whatsit::Bar }) .load(&conn) + .await .unwrap(); assert_eq!(results.len(), 1); assert_eq!(results[0], obj_bar) diff --git a/butane/tests/custom_pg.rs b/butane/tests/custom_pg.rs index 8a21fbb8..c2041cce 100644 --- a/butane/tests/custom_pg.rs +++ b/butane/tests/custom_pg.rs @@ -60,16 +60,16 @@ mod custom_pg { pt_to: Point, } - fn roundtrip_custom(conn: Connection) { + async fn roundtrip_custom(conn: Connection) { let mut trip = Trip { id: -1, pt_from: Point::new(0.0, 0.0), pt_to: Point::new(8.0, 9.0), state: ObjectState::default(), }; - trip.save(&conn).unwrap(); + trip.save(&conn).await.unwrap(); - let trip2 = Trip::get(&conn, trip.id).unwrap(); + let trip2 = Trip::get(&conn, trip.id).await.unwrap(); assert_eq!(trip, trip2); } maketest_pg!(roundtrip_custom, true); diff --git a/butane/tests/custom_type.rs b/butane/tests/custom_type.rs index 85fb0ab5..be8c0137 100644 --- a/butane/tests/custom_type.rs +++ b/butane/tests/custom_type.rs @@ -65,27 +65,28 @@ impl HasCustomField { } } -fn roundtrip_custom_type(conn: Connection) { +async fn roundtrip_custom_type(conn: Connection) { //create let mut obj = HasCustomField::new(1, Frobnozzle::Foo); - obj.save(&conn).unwrap(); + obj.save(&conn).await.unwrap(); // read - let obj2 = HasCustomField::get(&conn, 1).unwrap(); + let obj2 = HasCustomField::get(&conn, 1).await.unwrap(); assert_eq!(obj, obj2); } testall!(roundtrip_custom_type); -fn query_custom_type(conn: Connection) { +async fn query_custom_type(conn: Connection) { //create let mut obj_foo = HasCustomField::new(1, Frobnozzle::Foo); - obj_foo.save(&conn).unwrap(); + obj_foo.save(&conn).await.unwrap(); let mut obj_bar = HasCustomField::new(2, Frobnozzle::Bar); - obj_bar.save(&conn).unwrap(); + obj_bar.save(&conn).await.unwrap(); // query let results = query!(HasCustomField, frob == { Frobnozzle::Bar }) .load(&conn) + .await .unwrap(); assert_eq!(results.len(), 1); assert_eq!(results[0], obj_bar) diff --git a/butane/tests/fake.rs b/butane/tests/fake.rs index 9e816c85..9f2589cb 100644 --- a/butane/tests/fake.rs +++ b/butane/tests/fake.rs @@ -13,28 +13,28 @@ use common::blog::{Blog, Post, Tag}; use fake::{Fake, Faker}; #[cfg(feature = "fake")] -fn fake_blog_post(conn: Connection) { +async fn fake_blog_post(conn: Connection) { let mut fake_blog: Blog = Faker.fake(); - fake_blog.save(&conn).unwrap(); + fake_blog.save(&conn).await.unwrap(); let mut post: Post = Faker.fake(); post.blog = ForeignKey::::from(fake_blog); let mut tag_1: Tag = Faker.fake(); - tag_1.save(&conn).unwrap(); + tag_1.save(&conn).await.unwrap(); let mut tag_2: Tag = Faker.fake(); - tag_2.save(&conn).unwrap(); + tag_2.save(&conn).await.unwrap(); let mut tag_3: Tag = Faker.fake(); - tag_3.save(&conn).unwrap(); + tag_3.save(&conn).await.unwrap(); post.tags.add(&tag_1).unwrap(); post.tags.add(&tag_2).unwrap(); post.tags.add(&tag_3).unwrap(); - post.save(&conn).unwrap(); + post.save(&conn).await.unwrap(); let post_from_db = find!(Post, id == { post.id }, &conn).unwrap(); assert_eq!(post_from_db.title, post.title); - assert_eq!(post_from_db.tags.load(&conn).unwrap().count(), 3); + assert_eq!(post_from_db.tags.load(&conn).await.unwrap().count(), 3); } #[cfg(feature = "fake")] testall!(fake_blog_post); diff --git a/butane/tests/json.rs b/butane/tests/json.rs index 18968945..e1be7203 100644 --- a/butane/tests/json.rs +++ b/butane/tests/json.rs @@ -26,25 +26,25 @@ impl FooJJ { } } -fn json_null(conn: Connection) { +async fn json_null(conn: Connection) { //create let id = 4; let mut foo = FooJJ::new(id); - foo.save(&conn).unwrap(); + foo.save(&conn).await.unwrap(); // read - let mut foo2 = FooJJ::get(&conn, id).unwrap(); + let mut foo2 = FooJJ::get(&conn, id).await.unwrap(); assert_eq!(foo, foo2); // update foo2.bar = 43; - foo2.save(&conn).unwrap(); - let foo3 = FooJJ::get(&conn, id).unwrap(); + foo2.save(&conn).await.unwrap(); + let foo3 = FooJJ::get(&conn, id).await.unwrap(); assert_eq!(foo2, foo3); } testall!(json_null); -fn basic_json(conn: Connection) { +async fn basic_json(conn: Connection) { //create let id = 4; let mut foo = FooJJ::new(id); @@ -59,16 +59,16 @@ fn basic_json(conn: Connection) { }"#; foo.val = serde_json::from_str(data).unwrap(); - foo.save(&conn).unwrap(); + foo.save(&conn).await.unwrap(); // read - let mut foo2 = FooJJ::get(&conn, id).unwrap(); + let mut foo2 = FooJJ::get(&conn, id).await.unwrap(); assert_eq!(foo, foo2); // update foo2.bar = 43; - foo2.save(&conn).unwrap(); - let foo3 = FooJJ::get(&conn, id).unwrap(); + foo2.save(&conn).await.unwrap(); + let foo3 = FooJJ::get(&conn, id).await.unwrap(); assert_eq!(foo2, foo3); } testall!(basic_json); @@ -90,7 +90,7 @@ impl FooHH { } } } -fn basic_hashmap(conn: Connection) { +async fn basic_hashmap(conn: Connection) { //create let id = 4; let mut foo = FooHH::new(id); @@ -98,16 +98,16 @@ fn basic_hashmap(conn: Connection) { data.insert("a".to_string(), "1".to_string()); foo.val = data; - foo.save(&conn).unwrap(); + foo.save(&conn).await.unwrap(); // read - let mut foo2 = FooHH::get(&conn, id).unwrap(); + let mut foo2 = FooHH::get(&conn, id).await.unwrap(); assert_eq!(foo, foo2); // update foo2.bar = 43; - foo2.save(&conn).unwrap(); - let foo3 = FooHH::get(&conn, id).unwrap(); + foo2.save(&conn).await.unwrap(); + let foo3 = FooHH::get(&conn, id).await.unwrap(); assert_eq!(foo2, foo3); } testall!(basic_hashmap); @@ -135,7 +135,7 @@ impl FooHHO { } } } -fn hashmap_with_object_values(conn: Connection) { +async fn hashmap_with_object_values(conn: Connection) { //create let id = 4; let mut foo = FooHHO::new(id); @@ -143,16 +143,16 @@ fn hashmap_with_object_values(conn: Connection) { data.insert("a".to_string(), HashedObject { x: 1, y: 3 }); foo.val = data; - foo.save(&conn).unwrap(); + foo.save(&conn).await.unwrap(); // read - let mut foo2 = FooHHO::get(&conn, id).unwrap(); + let mut foo2 = FooHHO::get(&conn, id).await.unwrap(); assert_eq!(foo, foo2); // update foo2.bar = 43; - foo2.save(&conn).unwrap(); - let foo3 = FooHHO::get(&conn, id).unwrap(); + foo2.save(&conn).await.unwrap(); + let foo3 = FooHHO::get(&conn, id).await.unwrap(); assert_eq!(foo2, foo3); } testall!(hashmap_with_object_values); @@ -185,20 +185,20 @@ impl OuterFoo { } } -fn inline_json(conn: Connection) { +async fn inline_json(conn: Connection) { //create let id = 4; let mut foo = OuterFoo::new(id, InlineFoo::new(4, 8)); - foo.save(&conn).unwrap(); + foo.save(&conn).await.unwrap(); // read - let mut foo2 = OuterFoo::get(&conn, id).unwrap(); + let mut foo2 = OuterFoo::get(&conn, id).await.unwrap(); assert_eq!(foo, foo2); // update foo2.bar = InlineFoo::new(5, 9); - foo2.save(&conn).unwrap(); - let foo3 = OuterFoo::get(&conn, id).unwrap(); + foo2.save(&conn).await.unwrap(); + let foo3 = OuterFoo::get(&conn, id).await.unwrap(); assert_eq!(foo2, foo3); } testall!(inline_json); diff --git a/butane/tests/many.rs b/butane/tests/many.rs index 19168c48..2245bd6b 100644 --- a/butane/tests/many.rs +++ b/butane/tests/many.rs @@ -51,78 +51,78 @@ struct AutoItem { val: String, } -fn remove_one_from_many(conn: Connection) { +async fn remove_one_from_many(conn: Connection) { let mut cats_blog = Blog::new(1, "Cats"); - cats_blog.save(&conn).unwrap(); + cats_blog.save(&conn).await.unwrap(); let mut post = Post::new( 1, "The Cheetah", "This post is about a fast cat.", &cats_blog, ); - let tag_fast = create_tag(&conn, "fast"); - let tag_cat = create_tag(&conn, "cat"); - let tag_european = create_tag(&conn, "european"); + let tag_fast = create_tag(&conn, "fast").await; + let tag_cat = create_tag(&conn, "cat").await; + let tag_european = create_tag(&conn, "european").await; post.tags.add(&tag_fast).unwrap(); post.tags.add(&tag_cat).unwrap(); post.tags.add(&tag_european).unwrap(); - post.save(&conn).unwrap(); + post.save(&conn).await.unwrap(); // Wait a minute, Cheetahs aren't from Europe! post.tags.remove(&tag_european); - post.save(&conn).unwrap(); + post.save(&conn).await.unwrap(); - let post2 = Post::get(&conn, post.id).unwrap(); - assert_eq!(post2.tags.load(&conn).unwrap().count(), 2); + let post2 = Post::get(&conn, post.id).await.unwrap(); + assert_eq!(post2.tags.load(&conn).await.unwrap().count(), 2); } testall!(remove_one_from_many); -fn remove_multiple_from_many(conn: Connection) { +async fn remove_multiple_from_many(conn: Connection) { let mut cats_blog = Blog::new(1, "Cats"); - cats_blog.save(&conn).unwrap(); + cats_blog.save(&conn).await.unwrap(); let mut post = Post::new( 1, "The Cheetah", "This post is about a fast cat.", &cats_blog, ); - let tag_fast = create_tag(&conn, "fast"); - let tag_cat = create_tag(&conn, "cat"); - let tag_european = create_tag(&conn, "european"); - let tag_striped = create_tag(&conn, "striped"); + let tag_fast = create_tag(&conn, "fast").await; + let tag_cat = create_tag(&conn, "cat").await; + let tag_european = create_tag(&conn, "european").await; + let tag_striped = create_tag(&conn, "striped").await; post.tags.add(&tag_fast).unwrap(); post.tags.add(&tag_cat).unwrap(); post.tags.add(&tag_european).unwrap(); post.tags.add(&tag_striped).unwrap(); - post.save(&conn).unwrap(); + post.save(&conn).await.unwrap(); // Wait a minute, Cheetahs aren't from Europe and they don't have stripes! post.tags.remove(&tag_european); post.tags.remove(&tag_striped); - post.save(&conn).unwrap(); + post.save(&conn).await.unwrap(); - let post2 = Post::get(&conn, post.id).unwrap(); - assert_eq!(post2.tags.load(&conn).unwrap().count(), 2); + let post2 = Post::get(&conn, post.id).await.unwrap(); + assert_eq!(post2.tags.load(&conn).await.unwrap().count(), 2); } testall!(remove_multiple_from_many); -fn can_add_to_many_before_save(conn: Connection) { +async fn can_add_to_many_before_save(conn: Connection) { // Verify that for an object with an auto-pk, we can add items to a Many field before we actually // save the original object (and thus get the actual pk); let mut obj = AutoPkWithMany::new(); - obj.tags.add(&create_tag(&conn, "blue")).unwrap(); - obj.tags.add(&create_tag(&conn, "red")).unwrap(); - obj.save(&conn).unwrap(); + obj.tags.add(&create_tag(&conn, "blue").await).unwrap(); + obj.tags.add(&create_tag(&conn, "red").await).unwrap(); + obj.save(&conn).await.unwrap(); - let obj = AutoPkWithMany::get(&conn, obj.id).unwrap(); - let tags = obj.tags.load(&conn).unwrap(); + let obj = AutoPkWithMany::get(&conn, obj.id).await.unwrap(); + let tags = obj.tags.load(&conn).await.unwrap(); assert_eq!(tags.count(), 2); } testall!(can_add_to_many_before_save); -fn cant_add_unsaved_to_many(_conn: Connection) { +async fn cant_add_unsaved_to_many(_conn: Connection) { let unsaved_item = AutoItem { id: -1, val: "shiny".to_string(), @@ -135,14 +135,14 @@ fn cant_add_unsaved_to_many(_conn: Connection) { } testall!(cant_add_unsaved_to_many); -fn can_add_to_many_with_custom_table_name(conn: Connection) { +async fn can_add_to_many_with_custom_table_name(conn: Connection) { let mut obj = RenamedAutoPkWithMany::new(); - obj.tags.add(&create_tag(&conn, "blue")).unwrap(); - obj.tags.add(&create_tag(&conn, "red")).unwrap(); - obj.save(&conn).unwrap(); + obj.tags.add(&create_tag(&conn, "blue").await).unwrap(); + obj.tags.add(&create_tag(&conn, "red").await).unwrap(); + obj.save(&conn).await.unwrap(); - let obj = RenamedAutoPkWithMany::get(&conn, obj.id).unwrap(); - let tags = obj.tags.load(&conn).unwrap(); + let obj = RenamedAutoPkWithMany::get(&conn, obj.id).await.unwrap(); + let tags = obj.tags.load(&conn).await.unwrap(); assert_eq!(tags.count(), 2); } testall!(can_add_to_many_with_custom_table_name); diff --git a/butane/tests/migration-tests.rs b/butane/tests/migration-tests.rs index 53b26f23..db7867df 100644 --- a/butane/tests/migration-tests.rs +++ b/butane/tests/migration-tests.rs @@ -188,14 +188,15 @@ fn migration_add_field_sqlite() { } #[cfg(feature = "pg")] -#[test] -fn migration_add_field_pg() { - let (mut conn, _data) = pg_connection(); +#[tokio::test] +async fn migration_add_field_pg() { + let (mut conn, _data) = pg_connection().await; migration_add_field( &mut conn, "ALTER TABLE Foo ADD COLUMN baz BIGINT NOT NULL DEFAULT 0;", "ALTER TABLE Foo DROP COLUMN baz;", - ); + ) + .await; } #[cfg(feature = "sqlite")] @@ -210,14 +211,15 @@ fn migration_add_field_with_default_sqlite() { } #[cfg(feature = "pg")] -#[test] -fn migration_add_field_with_default_pg() { - let (mut conn, _data) = pg_connection(); +#[tokio::test] +async fn migration_add_field_with_default_pg() { + let (mut conn, _data) = pg_connection().await; migration_add_field_with_default( &mut conn, "ALTER TABLE Foo ADD COLUMN baz BIGINT NOT NULL DEFAULT 42;", "ALTER TABLE Foo DROP COLUMN baz;", - ); + ) + .await; } #[cfg(feature = "sqlite")] @@ -237,14 +239,15 @@ fn migration_add_and_remove_field_sqlite() { } #[cfg(feature = "pg")] -#[test] -fn migration_add_and_remove_field_pg() { - let (mut conn, _data) = pg_connection(); +#[tokio::test] +async fn migration_add_and_remove_field_pg() { + let (mut conn, _data) = pg_connection().await; migration_add_and_remove_field( &mut conn, "ALTER TABLE Foo ADD COLUMN baz BIGINT NOT NULL DEFAULT 0;ALTER TABLE Foo DROP COLUMN bar;", "ALTER TABLE Foo ADD COLUMN bar TEXT NOT NULL DEFAULT '';ALTER TABLE Foo DROP COLUMN baz;", - ); + ) + .await; } #[cfg(feature = "sqlite")] @@ -258,17 +261,18 @@ fn migration_delete_table_sqlite() { } #[cfg(feature = "pg")] -#[test] -fn migration_delete_table_pg() { - let (mut conn, _data) = pg_connection(); +#[tokio::test] +async fn migration_delete_table_pg() { + let (mut conn, _data) = pg_connection().await; migration_delete_table( &mut conn, "DROP TABLE Foo;", "CREATE TABLE Foo (id BIGINT NOT NULL PRIMARY KEY,bar TEXT NOT NULL);", - ); + ) + .await; } -fn test_migrate( +async fn test_migrate( conn: &mut Connection, init_tokens: TokenStream, v2_tokens: TokenStream, @@ -285,17 +289,17 @@ fn test_migrate( .create_migration(&backend, "v2", ms.latest().as_ref()) .unwrap()); - let mut to_apply = ms.unapplied_migrations(conn).unwrap(); + let mut to_apply = ms.unapplied_migrations(conn).await.unwrap(); assert_eq!(to_apply.len(), 2); for m in &to_apply { - m.apply(conn).unwrap(); + m.apply(conn).await.unwrap(); } verify_sql(conn, &ms, expected_up_sql, expected_down_sql); // Now downgrade, just to make sure we can to_apply.reverse(); for m in to_apply { - m.downgrade(conn).unwrap(); + m.downgrade(conn).await.unwrap(); } } @@ -315,7 +319,7 @@ fn verify_sql( assert_eq!(actual_down_sql, expected_down_sql); } -fn migration_add_field(conn: &mut Connection, up_sql: &str, down_sql: &str) { +async fn migration_add_field(conn: &mut Connection, up_sql: &str, down_sql: &str) { let init = quote! { struct Foo { id: i64, @@ -330,10 +334,10 @@ fn migration_add_field(conn: &mut Connection, up_sql: &str, down_sql: &str) { baz: u32, } }; - test_migrate(conn, init, v2, up_sql, down_sql); + test_migrate(conn, init, v2, up_sql, down_sql).await; } -fn migration_add_field_with_default(conn: &mut Connection, up_sql: &str, down_sql: &str) { +async fn migration_add_field_with_default(conn: &mut Connection, up_sql: &str, down_sql: &str) { let init = quote! { struct Foo { id: i64, @@ -349,10 +353,10 @@ fn migration_add_field_with_default(conn: &mut Connection, up_sql: &str, down_sq baz: u32, } }; - test_migrate(conn, init, v2, up_sql, down_sql); + test_migrate(conn, init, v2, up_sql, down_sql).await; } -fn migration_add_and_remove_field(conn: &mut Connection, up_sql: &str, down_sql: &str) { +async fn migration_add_and_remove_field(conn: &mut Connection, up_sql: &str, down_sql: &str) { let init = quote! { struct Foo { id: i64, @@ -366,10 +370,14 @@ fn migration_add_and_remove_field(conn: &mut Connection, up_sql: &str, down_sql: baz: u32, } }; - test_migrate(conn, init, v2, up_sql, down_sql); + test_migrate(conn, init, v2, up_sql, down_sql).await; } -fn migration_delete_table(conn: &mut Connection, expected_up_sql: &str, expected_down_sql: &str) { +async fn migration_delete_table( + conn: &mut Connection, + expected_up_sql: &str, + expected_down_sql: &str, +) { let init_tokens = quote! { struct Foo { id: i64, @@ -387,16 +395,16 @@ fn migration_delete_table(conn: &mut Connection, expected_up_sql: &str, expected .create_migration(&backend, "v2", ms.latest().as_ref()) .unwrap()); - let mut to_apply = ms.unapplied_migrations(conn).unwrap(); + let mut to_apply = ms.unapplied_migrations(conn).await.unwrap(); assert_eq!(to_apply.len(), 2); for m in &to_apply { - m.apply(conn).unwrap(); + m.apply(conn).await.unwrap(); } verify_sql(conn, &ms, expected_up_sql, expected_down_sql); // Now downgrade, just to make sure we can to_apply.reverse(); for m in to_apply { - m.downgrade(conn).unwrap(); + m.downgrade(conn).await.unwrap(); } } diff --git a/butane/tests/nullable.rs b/butane/tests/nullable.rs index 5884184b..24c1b6c4 100644 --- a/butane/tests/nullable.rs +++ b/butane/tests/nullable.rs @@ -21,39 +21,39 @@ impl WithNullable { } } -fn basic_optional(conn: Connection) { +async fn basic_optional(conn: Connection) { let mut with_none = WithNullable::new(1); - with_none.save(&conn).unwrap(); + with_none.save(&conn).await.unwrap(); let mut with_some = WithNullable::new(2); with_some.foo = Some(42); - with_some.save(&conn).unwrap(); + with_some.save(&conn).await.unwrap(); - let obj = WithNullable::get(&conn, 1).unwrap(); + let obj = WithNullable::get(&conn, 1).await.unwrap(); assert_eq!(obj.foo, None); - let obj = WithNullable::get(&conn, 2).unwrap(); + let obj = WithNullable::get(&conn, 2).await.unwrap(); assert_eq!(obj.foo, Some(42)); } testall!(basic_optional); -fn query_optional_with_some(conn: Connection) { +async fn query_optional_with_some(conn: Connection) { let mut obj = WithNullable::new(1); - obj.save(&conn).unwrap(); + obj.save(&conn).await.unwrap(); let mut obj = WithNullable::new(2); obj.foo = Some(42); - obj.save(&conn).unwrap(); + obj.save(&conn).await.unwrap(); let mut obj = WithNullable::new(3); obj.foo = Some(43); - obj.save(&conn).unwrap(); + obj.save(&conn).await.unwrap(); let mut obj = WithNullable::new(4); obj.foo = Some(44); - obj.save(&conn).unwrap(); + obj.save(&conn).await.unwrap(); - let mut objs = query!(WithNullable, foo > 42).load(&conn).unwrap(); + let mut objs = query!(WithNullable, foo > 42).load(&conn).await.unwrap(); objs.sort_by(|o1, o2| o1.foo.partial_cmp(&o2.foo).unwrap()); assert_eq!(objs.len(), 2); assert_eq!(objs[0].foo, Some(43)); @@ -61,15 +61,15 @@ fn query_optional_with_some(conn: Connection) { } testall!(query_optional_with_some); -fn query_optional_with_none(conn: Connection) { +async fn query_optional_with_none(conn: Connection) { let mut obj = WithNullable::new(1); - obj.save(&conn).unwrap(); + obj.save(&conn).await.unwrap(); let mut obj = WithNullable::new(2); obj.foo = Some(42); - obj.save(&conn).unwrap(); + obj.save(&conn).await.unwrap(); - let objs = query!(WithNullable, foo == None).load(&conn).unwrap(); + let objs = query!(WithNullable, foo == None).load(&conn).await.unwrap(); assert_eq!(objs.len(), 1); assert_eq!(objs[0].id, 1); } diff --git a/butane/tests/query.rs b/butane/tests/query.rs index f602f14a..17156c50 100644 --- a/butane/tests/query.rs +++ b/butane/tests/query.rs @@ -10,9 +10,9 @@ mod common; use common::blog; use common::blog::{Blog, Post, PostMetadata, Tag}; -fn equality(conn: Connection) { - blog::setup_blog(&conn); - let mut posts = query!(Post, published == true).load(&conn).unwrap(); +async fn equality(conn: Connection) { + blog::setup_blog(&conn).await; + let mut posts = query!(Post, published == true).load(&conn).await.unwrap(); assert_eq!(posts.len(), 3); posts.sort_by(|p1, p2| p1.id.partial_cmp(&p2.id).unwrap()); assert_eq!(posts[0].title, "The Tiger"); @@ -21,9 +21,12 @@ fn equality(conn: Connection) { } testall!(equality); -fn equality_separate_dataresult(conn: Connection) { - blog::setup_blog(&conn); - let mut posts = query!(PostMetadata, published == true).load(&conn).unwrap(); +async fn equality_separate_dataresult(conn: Connection) { + blog::setup_blog(&conn).await; + let mut posts = query!(PostMetadata, published == true) + .load(&conn) + .await + .unwrap(); assert_eq!(posts.len(), 3); posts.sort_by(|p1, p2| p1.id.partial_cmp(&p2.id).unwrap()); assert_eq!(posts[0].title, "The Tiger"); @@ -32,11 +35,12 @@ fn equality_separate_dataresult(conn: Connection) { } testall!(equality_separate_dataresult); -fn ordered(conn: Connection) { - blog::setup_blog(&conn); +async fn ordered(conn: Connection) { + blog::setup_blog(&conn).await; let posts = query!(Post, published == true) .order_asc(colname!(Post, title)) .load(&conn) + .await .unwrap(); assert_eq!(posts.len(), 3); assert_eq!(posts[0].title, "Mount Doom"); @@ -45,9 +49,9 @@ fn ordered(conn: Connection) { } testall!(ordered); -fn comparison(conn: Connection) { - blog::setup_blog(&conn); - let mut posts = query!(Post, likes < 5).load(&conn).unwrap(); +async fn comparison(conn: Connection) { + blog::setup_blog(&conn).await; + let mut posts = query!(Post, likes < 5).load(&conn).await.unwrap(); assert_eq!(posts.len(), 2); posts.sort_by(|p1, p2| p1.id.partial_cmp(&p2.id).unwrap()); assert_eq!(posts[0].title, "The Tiger"); @@ -55,9 +59,9 @@ fn comparison(conn: Connection) { } testall!(comparison); -fn like(conn: Connection) { - blog::setup_blog(&conn); - let mut posts = query!(Post, title.like("M%")).load(&conn).unwrap(); +async fn like(conn: Connection) { + blog::setup_blog(&conn).await; + let mut posts = query!(Post, title.like("M%")).load(&conn).await.unwrap(); assert_eq!(posts.len(), 2); posts.sort_by(|p1, p2| p1.id.partial_cmp(&p2.id).unwrap()); assert_eq!(posts[0].title, "Mount Doom"); @@ -65,18 +69,19 @@ fn like(conn: Connection) { } testall!(like); -fn combination(conn: Connection) { - blog::setup_blog(&conn); +async fn combination(conn: Connection) { + blog::setup_blog(&conn).await; let posts = query!(Post, published == true && likes < 5) .load(&conn) + .await .unwrap(); assert_eq!(posts.len(), 1); assert_eq!(posts[0].title, "The Tiger"); } testall!(combination); -fn combination_allof(conn: Connection) { - blog::setup_blog(&conn); +async fn combination_allof(conn: Connection) { + blog::setup_blog(&conn).await; let posts = Post::query() .filter(BoolExpr::AllOf(vec![ filter!(Post, published == true), @@ -84,23 +89,25 @@ fn combination_allof(conn: Connection) { filter!(Post, title == "The Tiger"), ])) .load(&conn) + .await .unwrap(); assert_eq!(posts.len(), 1); assert_eq!(posts[0].title, "The Tiger"); } testall!(combination_allof); -fn not_found(conn: Connection) { - blog::setup_blog(&conn); +async fn not_found(conn: Connection) { + blog::setup_blog(&conn).await; let posts = query!(Post, published == false && likes > 5) .load(&conn) + .await .unwrap(); assert_eq!(posts.len(), 0); } testall!(not_found); -fn rustval(conn: Connection) { - blog::setup_blog(&conn); +async fn rustval(conn: Connection) { + blog::setup_blog(&conn).await; // We don't need to escape into rust for this, but we can let post = find!(Post, title == { "The Tiger" }, &conn).unwrap(); assert_eq!(post.title, "The Tiger"); @@ -112,15 +119,16 @@ fn rustval(conn: Connection) { } testall!(rustval); -fn fkey_match(conn: Connection) { - blog::setup_blog(&conn); +async fn fkey_match(conn: Connection) { + blog::setup_blog(&conn).await; let blog: Blog = find!(Blog, name == "Cats", &conn).unwrap(); - let mut posts = query!(Post, blog == { &blog }).load(&conn).unwrap(); - let posts2 = query!(Post, blog == { blog }).load(&conn).unwrap(); + let mut posts = query!(Post, blog == { &blog }).load(&conn).await.unwrap(); + let posts2 = query!(Post, blog == { blog }).load(&conn).await.unwrap(); let blog_id = blog.id; - let posts3 = query!(Post, blog == { blog_id }).load(&conn).unwrap(); + let posts3 = query!(Post, blog == { blog_id }).load(&conn).await.unwrap(); let posts4 = query!(Post, blog.matches(name == "Cats")) .load(&conn) + .await .unwrap(); assert_eq!(posts.len(), 2); @@ -133,10 +141,10 @@ fn fkey_match(conn: Connection) { } testall!(fkey_match); -fn many_load(conn: Connection) { - blog::setup_blog(&conn); +async fn many_load(conn: Connection) { + blog::setup_blog(&conn).await; let post: Post = find!(Post, title == "The Tiger", &conn).unwrap(); - let tags = post.tags.load(&conn).unwrap(); + let tags = post.tags.load(&conn).await.unwrap(); let mut tags: Vec<&Tag> = tags.collect(); tags.sort_by(|t1, t2| t1.tag.partial_cmp(&t2.tag).unwrap()); assert_eq!(tags[0].tag, "asia"); @@ -144,12 +152,12 @@ fn many_load(conn: Connection) { } testall!(many_load); -fn many_serialize(conn: Connection) { - blog::setup_blog(&conn); +async fn many_serialize(conn: Connection) { + blog::setup_blog(&conn).await; let post: Post = find!(Post, title == "The Tiger", &conn).unwrap(); let tags_json: String = serde_json::to_string(&post.tags).unwrap(); let tags: Many = serde_json::from_str(&tags_json).unwrap(); - let tags = tags.load(&conn).unwrap(); + let tags = tags.load(&conn).await.unwrap(); let mut tags: Vec<&Tag> = tags.collect(); tags.sort_by(|t1, t2| t1.tag.partial_cmp(&t2.tag).unwrap()); assert_eq!(tags[0].tag, "asia"); @@ -157,9 +165,12 @@ fn many_serialize(conn: Connection) { } testall!(many_serialize); -fn many_objects_with_tag(conn: Connection) { - blog::setup_blog(&conn); - let mut posts = query!(Post, tags.contains("danger")).load(&conn).unwrap(); +async fn many_objects_with_tag(conn: Connection) { + blog::setup_blog(&conn).await; + let mut posts = query!(Post, tags.contains("danger")) + .load(&conn) + .await + .unwrap(); posts.sort_by(|p1, p2| p1.id.partial_cmp(&p2.id).unwrap()); assert_eq!(posts[0].title, "The Tiger"); assert_eq!(posts[1].title, "Mount Doom"); @@ -167,10 +178,11 @@ fn many_objects_with_tag(conn: Connection) { } testall!(many_objects_with_tag); -fn many_objects_with_tag_explicit(conn: Connection) { - blog::setup_blog(&conn); +async fn many_objects_with_tag_explicit(conn: Connection) { + blog::setup_blog(&conn).await; let mut posts = query!(Post, tags.contains(tag == "danger")) .load(&conn) + .await .unwrap(); posts.sort_by(|p1, p2| p1.id.partial_cmp(&p2.id).unwrap()); assert_eq!(posts[0].title, "The Tiger"); @@ -179,8 +191,8 @@ fn many_objects_with_tag_explicit(conn: Connection) { } testall!(many_objects_with_tag_explicit); -fn by_timestamp(conn: Connection) { - blog::setup_blog(&conn); +async fn by_timestamp(conn: Connection) { + blog::setup_blog(&conn).await; let mut post = find!(Post, title == "Sir Charles", &conn).unwrap(); // Pretend this post was published in 1970 post.pub_time = Some( @@ -189,7 +201,7 @@ fn by_timestamp(conn: Connection) { .unwrap() .naive_utc(), ); - post.save(&conn).unwrap(); + post.save(&conn).await.unwrap(); // And pretend another post was later in 1971 let mut post = find!(Post, title == "The Tiger", &conn).unwrap(); post.pub_time = Some( @@ -198,7 +210,7 @@ fn by_timestamp(conn: Connection) { .unwrap() .naive_utc(), ); - post.save(&conn).unwrap(); + post.save(&conn).await.unwrap(); // Now find all posts published before 1971. Assume we haven't gone // back in time to run these unit tests. @@ -213,18 +225,20 @@ fn by_timestamp(conn: Connection) { ) .order_desc(colname!(Post, pub_time)) .load(&conn) + .await .unwrap(); assert_eq!(posts[0].title, "The Tiger"); assert_eq!(posts[1].title, "Sir Charles"); } testall!(by_timestamp); -fn limit(conn: Connection) { - blog::setup_blog(&conn); +async fn limit(conn: Connection) { + blog::setup_blog(&conn).await; let posts = Post::query() .order_asc(colname!(Post, title)) .limit(2) .load(&conn) + .await .unwrap(); assert_eq!(posts.len(), 2); assert_eq!(posts[0].title, "Mount Doom"); @@ -232,13 +246,14 @@ fn limit(conn: Connection) { } testall!(limit); -fn offset(conn: Connection) { - blog::setup_blog(&conn); +async fn offset(conn: Connection) { + blog::setup_blog(&conn).await; // Now get the more posts after the two we got in the limit test above let posts = Post::query() .order_asc(colname!(Post, title)) .offset(2) .load(&conn) + .await .unwrap(); assert_eq!(posts.len(), 2); assert_eq!(posts[0].title, "Sir Charles"); diff --git a/butane/tests/r2d2.rs b/butane/tests/r2d2.rs index 9d0040d8..727bea7c 100644 --- a/butane/tests/r2d2.rs +++ b/butane/tests/r2d2.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "r2d2")] use butane_test_helper::*; #[cfg(feature = "r2d2")] diff --git a/butane/tests/uuid.rs b/butane/tests/uuid.rs index d2ac66a3..0d6aa0ca 100644 --- a/butane/tests/uuid.rs +++ b/butane/tests/uuid.rs @@ -21,21 +21,21 @@ impl FooUU { } } -fn basic_uuid(conn: Connection) { +async fn basic_uuid(conn: Connection) { //create let id = Uuid::new_v4(); let mut foo = FooUU::new(id); foo.bar = 42; - foo.save(&conn).unwrap(); + foo.save(&conn).await.unwrap(); // read - let mut foo2 = FooUU::get(&conn, id).unwrap(); + let mut foo2 = FooUU::get(&conn, id).await.unwrap(); assert_eq!(foo, foo2); // update foo2.bar = 43; - foo2.save(&conn).unwrap(); - let foo3 = FooUU::get(&conn, id).unwrap(); + foo2.save(&conn).await.unwrap(); + let foo3 = FooUU::get(&conn, id).await.unwrap(); assert_eq!(foo2, foo3); } testall!(basic_uuid); diff --git a/butane_cli/Cargo.toml b/butane_cli/Cargo.toml index d770d705..0acb0110 100644 --- a/butane_cli/Cargo.toml +++ b/butane_cli/Cargo.toml @@ -17,11 +17,13 @@ name = "butane" path = "src/main.rs" [features] -sqlite-bundled = ["butane/sqlite-bundled"] +# todo re-enable sqlite +#sqlite-bundled = ["butane/sqlite-bundled"] [dependencies] anyhow = "1.0" -butane = { features = ["default", "pg", "sqlite"], workspace = true } +# todo re-enable sqlite +butane = { features = ["default", "pg"], workspace = true } chrono = { workspace = true } clap = { version = "4.1" } quote = { workspace = true } diff --git a/butane_core/Cargo.toml b/butane_core/Cargo.toml index be8c03c6..0f88d11e 100644 --- a/butane_core/Cargo.toml +++ b/butane_core/Cargo.toml @@ -14,8 +14,9 @@ datetime = ["chrono", "tokio-postgres?/with-chrono-0_4"] debug = ["log"] fake = ["dep:fake", "rand"] json = ["tokio-postgres?/with-serde_json-1", "rusqlite?/serde_json"] -sqlite = ["rusqlite"] -sqlite-bundled = ["rusqlite/bundled"] +# todo re-enable sqlite +#sqlite = ["rusqlite"] +#sqlite-bundled = ["rusqlite/bundled"] tls = ["native-tls", "postgres-native-tls"] pg = ["tokio-postgres", "bytes"] @@ -40,7 +41,8 @@ tokio-postgres = { optional = true, workspace = true } postgres-native-tls = { version = "0.5", optional = true } proc-macro2 = { workspace = true } quote = { workspace = true } -r2d2 = { optional = true, workspace = true } +# todo re-enable r2d2 +#r2d2 = { optional = true, workspace = true } rand = { optional = true, workspace = true } regex = { version = "1.5", features = ["std"] } rusqlite = { workspace = true, optional = true } diff --git a/butane_core/src/db/mod.rs b/butane_core/src/db/mod.rs index 4f9a045c..52f35ce7 100644 --- a/butane_core/src/db/mod.rs +++ b/butane_core/src/db/mod.rs @@ -28,13 +28,15 @@ mod helper; mod macros; #[cfg(feature = "pg")] pub mod pg; +// TODO re-enable //#[cfg(feature = "sqlite")] //pub mod sqlite; -#[cfg(feature = "r2d2")] -pub mod r2; -#[cfg(feature = "r2d2")] -pub use r2::ConnectionManager; +// TODO re-enable +//#[cfg(feature = "r2d2")] +//pub mod r2; +//#[cfg(feature = "r2d2")] +//pub use r2::ConnectionManager; // Macros are always exported at the root of the crate use crate::connection_method_wrapper; diff --git a/butane_core/src/db/pg.rs b/butane_core/src/db/pg.rs index 747538f8..875ce54b 100644 --- a/butane_core/src/db/pg.rs +++ b/butane_core/src/db/pg.rs @@ -88,6 +88,7 @@ impl PgConnection { } let (client, conn) = postgres::connect(params, connector).await?; let conn_handle = tokio::spawn(async move { + #[allow(unused_variables)] // used only when logging is enabled if let Err(e) = conn.await { warn!("Postgres connection error {}", e); } diff --git a/butane_core/tests/connection.rs b/butane_core/tests/connection.rs index 518300e9..6866f633 100644 --- a/butane_core/tests/connection.rs +++ b/butane_core/tests/connection.rs @@ -2,7 +2,7 @@ use butane_core::db::{connect, BackendConnection, Connection, ConnectionSpec}; use butane_test_helper::*; -fn connection_not_closed(conn: Connection) { +async fn connection_not_closed(conn: Connection) { assert!(!conn.is_closed()); } testall_no_migrate!(connection_not_closed); @@ -22,13 +22,13 @@ fn persist_invalid_connection_backend() { assert_eq!(spec, loaded_spec); } -#[test] -fn invalid_pg_connection() { +#[tokio::test] +async fn invalid_pg_connection() { let spec = ConnectionSpec::new("pg", "does_not_parse"); assert_eq!(spec.backend_name, "pg".to_string()); assert_eq!(spec.conn_str, "does_not_parse".to_string()); - let result = connect(&spec); + let result = connect(&spec).await; assert!(matches!(result, Err(butane_core::Error::Postgres(_)))); match result { Err(butane_core::Error::Postgres(e)) => { @@ -40,8 +40,8 @@ fn invalid_pg_connection() { } } -#[test] -fn unreachable_pg_connection() { +#[tokio::test] +async fn unreachable_pg_connection() { let spec = ConnectionSpec::new("pg", "host=does_not_exist user=does_not_exist"); assert_eq!(spec.backend_name, "pg".to_string()); assert_eq!( @@ -49,7 +49,7 @@ fn unreachable_pg_connection() { "host=does_not_exist user=does_not_exist".to_string() ); - let result = connect(&spec); + let result = connect(&spec).await; assert!(matches!(result, Err(butane_core::Error::Postgres(_)))); match result { Err(butane_core::Error::Postgres(e)) => { @@ -61,7 +61,7 @@ fn unreachable_pg_connection() { } } -fn debug_connection(conn: Connection) { +async fn debug_connection(conn: Connection) { let backend_name = conn.backend_name().clone(); if backend_name == "pg" { diff --git a/butane_core/tests/transactions.rs b/butane_core/tests/transactions.rs index ff1ff179..6578c7e6 100644 --- a/butane_core/tests/transactions.rs +++ b/butane_core/tests/transactions.rs @@ -2,30 +2,30 @@ use butane_core::db::{BackendConnection, Connection}; use butane_test_helper::*; -fn commit_empty_transaction(mut conn: Connection) { +async fn commit_empty_transaction(mut conn: Connection) { assert!(!conn.is_closed()); - let tr = conn.transaction().unwrap(); + let tr = conn.transaction().await.unwrap(); - assert!(tr.commit().is_ok()); + assert!(tr.commit().await.is_ok()); // it is impossible to reuse the transaction after this. // i.e. already_consumed is unreachable. } testall_no_migrate!(commit_empty_transaction); -fn rollback_empty_transaction(mut conn: Connection) { - let tr = conn.transaction().unwrap(); +async fn rollback_empty_transaction(mut conn: Connection) { + let tr = conn.transaction().await.unwrap(); - assert!(tr.rollback().is_ok()); + assert!(tr.rollback().await.is_ok()); // it is impossible to reuse the transaction after this. // i.e. already_consumed is unreachable. } testall_no_migrate!(rollback_empty_transaction); -fn debug_transaction_before_consuming(mut conn: Connection) { +async fn debug_transaction_before_consuming(mut conn: Connection) { let backend_name = conn.backend_name().clone(); - let tr = conn.transaction().unwrap(); + let tr = conn.transaction().await.unwrap(); if backend_name == "pg" { assert!(format!("{:?}", tr).contains("{ trans: true }")); @@ -33,6 +33,6 @@ fn debug_transaction_before_consuming(mut conn: Connection) { assert!(format!("{:?}", tr).contains("path: Some(\"\")")); } - assert!(tr.commit().is_ok()); + assert!(tr.commit().await.is_ok()); } testall_no_migrate!(debug_transaction_before_consuming); diff --git a/butane_test_helper/Cargo.toml b/butane_test_helper/Cargo.toml index 5758c79f..62b72e9d 100644 --- a/butane_test_helper/Cargo.toml +++ b/butane_test_helper/Cargo.toml @@ -12,7 +12,8 @@ repository = "https://github.com/Electron100/butane" documentation = "https://docs.rs/butane/" [dependencies] -butane_core = { features = ["pg", "sqlite"], workspace = true } +# todo re-enable sqlite +butane_core = { features = ["pg"], workspace = true } libc = "0.2" once_cell = { workspace = true } tokio-postgres = { features = ["with-geo-types-0_7"], workspace = true } diff --git a/butane_test_helper/src/lib.rs b/butane_test_helper/src/lib.rs index 5dc21970..ee79152e 100644 --- a/butane_test_helper/src/lib.rs +++ b/butane_test_helper/src/lib.rs @@ -200,15 +200,15 @@ pub fn sqlite_teardown(_: ()) {} macro_rules! maketest { ($fname:ident, $backend:expr, $connstr:expr, $dataname:ident, $migrate:expr) => { paste::item! { - #[test] - pub fn [<$fname _ $backend>]() { + #[tokio::test] + pub async fn [<$fname _ $backend>]() { env_logger::try_init().ok(); let backend = butane_core::db::get_backend(&stringify!($backend)).expect("Could not find backend"); - let $dataname = butane_test_helper::[<$backend _setup>](); + let $dataname = butane_test_helper::[<$backend _setup>]().await; eprintln!("connecting to {}", &$connstr); - let mut conn = backend.connect(&$connstr).expect("Could not connect backend"); - butane_test_helper::setup_db(backend, &mut conn, $migrate); - $fname(conn); + let mut conn = backend.connect(&$connstr).await.expect("Could not connect backend"); + butane_test_helper::setup_db(backend, &mut conn, $migrate).await; + $fname(conn).await; butane_test_helper::[<$backend _teardown>]($dataname); } } @@ -231,11 +231,12 @@ macro_rules! maketest_pg { #[macro_export] macro_rules! testall { ($fname:ident) => { - cfg_if::cfg_if! { + // TODO re-enable + /*cfg_if::cfg_if! { if #[cfg(feature = "sqlite")] { maketest!($fname, sqlite, &format!(":memory:"), setup_data, true); } - } + }*/ cfg_if::cfg_if! { if #[cfg(feature = "pg")] { maketest_pg!($fname, true); @@ -247,11 +248,12 @@ macro_rules! testall { #[macro_export] macro_rules! testall_no_migrate { ($fname:ident) => { - cfg_if::cfg_if! { + // TODO re-enable + /*cfg_if::cfg_if! { if #[cfg(feature = "sqlite")] { maketest!($fname, sqlite, &format!(":memory:"), setup_data, false); } - } + }*/ cfg_if::cfg_if! { if #[cfg(feature = "pg")] { maketest_pg!($fname, false); diff --git a/example/Cargo.toml b/example/Cargo.toml index 8bb4198f..407429e9 100644 --- a/example/Cargo.toml +++ b/example/Cargo.toml @@ -7,7 +7,8 @@ publish = false build = "build.rs" [dependencies] -butane = { features = ["sqlite"], workspace = true } +# todo re-enable sqlite +butane = { features = ["pg"], workspace = true } tokio = { workspace = true, features = ["macros"] } [dev-dependencies] diff --git a/example/tests/test_cli.rs b/example/tests/test_cli.rs index 0e551645..3ef9c514 100644 --- a/example/tests/test_cli.rs +++ b/example/tests/test_cli.rs @@ -1,5 +1,7 @@ use assert_cmd::Command; +// todo either switch this to postgres or re-enable sqlite +/* #[test] fn test_migrate_and_query() { let db = "db.sqlite"; @@ -53,3 +55,4 @@ fn test_migrate_and_query() { .assert() .success(); } +*/ diff --git a/examples/getting_started/Cargo.toml b/examples/getting_started/Cargo.toml index 01115f11..09ffcc05 100644 --- a/examples/getting_started/Cargo.toml +++ b/examples/getting_started/Cargo.toml @@ -21,7 +21,8 @@ doc = false doc = false [dependencies] -butane = { features = ["default", "sqlite"], workspace = true } +# todo re-enable sqlite +butane = { features = ["default", "pg"], workspace = true } tokio = { workspace = true, features = ["macros"] } From 36f8574b53aa729357625639159737eb7699ef06 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Thu, 6 Jul 2023 10:38:09 -0400 Subject: [PATCH 21/78] pg: remove unused conn handle The previous comment here indicated we saved the handle to keep the task alive, but the docs for `JoinHandle` specifically say that a drop detaches the handle from the task. It does not kill the task. --- butane_core/src/db/pg.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/butane_core/src/db/pg.rs b/butane_core/src/db/pg.rs index 5f824f85..c07ff818 100644 --- a/butane_core/src/db/pg.rs +++ b/butane_core/src/db/pg.rs @@ -63,21 +63,18 @@ pub struct PgConnection { #[cfg(feature = "debug")] params: Box, client: postgres::Client, - // Save the handle to the task running the connection to keep it alive - conn_handle: tokio::task::JoinHandle<()>, } impl PgConnection { async fn open(params: &str) -> Result { - let (client, conn_handle) = Self::connect(params).await?; + let client = Self::connect(params).await?; Ok(Self { #[cfg(feature = "debug")] params: params.into(), client, - conn_handle, }) } - async fn connect(params: &str) -> Result<(postgres::Client, tokio::task::JoinHandle<()>)> { + async fn connect(params: &str) -> Result { cfg_if::cfg_if! { if #[cfg(feature = "tls")] { let connector = native_tls::TlsConnector::new()?; @@ -87,13 +84,13 @@ impl PgConnection { } } let (client, conn) = postgres::connect(params, connector).await?; - let conn_handle = tokio::spawn(async move { + tokio::spawn(async move { #[allow(unused_variables)] // used only when logging is enabled if let Err(e) = conn.await { warn!("Postgres connection error {}", e); } }); - Ok((client, conn_handle)) + Ok(client) } } impl PgConnectionLike for PgConnection { From b4ea4a0e5c6b7e391a55b6a9d4e82b94e4ad588e Mon Sep 17 00:00:00 2001 From: James Oakley Date: Fri, 7 Jul 2023 21:37:43 -0400 Subject: [PATCH 22/78] Introduce maybe-async-cfg Just ConnectionMethods trait for now. More to come. --- Cargo.lock | 69 ++++++++++++++++ Cargo.toml | 1 + butane_core/Cargo.toml | 1 + butane_core/src/db/connmethods.rs | 131 ++++++++++++++---------------- butane_core/src/db/mod.rs | 6 +- butane_core/src/fkey.rs | 10 ++- butane_core/src/many.rs | 10 ++- 7 files changed, 150 insertions(+), 78 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 42dcb4bf..2e9e49d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -320,6 +320,7 @@ dependencies = [ "futures-util", "hex", "log", + "maybe-async-cfg", "native-tls", "once_cell", "paste", @@ -1015,6 +1016,19 @@ version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +[[package]] +name = "maybe-async-cfg" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21fb8fac02158b9b529eb692491895d8459dbac939f3bc6e32159969646ffe55" +dependencies = [ + "proc-macro-error", + "proc-macro2 1.0.63", + "pulldown-cmark", + "quote 1.0.29", + "syn 1.0.109", +] + [[package]] name = "md-5" version = "0.10.5" @@ -1308,6 +1322,30 @@ dependencies = [ "termtree", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2 1.0.63", + "quote 1.0.29", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2 1.0.63", + "quote 1.0.29", + "version_check", +] + [[package]] name = "proc-macro2" version = "0.4.30" @@ -1326,6 +1364,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pulldown-cmark" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a1a2f1f0a7ecff9c31abbe177637be0e97a0aef46cf8738ece09327985d998" +dependencies = [ + "bitflags 1.3.2", + "memchr", + "unicase", +] + [[package]] name = "quote" version = "0.6.13" @@ -1707,6 +1756,17 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2 1.0.63", + "quote 1.0.29", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.23" @@ -1918,6 +1978,15 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.13" diff --git a/Cargo.toml b/Cargo.toml index bc90e41b..f60d4bd3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ chrono = { version = "0.4", default-features = false, features = [ env_logger = "0.10" fake = "2.6" log = "0.4" +maybe-async-cfg = { version = "0.2" } once_cell = "1.5.2" paste = "1.0.11" proc-macro2 = { version = "1.0", default-features = false } diff --git a/butane_core/Cargo.toml b/butane_core/Cargo.toml index 932802c5..39810124 100644 --- a/butane_core/Cargo.toml +++ b/butane_core/Cargo.toml @@ -34,6 +34,7 @@ fs2 = "0.4" # for file locks futures-util = "0.3" hex = "0.4" log = { optional = true, workspace = true } +maybe-async-cfg = { workspace = true } native-tls = { version = "0.2", optional = true } once_cell = { workspace = true } pin-project = "1" diff --git a/butane_core/src/db/connmethods.rs b/butane_core/src/db/connmethods.rs index 9c2c01ba..d6190a8f 100644 --- a/butane_core/src/db/connmethods.rs +++ b/butane_core/src/db/connmethods.rs @@ -7,74 +7,74 @@ use async_trait::async_trait; use std::ops::{Deref, DerefMut}; use std::vec::Vec; -macro_rules! connection_methods_trait { - (async) => { - connection_methods_trait!(async #[async_trait]); - }; - (sync) => { - connection_methods_trait!(); - }; - ($($async:tt)? $(#[$attr:tt])?) => { - /// Methods available on a database connection. Most users do not need - /// to call these methods directly and will instead use methods on - /// [DataObject][crate::DataObject] or the `query!` macro. This trait is - /// implemented by both database connections and transactions. - $(#[$attr])? - pub trait ConnectionMethods: Sync { - $($async)? fn execute(&self, sql: &str) -> Result<()>; - $($async)? fn query<'a, 'b, 'c: 'a>( - &'c self, - table: &str, - columns: &'b [Column], - expr: Option, - limit: Option, - offset: Option, - sort: Option<&[Order]>, - ) -> Result>; - $($async)? fn insert_returning_pk( - &self, - table: &str, - columns: &[Column], - pkcol: &Column, - values: &[SqlValRef<'_>], - ) -> Result; - /// Like `insert_returning_pk` but with no return value - $($async)? fn insert_only( - &self, - table: &str, - columns: &[Column], - values: &[SqlValRef<'_>], - ) -> Result<()>; - /// Insert unless there's a conflict on the primary key column, in which case update - $($async)? fn insert_or_replace( - &self, - table: &str, - columns: &[Column], - pkcol: &Column, - values: &[SqlValRef<'_>], - ) -> Result<()>; - $($async)? fn update<'a>( - &self, - table: &str, - pkcol: Column, - pk: SqlValRef<'a>, - columns: &[Column], - values: &[SqlValRef<'_>], - ) -> Result<()>; - $($async)? fn delete(&self, table: &str, pkcol: &'static str, pk: SqlVal) -> Result<()>; - // self.delete_where(table, BoolExpr::Eq(pkcol, Expr::Val(pk))).await?; - $($async)? fn delete_where(&self, table: &str, expr: BoolExpr) -> Result; - /// Tests if a table exists in the database. - $($async)? fn has_table(&self, table: &str) -> Result; - } - }; +mod internal { + use super::*; + + /// Methods available on a database connection. Most users do not need + /// to call these methods directly and will instead use methods on + /// [DataObject][crate::DataObject] or the `query!` macro. This trait is + /// implemented by both database connections and transactions. + #[maybe_async_cfg::maybe(sync(), async())] + #[async_trait] + pub trait ConnectionMethods: Sync { + async fn execute(&self, sql: &str) -> Result<()>; + async fn query<'a, 'b, 'c: 'a>( + &'c self, + table: &str, + columns: &'b [Column], + expr: Option, + limit: Option, + offset: Option, + sort: Option<&[Order]>, + ) -> Result>; + async fn insert_returning_pk( + &self, + table: &str, + columns: &[Column], + pkcol: &Column, + values: &[SqlValRef<'_>], + ) -> Result; + /// Like `insert_returning_pk` but with no return value + async fn insert_only( + &self, + table: &str, + columns: &[Column], + values: &[SqlValRef<'_>], + ) -> Result<()>; + /// Insert unless there's a conflict on the primary key column, in which case update + async fn insert_or_replace( + &self, + table: &str, + columns: &[Column], + pkcol: &Column, + values: &[SqlValRef<'_>], + ) -> Result<()>; + async fn update<'a>( + &self, + table: &str, + pkcol: Column, + pk: SqlValRef<'a>, + columns: &[Column], + values: &[SqlValRef<'_>], + ) -> Result<()>; + async fn delete(&self, table: &str, pkcol: &'static str, pk: SqlVal) -> Result<()>; + // self.delete_where(table, BoolExpr::Eq(pkcol, Expr::Val(pk))).await?; + async fn delete_where(&self, table: &str, expr: BoolExpr) -> Result; + /// Tests if a table exists in the database. + async fn has_table(&self, table: &str) -> Result; + } } -connection_methods_trait!(async); +pub use internal::ConnectionMethodsAsync as ConnectionMethods; + +pub trait ConnectionMethodWrapper { + type Wrapped: ConnectionMethods; + fn wrapped_connection_methods(&self) -> Result<&Self::Wrapped>; +} mod sync { use super::*; - connection_methods_trait!(sync); + pub use internal::ConnectionMethodsSync as ConnectionMethods; } /// Represents a database column. Most users do not need to use this @@ -152,11 +152,6 @@ where pub type RawQueryResult<'a> = Box; pub type QueryResult = Vec; -pub trait ConnectionMethodWrapper { - type Wrapped: ConnectionMethods; - fn wrapped_connection_methods(&self) -> Result<&Self::Wrapped>; -} - #[derive(Debug)] pub(crate) struct VecRows { rows: Vec, diff --git a/butane_core/src/db/mod.rs b/butane_core/src/db/mod.rs index 1a381b19..f7d07ea1 100644 --- a/butane_core/src/db/mod.rs +++ b/butane_core/src/db/mod.rs @@ -41,9 +41,9 @@ pub mod pg; // Macros are always exported at the root of the crate use crate::connection_method_wrapper; -pub use connmethods::{ - BackendRow, BackendRows, Column, ConnectionMethods, MapDeref, QueryResult, RawQueryResult, -}; +pub use connmethods::{BackendRow, BackendRows, Column, MapDeref, QueryResult, RawQueryResult}; + +pub use connmethods::ConnectionMethods; /// Database connection. #[async_trait] diff --git a/butane_core/src/fkey.rs b/butane_core/src/fkey.rs index 2006df3e..6e860507 100644 --- a/butane_core/src/fkey.rs +++ b/butane_core/src/fkey.rs @@ -1,9 +1,8 @@ -use crate::db::ConnectionMethods; use crate::*; -use tokio::sync::OnceCell; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::borrow::Cow; use std::fmt::{Debug, Formatter}; +use tokio::sync::OnceCell; #[cfg(feature = "fake")] use fake::{Dummy, Faker}; @@ -76,14 +75,17 @@ impl ForeignKey { } } +//todo support sync load with ForeignKey too impl ForeignKey { /// Loads the value referred to by this foreign key from the /// database if necessary and returns a reference to it. - pub async fn load(&self, conn: &impl ConnectionMethods) -> Result<&T> { + pub async fn load(&self, conn: &impl crate::ConnectionMethods) -> Result<&T> { self.val .get_or_try_init(|| async { let pk = self.valpk.get().unwrap(); - T::get(conn, &T::PKType::from_sql_ref(pk.as_ref())?).await.map(Box::new) + T::get(conn, &T::PKType::from_sql_ref(pk.as_ref())?) + .await + .map(Box::new) }) .await .map(|v| v.as_ref()) diff --git a/butane_core/src/many.rs b/butane_core/src/many.rs index 0d200882..b455f93a 100644 --- a/butane_core/src/many.rs +++ b/butane_core/src/many.rs @@ -1,4 +1,4 @@ -use crate::db::{Column, ConnectionMethods}; +use crate::db::Column; use crate::query::{BoolExpr, Expr}; use crate::{DataObject, Error, FieldType, Result, SqlType, SqlVal, ToSql}; use serde::{Deserialize, Serialize}; @@ -101,8 +101,9 @@ where .map(|v| v.iter()) } + // todo support save and load for sync too /// Used by macro-generated code. You do not need to call this directly. - pub async fn save(&mut self, conn: &impl ConnectionMethods) -> Result<()> { + pub async fn save(&mut self, conn: &impl crate::ConnectionMethods) -> Result<()> { let owner = self.owner.as_ref().ok_or(Error::NotInitialized)?; while !self.new_values.is_empty() { conn.insert_only( @@ -128,7 +129,10 @@ where /// Loads the values referred to by this foreign key from the /// database if necessary and returns a reference to them. - pub async fn load(&self, conn: &impl ConnectionMethods) -> Result> { + pub async fn load( + &self, + conn: &impl crate::ConnectionMethods, + ) -> Result> { let vals: Result<&Vec> = self .all_values .get_or_try_init(|| async { From 357da6c75b8ef2e614b5a186c4696542ae31acea Mon Sep 17 00:00:00 2001 From: James Oakley Date: Tue, 11 Jul 2023 10:59:37 -0400 Subject: [PATCH 23/78] Add sync option for mod db --- butane_core/src/db/connmethods.rs | 2 +- butane_core/src/db/macros.rs | 5 + butane_core/src/db/mod.rs | 243 +++++++++++++++++++----------- butane_core/src/lib.rs | 3 + 4 files changed, 167 insertions(+), 86 deletions(-) diff --git a/butane_core/src/db/connmethods.rs b/butane_core/src/db/connmethods.rs index d6190a8f..bdc887de 100644 --- a/butane_core/src/db/connmethods.rs +++ b/butane_core/src/db/connmethods.rs @@ -72,7 +72,7 @@ pub trait ConnectionMethodWrapper { fn wrapped_connection_methods(&self) -> Result<&Self::Wrapped>; } -mod sync { +pub mod sync { use super::*; pub use internal::ConnectionMethodsSync as ConnectionMethods; } diff --git a/butane_core/src/db/macros.rs b/butane_core/src/db/macros.rs index 1ee61374..a688a154 100644 --- a/butane_core/src/db/macros.rs +++ b/butane_core/src/db/macros.rs @@ -1,6 +1,11 @@ #[macro_export] macro_rules! connection_method_wrapper { ($ty:path) => { + #[maybe_async_cfg::maybe( + idents(ConnectionMethods(sync = "ConnectionMethodsSync", async)), + sync(), + async() + )] #[async_trait::async_trait] impl ConnectionMethods for $ty { async fn execute(&self, sql: &str) -> Result<()> { diff --git a/butane_core/src/db/mod.rs b/butane_core/src/db/mod.rs index f7d07ea1..b6b647ea 100644 --- a/butane_core/src/db/mod.rs +++ b/butane_core/src/db/mod.rs @@ -45,52 +45,172 @@ pub use connmethods::{BackendRow, BackendRows, Column, MapDeref, QueryResult, Ra pub use connmethods::ConnectionMethods; -/// Database connection. -#[async_trait] -pub trait BackendConnection: ConnectionMethods + Debug + Send + 'static { - /// Begin a database transaction. The transaction object must be - /// used in place of this connection until it is committed and aborted. - async fn transaction(&mut self) -> Result; - /// Retrieve the backend backend this connection - fn backend(&self) -> Box; - fn backend_name(&self) -> &'static str; - /// Tests if the connection has been closed. Backends which do not - /// support this check should return false. - fn is_closed(&self) -> bool; -} +mod internal { + use super::*; + use connmethods::sync::ConnectionMethods as ConnectionMethodsSync; -/// Database connection. May be a connection to any type of database -/// as it is a boxed abstraction over a specific connection. -#[derive(Debug)] -pub struct Connection { - conn: Box, -} -impl Connection { - pub async fn execute(&mut self, sql: impl AsRef) -> Result<()> { - self.conn.execute(sql.as_ref()).await + /// Database connection. + #[maybe_async_cfg::maybe( + idents(ConnectionMethods(sync = "ConnectionMethodsSync", async)), + sync(), + async() + )] + #[async_trait] + pub trait BackendConnection: ConnectionMethods + Debug + Send + 'static { + /// Begin a database transaction. The transaction object must be + /// used in place of this connection until it is committed and aborted. + async fn transaction(&mut self) -> Result; + /// Retrieve the backend backend this connection + fn backend(&self) -> Box; + fn backend_name(&self) -> &'static str; + /// Tests if the connection has been closed. Backends which do not + /// support this check should return false. + fn is_closed(&self) -> bool; } - // For use with connection_method_wrapper macro - #[allow(clippy::unnecessary_wraps)] - fn wrapped_connection_methods(&self) -> Result<&dyn BackendConnection> { - Ok(self.conn.as_ref()) + + /// Database connection. May be a connection to any type of database + /// as it is a boxed abstraction over a specific connection. + #[maybe_async_cfg::maybe( + idents(BackendConnection( + sync = "BackendConnectionSync", + async = "BackendConnectionAsync" + )), + sync(), + async() + )] + #[derive(Debug)] + pub struct Connection { + pub(super) conn: Box, } -} -#[async_trait] -impl BackendConnection for Connection { - async fn transaction(&mut self) -> Result { - self.conn.transaction().await + + #[maybe_async_cfg::maybe( + idents(BackendConnection( + sync = "BackendConnectionSync", + async = "BackendConnectionAsync" + )), + sync(), + async() + )] + impl Connection { + pub async fn execute(&mut self, sql: impl AsRef) -> Result<()> { + self.conn.execute(sql.as_ref()).await + } + // For use with connection_method_wrapper macro + #[allow(clippy::unnecessary_wraps)] + fn wrapped_connection_methods(&self) -> Result<&dyn BackendConnection> { + Ok(self.conn.as_ref()) + } } - fn backend(&self) -> Box { - self.conn.backend() + + #[maybe_async_cfg::maybe( + idents( + BackendConnection(sync = "BackendConnectionSync", async = "BackendConnectionAsync"), + Connection(sync = "ConnectionSync", async = "ConnectionAsync") + ), + sync(), + async() + )] + #[async_trait] + impl BackendConnection for Connection { + async fn transaction(&mut self) -> Result { + self.conn.transaction().await + } + fn backend(&self) -> Box { + self.conn.backend() + } + fn backend_name(&self) -> &'static str { + self.conn.backend_name() + } + fn is_closed(&self) -> bool { + self.conn.is_closed() + } } - fn backend_name(&self) -> &'static str { - self.conn.backend_name() + connection_method_wrapper!(Connection); + + #[maybe_async_cfg::maybe( + idents(ConnectionMethods(sync = "ConnectionMethodsSync", async)), + sync(), + async() + )] + #[async_trait] + pub trait BackendTransaction<'c>: ConnectionMethods + Debug + Send { + /// Commit the transaction Unfortunately because we use this as a + /// trait object, we can't consume self. It should be understood + /// that no methods should be called after commit. This trait is + /// not public, and that behavior is enforced by Transaction + async fn commit(&mut self) -> Result<()>; + /// Roll back the transaction. Same comment about consuming self as above. + async fn rollback(&mut self) -> Result<()>; + + // Workaround for https://github.com/rust-lang/rfcs/issues/2765 + fn connection_methods(&self) -> &dyn ConnectionMethods; + fn connection_methods_mut(&mut self) -> &mut dyn ConnectionMethods; + } + + /// Database transaction. + /// + /// Begin a transaction using the `BackendConnection` + /// [`transaction`][crate::db::BackendConnection::transaction] method. + #[maybe_async_cfg::maybe( + idents(BackendTransaction( + sync = "BackendTransactionSync", + async = "BackendTransactionAsync" + )), + sync(), + async() + )] + #[derive(Debug)] + pub struct Transaction<'c> { + trans: Box + 'c>, } - fn is_closed(&self) -> bool { - self.conn.is_closed() + + #[maybe_async_cfg::maybe( + idents( + BackendTransaction(sync = "BackendTransactionSync", async = "BackendTransactionAsync"), + ConnectionMethods(sync = "ConnectionMethodsSync", async) + ), + sync(), + async() + )] + impl<'c> Transaction<'c> { + // unused may occur if no backends are selected + #[allow(unused)] + pub(super) fn new(trans: Box + 'c>) -> Self { + Transaction { trans } + } + /// Commit the transaction + pub async fn commit(mut self) -> Result<()> { + self.trans.commit().await + } + /// Roll back the transaction. Equivalent to dropping it. + pub async fn rollback(mut self) -> Result<()> { + self.trans.deref_mut().rollback().await + } + // For use with connection_method_wrapper macro + #[allow(clippy::unnecessary_wraps)] + fn wrapped_connection_methods(&self) -> Result<&dyn ConnectionMethods> { + let a: &dyn BackendTransaction<'c> = self.trans.as_ref(); + Ok(a.connection_methods()) + } } + + connection_method_wrapper!(Transaction<'_>); +} + +pub use internal::BackendConnectionAsync as BackendConnection; +use internal::BackendTransactionAsync as BackendTransaction; +pub use internal::ConnectionAsync as Connection; +pub use internal::TransactionAsync as Transaction; + +pub mod sync { + //! Synchronous (non-async versions of traits) + + pub use super::connmethods::sync::ConnectionMethods; + pub use super::internal::BackendConnectionSync as BackendConnection; + use super::internal::BackendTransactionSync as BackendTransaction; + pub use super::internal::ConnectionSync as Connection; + pub use super::internal::TransactionSync as Transaction; } -connection_method_wrapper!(Connection); /// Connection specification. Contains the name of a database backend /// and the backend-specific connection string. See [connect][crate::db::connect] @@ -173,50 +293,3 @@ pub async fn connect(spec: &ConnectionSpec) -> Result { .connect(&spec.conn_str) .await } - -#[async_trait] -trait BackendTransaction<'c>: ConnectionMethods + Debug + Send { - /// Commit the transaction Unfortunately because we use this as a - /// trait object, we can't consume self. It should be understood - /// that no methods should be called after commit. This trait is - /// not public, and that behavior is enforced by Transaction - async fn commit(&mut self) -> Result<()>; - /// Roll back the transaction. Same comment about consuming self as above. - async fn rollback(&mut self) -> Result<()>; - - // Workaround for https://github.com/rust-lang/rfcs/issues/2765 - fn connection_methods(&self) -> &dyn ConnectionMethods; - fn connection_methods_mut(&mut self) -> &mut dyn ConnectionMethods; -} - -/// Database transaction. -/// -/// Begin a transaction using the `BackendConnection` -/// [`transaction`][crate::db::BackendConnection::transaction] method. -#[derive(Debug)] -pub struct Transaction<'c> { - trans: Box + 'c>, -} -impl<'c> Transaction<'c> { - // unused may occur if no backends are selected - #[allow(unused)] - fn new(trans: Box + 'c>) -> Self { - Transaction { trans } - } - /// Commit the transaction - pub async fn commit(mut self) -> Result<()> { - self.trans.commit().await - } - /// Roll back the transaction. Equivalent to dropping it. - pub async fn rollback(mut self) -> Result<()> { - self.trans.deref_mut().rollback().await - } - // For use with connection_method_wrapper macro - #[allow(clippy::unnecessary_wraps)] - fn wrapped_connection_methods(&self) -> Result<&dyn ConnectionMethods> { - let a: &dyn BackendTransaction<'c> = self.trans.as_ref(); - Ok(a.connection_methods()) - } -} - -connection_method_wrapper!(Transaction<'_>); diff --git a/butane_core/src/lib.rs b/butane_core/src/lib.rs index d726e867..c603ba63 100644 --- a/butane_core/src/lib.rs +++ b/butane_core/src/lib.rs @@ -23,6 +23,9 @@ pub mod uuid; use fake::{Dummy, Faker}; use db::{BackendRow, Column, ConnectionMethods}; +mod sync { + pub use crate::db::sync::ConnectionMethods; +} use custom::SqlTypeCustom; pub use query::Query; From d7305c39c1a4de77ea830b69c4bed2ebf35bbf14 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Fri, 28 Jul 2023 11:07:32 -0400 Subject: [PATCH 24/78] Sync-to-Async Adapter mostly working Sqlite compilation re-enabled but sqlite not enabled in tests yet Still need to: - A bunch of TODOs - Sqlite tests - Sync tests - remove ?Send bound from async-trait. This may be tricky, but I believe it's required for the tokio rt executor. - Clean up sync naming - Clippy lints --- Cargo.lock | 39 ++ Cargo.toml | 1 + butane/Cargo.toml | 5 +- butane/tests/migration-tests.rs | 35 +- butane_cli/Cargo.toml | 1 + butane_core/Cargo.toml | 11 +- butane_core/src/codegen/dbobj.rs | 2 +- butane_core/src/db/adapter.rs | 426 ++++++++++++++++++ butane_core/src/db/connmethods.rs | 77 +++- butane_core/src/db/macros.rs | 12 +- butane_core/src/db/mod.rs | 157 +++++-- butane_core/src/db/pg.rs | 18 +- butane_core/src/db/sqlite.rs | 469 ++++++++++---------- butane_core/src/lib.rs | 20 +- butane_core/src/migrations/fsmigrations.rs | 5 +- butane_core/src/migrations/memmigrations.rs | 5 +- butane_core/src/migrations/migration.rs | 8 +- butane_core/src/migrations/mod.rs | 6 +- butane_test_helper/Cargo.toml | 2 +- butane_test_helper/src/lib.rs | 10 +- 20 files changed, 960 insertions(+), 349 deletions(-) create mode 100644 butane_core/src/db/adapter.rs diff --git a/Cargo.lock b/Cargo.lock index 2e9e49d1..d6301eea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -312,6 +312,8 @@ dependencies = [ "bytes", "cfg-if", "chrono", + "crossbeam-channel", + "dyn-clone", "env_logger", "fake", "fallible-iterator", @@ -448,6 +450,25 @@ dependencies = [ "libc", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -563,6 +584,12 @@ dependencies = [ "syn 2.0.23", ] +[[package]] +name = "dyn-clone" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "304e6508efa593091e97a9abbc10f90aa7ca635b6d2784feff3c89d41dd12272" + [[package]] name = "either" version = "1.8.1" @@ -984,6 +1011,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" dependencies = [ + "cc", "pkg-config", "vcpkg", ] @@ -1092,6 +1120,16 @@ dependencies = [ "libm", ] +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "object" version = "0.31.1" @@ -1863,6 +1901,7 @@ dependencies = [ "bytes", "libc", "mio", + "num_cpus", "pin-project-lite", "socket2 0.4.9", "tokio-macros", diff --git a/Cargo.toml b/Cargo.toml index f60d4bd3..982f1135 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ chrono = { version = "0.4", default-features = false, features = [ "serde", "std", ] } +crossbeam-channel = "0.5" env_logger = "0.10" fake = "2.6" log = "0.4" diff --git a/butane/Cargo.toml b/butane/Cargo.toml index 28836975..bc1691fb 100644 --- a/butane/Cargo.toml +++ b/butane/Cargo.toml @@ -16,9 +16,8 @@ build = "build.rs" default = ["datetime", "json", "uuid"] fake = ["butane_core/fake"] json = ["butane_codegen/json", "butane_core/json"] -# todo re-enable sqlite -#sqlite = ["butane_core/sqlite"] -#sqlite-bundled = ["butane_core/sqlite-bundled"] +sqlite = ["butane_core/sqlite"] +sqlite-bundled = ["butane_core/sqlite-bundled"] pg = ["butane_core/pg"] datetime = ["butane_codegen/datetime", "butane_core/datetime"] debug = ["butane_core/debug"] diff --git a/butane/tests/migration-tests.rs b/butane/tests/migration-tests.rs index 8a82a9ee..2415c20c 100644 --- a/butane/tests/migration-tests.rs +++ b/butane/tests/migration-tests.rs @@ -174,10 +174,10 @@ fn current_migration_custom_type() { } #[cfg(feature = "sqlite")] -#[test] -fn migration_add_field_sqlite() { +#[tokio::test] +async fn migration_add_field_sqlite() { migration_add_field( - &mut sqlite_connection(), + &mut butane_test_helper::sqlite_connection().await, "ALTER TABLE Foo ADD COLUMN baz INTEGER NOT NULL DEFAULT 0;", // The exact details of futzing a DROP COLUMN in sqlite aren't // important (e.g. the temp table naming is certainly not part @@ -186,7 +186,7 @@ fn migration_add_field_sqlite() { // changes. If the change is innocuous, this test should just // be updated. "CREATE TABLE Foo__butane_tmp (id INTEGER NOT NULL PRIMARY KEY,bar TEXT NOT NULL);INSERT INTO Foo__butane_tmp SELECT id, bar FROM Foo;DROP TABLE Foo;ALTER TABLE Foo__butane_tmp RENAME TO Foo;", - ); + ).await; } #[cfg(feature = "pg")] @@ -202,16 +202,17 @@ async fn migration_add_field_pg() { } #[cfg(feature = "sqlite")] -#[test] -fn migration_add_field_with_default_sqlite() { +#[tokio::test] +async fn migration_add_field_with_default_sqlite() { migration_add_field_with_default( - &mut sqlite_connection(), + &mut sqlite_connection().await, "ALTER TABLE Foo ADD COLUMN baz INTEGER NOT NULL DEFAULT 42;", // See comments on migration_add_field_sqlite r#"CREATE TABLE Foo__butane_tmp (id INTEGER NOT NULL PRIMARY KEY,bar TEXT NOT NULL); INSERT INTO Foo__butane_tmp SELECT id, bar FROM Foo; DROP TABLE Foo;ALTER TABLE Foo__butane_tmp RENAME TO Foo;"#, - ); + ) + .await; } #[cfg(feature = "pg")] @@ -227,10 +228,10 @@ async fn migration_add_field_with_default_pg() { } #[cfg(feature = "sqlite")] -#[test] -fn migration_add_and_remove_field_sqlite() { +#[tokio::test] +async fn migration_add_and_remove_field_sqlite() { migration_add_and_remove_field( - &mut sqlite_connection(), + &mut sqlite_connection().await, // The exact details of futzing a DROP COLUMN in sqlite aren't // important (e.g. the temp table naming is certainly not part // of the API contract), but the goal here is to ensure we're @@ -245,7 +246,8 @@ fn migration_add_and_remove_field_sqlite() { CREATE TABLE Foo__butane_tmp (id INTEGER NOT NULL PRIMARY KEY,bar TEXT NOT NULL); INSERT INTO Foo__butane_tmp SELECT id, bar FROM Foo;DROP TABLE Foo; ALTER TABLE Foo__butane_tmp RENAME TO Foo;"#, - ); + ) + .await; } #[cfg(feature = "pg")] @@ -261,13 +263,14 @@ async fn migration_add_and_remove_field_pg() { } #[cfg(feature = "sqlite")] -#[test] -fn migration_delete_table_sqlite() { +#[tokio::test] +async fn migration_delete_table_sqlite() { migration_delete_table( - &mut sqlite_connection(), + &mut sqlite_connection().await, "DROP TABLE Foo;", "CREATE TABLE Foo (id INTEGER NOT NULL PRIMARY KEY,bar TEXT NOT NULL);", - ); + ) + .await; } #[cfg(feature = "pg")] diff --git a/butane_cli/Cargo.toml b/butane_cli/Cargo.toml index 0acb0110..799d59e7 100644 --- a/butane_cli/Cargo.toml +++ b/butane_cli/Cargo.toml @@ -15,6 +15,7 @@ doc = false [[bin]] name = "butane" path = "src/main.rs" +doc = false [features] # todo re-enable sqlite diff --git a/butane_core/Cargo.toml b/butane_core/Cargo.toml index 39810124..dcbac9fa 100644 --- a/butane_core/Cargo.toml +++ b/butane_core/Cargo.toml @@ -10,15 +10,15 @@ repository = "https://github.com/Electron100/butane" [features] +async-adapter = [] datetime = ["chrono", "tokio-postgres?/with-chrono-0_4"] debug = ["log"] fake = ["dep:fake", "rand"] json = ["tokio-postgres?/with-serde_json-1", "rusqlite?/serde_json"] log = ["dep:log", "rusqlite?/trace"] pg = ["bytes", "tokio-postgres"] -# todo re-enable sqlite -# sqlite = ["rusqlite"] -# sqlite-bundled = ["rusqlite/bundled"] +sqlite = ["rusqlite", "async-adapter"] +sqlite-bundled = ["rusqlite/bundled"] tls = ["native-tls", "postgres-native-tls"] @@ -27,6 +27,9 @@ async-trait = { workspace = true} bytes = { version = "1.0", optional = true } cfg-if = { workspace = true } chrono = { optional = true, workspace = true } +# todo make adapter optional +crossbeam-channel = { workspace = true } +dyn-clone = { version = "1.0" } fake = { workspace = true, optional = true } fallible-iterator = "0.2" fallible-streaming-iterator = "0.1" @@ -38,7 +41,7 @@ maybe-async-cfg = { workspace = true } native-tls = { version = "0.2", optional = true } once_cell = { workspace = true } pin-project = "1" -tokio = {workspace = true, features = ["rt", "sync"]} +tokio = {workspace = true, features = ["rt", "sync", "rt-multi-thread"]} tokio-postgres = { optional = true, workspace = true } postgres-native-tls = { version = "0.5", optional = true } proc-macro2 = { workspace = true } diff --git a/butane_core/src/codegen/dbobj.rs b/butane_core/src/codegen/dbobj.rs index 80193662..0941c502 100644 --- a/butane_core/src/codegen/dbobj.rs +++ b/butane_core/src/codegen/dbobj.rs @@ -56,7 +56,7 @@ pub fn impl_dbobject(ast_struct: &ItemStruct, config: &Config) -> TokenStream2 { quote!( #dataresult - #[butane::internal::async_trait] + #[butane::internal::async_trait(?Send)] impl butane::DataObject for #tyname { type PKType = #pktype; type Fields = #fields_type; diff --git a/butane_core/src/db/adapter.rs b/butane_core/src/db/adapter.rs new file mode 100644 index 00000000..f544f93a --- /dev/null +++ b/butane_core/src/db/adapter.rs @@ -0,0 +1,426 @@ +//! Adapter between sync and async connections + +use super::*; +use crate::query::Order; +use std::sync::Arc; +use std::thread; +use std::thread::JoinHandle; +use tokio::sync::oneshot; + +enum Command { + Func(Box), + Shutdown, +} + +#[derive(Debug)] +struct AsyncAdapterEnv { + sender: crossbeam_channel::Sender, + thread_handle: Option>, +} + +impl AsyncAdapterEnv { + fn new() -> Self { + // We spawn off a new thread and do all the blocking/sqlite work on that thread. Much of this is inspired + // by the crate async_sqlite + let (sender, receiver) = crossbeam_channel::unbounded(); + let thread_handle = thread::spawn(move || { + while let Ok(cmd) = receiver.recv() { + match cmd { + Command::Func(func) => func(), + Command::Shutdown => { + // TODO should connection support an explicit close? + return; + } + } + } + }); + Self { + sender, + thread_handle: Some(thread_handle), + } + } + + async fn invoke<'c, 's, 'result, F, T, U>( + &'s self, + context: &SyncSendPtrMut, + func: F, + ) -> Result + // todo can this just be result + where + F: FnOnce(&'c T) -> Result + Send, + F: 'result, + U: Send + 'result, + T: ?Sized + 'c, // TODO should this be Send + 's: 'result, + 'c: 'result, + { + // todo parts of this can be shared with the other two invoke functions + let (tx, rx) = oneshot::channel(); + let context_ptr = SendPtr::new(context.inner); + let func_taking_ptr = |ctx: SendPtr| func(unsafe { ctx.inner.as_ref() }.unwrap()); + let wrapped_func = move || _ = tx.send(func_taking_ptr(context_ptr)); + let boxed_func: Box = Box::new(wrapped_func); + let static_func: Box = + unsafe { std::mem::transmute(boxed_func) }; + self.sender.send(Command::Func(static_func))?; + // https://stackoverflow.com/questions/52424449/is-there-a-way-to-express-same-generic-type-with-different-lifetime-bound + //https://docs.rs/crossbeam/0.8.2/crossbeam/fn.scope.html + // TODO ensure soundness and document why + Ok(rx.await??) + } + + async fn invoke_mut<'c, 's, 'result, F, T, U>( + &'s self, + context: &SyncSendPtrMut, + func: F, + ) -> Result + where + F: FnOnce(&'c mut T) -> Result + Send, + F: 'result, + U: Send + 'result, + T: ?Sized + 'c, // TODO should this be Send + 's: 'result, + 'c: 'result, + { + let (tx, rx) = oneshot::channel(); + let context_ptr = SendPtrMut::new(context.inner); + let func_taking_ptr = |ctx: SendPtrMut| func(unsafe { ctx.inner.as_mut().unwrap() }); + let wrapped_func = move || _ = tx.send(func_taking_ptr(context_ptr)); + let boxed_func: Box = Box::new(wrapped_func); + let static_func: Box = + unsafe { std::mem::transmute(boxed_func) }; + self.sender.send(Command::Func(static_func))?; + // https://stackoverflow.com/questions/52424449/is-there-a-way-to-express-same-generic-type-with-different-lifetime-bound + //https://docs.rs/crossbeam/0.8.2/crossbeam/fn.scope.html + // TODO ensure soundness and document why + Ok(rx.await??) + } + + fn invoke_blocking<'c, 's, 'result, F, T, U>(&'s self, context: *const T, func: F) -> Result + where + F: FnOnce(&'c T) -> Result + Send, + F: 'result, + U: Send + 'result, + T: ?Sized + 'c, + 's: 'result, + 'c: 'result, + { + let (tx, rx) = crossbeam_channel::unbounded(); + let context_ptr = SendPtr::new(context as *const T); + let func_taking_ptr = |ctx: SendPtr| func(unsafe { ctx.inner.as_ref() }.unwrap()); + let wrapped_func = move || _ = tx.send(func_taking_ptr(context_ptr)); + let boxed_func: Box = Box::new(wrapped_func); + let static_func: Box = + unsafe { std::mem::transmute(boxed_func) }; + self.sender.send(Command::Func(static_func))?; + // TODO ensure soundness and document why + rx.recv()? + } +} + +impl Drop for AsyncAdapterEnv { + fn drop(&mut self) { + self.sender + .send(Command::Shutdown) + .expect("Cannot send async adapter env shutdown command, cannot join thread"); + self.thread_handle.take().map(|h| h.join()); + } +} + +struct SendPtr { + inner: *const T, +} +impl SendPtr { + fn new(inner: *const T) -> Self { + Self { inner } + } +} +unsafe impl Send for SendPtr {} + +struct SendPtrMut { + inner: *mut T, +} +impl SendPtrMut { + fn new(inner: *mut T) -> Self { + Self { inner } + } +} +unsafe impl Send for SendPtrMut {} + +#[derive(Debug)] +struct SyncSendPtrMut { + inner: *mut T, +} +impl SyncSendPtrMut { + fn new(inner: *mut T) -> Self { + // todo should this be unsafe + Self { inner } + } +} +impl From for SyncSendPtrMut +where + T: Sized, +{ + fn from(val: T) -> Self { + Self { + inner: Box::into_raw(Box::new(val)), + } // todo should this be unsafe + } +} +unsafe impl Send for SyncSendPtrMut {} +unsafe impl Sync for SyncSendPtrMut {} + +#[derive(Debug)] +pub(super) struct AsyncAdapter { + env: Arc, + context: SyncSendPtrMut, +} + +impl AsyncAdapter { + //todo document what this is for + fn new_internal(&self, context_ptr: SyncSendPtrMut) -> AsyncAdapter { + AsyncAdapter { + env: self.env.clone(), + context: context_ptr, + } + } + + /// Invokes the provided function with a sync method. + async fn invoke<'c, 's, 'result, F, U>(&'s self, func: F) -> Result + where + F: FnOnce(&'c T) -> Result + Send, + F: 'result, + U: Send + 'result, + 's: 'result, + 'c: 'result, + 's: 'c, + { + // todo verify the interior mutability won't panic here + self.env.invoke(&self.context, func).await + } + + async fn invoke_mut<'c, 'result, F, U>(&'c self, func: F) -> Result + where + F: FnOnce(&'c mut T) -> Result + Send, + F: 'result, + U: Send + 'result, + 'c: 'result, + { + // todo verify the interior mutability won't panic here + self.env.invoke_mut(&self.context, func).await + } + + fn invoke_blocking<'c, 'result, F, U>(&'c self, func: F) -> Result + where + F: FnOnce(&'c T) -> Result + Send, + F: 'result, + U: Send + 'result, + 'c: 'result, + { + // todo verify the interior mutability won't panic here + self.env.invoke_blocking(self.context.inner, func) + } +} + +impl AsyncAdapter { + pub(super) fn new(create_context: F) -> Result + where + Self: Sized, + F: FnOnce() -> Result + Send, + { + // TOOD execute the create context function on the thread + let context = create_context()?; + Ok(Self { + env: Arc::new(AsyncAdapterEnv::new()), + context: SyncSendPtrMut::new(Box::into_raw(Box::new(context))), + }) + } +} + +impl Drop for AsyncAdapter { + fn drop(&mut self) { + // Drops the box to Drop T + self.env + .invoke_blocking(&self.context, |context| unsafe { + std::mem::drop(Box::from_raw(context.inner)); + Ok(()) + }) + .unwrap(); + // Note, self.context.inner is now a dangling pointer + } +} + +#[async_trait(?Send)] +impl ConnectionMethods for AsyncAdapter +where + T: sync::ConnectionMethods + ?Sized, +{ + async fn execute(&self, sql: &str) -> Result<()> { + self.invoke(|conn| conn.execute(sql)).await + } + + async fn query<'c>( + &'c self, + table: &str, + columns: &[Column], + expr: Option, + limit: Option, + offset: Option, + sort: Option<&[Order]>, + ) -> Result> { + let rows = self + .invoke(|conn| { + let rows: Box = + conn.query(table, columns, expr, limit, offset, sort)?; + let vec_rows = super::connmethods::vec_from_backend_rows(rows, columns)?; + Ok(Box::new(vec_rows)) + }) + .await?; + Ok(rows) + } + async fn insert_returning_pk( + &self, + table: &str, + columns: &[Column], + pkcol: &Column, + values: &[SqlValRef<'_>], + ) -> Result { + self.invoke(|conn| conn.insert_returning_pk(table, columns, pkcol, values)) + .await + } + /// Like `insert_returning_pk` but with no return value + async fn insert_only( + &self, + table: &str, + columns: &[Column], + values: &[SqlValRef<'_>], + ) -> Result<()> { + self.invoke(|conn| conn.insert_only(table, columns, values)) + .await + } + /// Insert unless there's a conflict on the primary key column, in which case update + async fn insert_or_replace( + &self, + table: &str, + columns: &[Column], + pkcol: &Column, + values: &[SqlValRef<'_>], + ) -> Result<()> { + self.invoke(|conn| conn.insert_or_replace(table, columns, pkcol, values)) + .await + } + async fn update( + &self, + table: &str, + pkcol: Column, + pk: SqlValRef<'_>, + columns: &[Column], + values: &[SqlValRef<'_>], + ) -> Result<()> { + self.invoke(|conn| conn.update(table, pkcol, pk, columns, values)) + .await + } + async fn delete_where(&self, table: &str, expr: BoolExpr) -> Result { + self.invoke(|conn| conn.delete_where(table, expr)).await + } + /// Tests if a table exists in the database. + async fn has_table(&self, table: &str) -> Result { + self.invoke(|conn| conn.has_table(table)).await + } +} + +#[async_trait(?Send)] +impl BackendConnection for AsyncAdapter +where + T: sync::BackendConnection, +{ + async fn transaction<'c>(&'c mut self) -> Result> { + let transaction_ptr: SyncSendPtrMut = self + .invoke_mut(|conn| { + let transaction: sync::Transaction = conn.transaction()?; + let transaction_ptr: *mut dyn sync::BackendTransaction = + Box::into_raw(transaction.trans); + Ok(SyncSendPtrMut::new(transaction_ptr)) + }) + .await?; + let transaction_adapter = self.new_internal(transaction_ptr); + Ok(Transaction::new(Box::new(transaction_adapter))) + } + + fn backend(&self) -> Box { + // no sync-to-async translation needed but we still have to + // dispatch to our worker thread because only that thread owns + // the BackendConnection object. + // todo clean up unwrap + Box::new(BackendAdapter::new( + self.invoke_blocking(|conn| Ok(conn.backend())).unwrap(), + )) + } + fn backend_name(&self) -> &'static str { + // todo clean up unwrap + self.invoke_blocking(|conn| Ok(conn.backend_name())) + .unwrap() + } + /// Tests if the connection has been closed. Backends which do not + /// support this check should return false. + fn is_closed(&self) -> bool { + // todo clean up unwrap + self.invoke_blocking(|conn| Ok(conn.is_closed())).unwrap() + } +} + +#[async_trait(?Send)] +impl BackendTransaction<'c> for AsyncAdapter +where + T: sync::BackendTransaction<'c> + ?Sized, +{ + async fn commit(&mut self) -> Result<()> { + self.invoke_mut(|conn| conn.commit()).await + } + async fn rollback(&mut self) -> Result<()> { + self.invoke_mut(|conn| conn.rollback()).await + } + fn connection_methods(&self) -> &dyn ConnectionMethods { + self + } + fn connection_methods_mut(&mut self) -> &mut dyn ConnectionMethods { + self + } +} + +#[derive(Clone)] +pub(super) struct BackendAdapter +where + T: sync::Backend + Clone, +{ + inner: T, +} +impl BackendAdapter { + pub(super) fn new(inner: T) -> Self { + BackendAdapter { inner } + } +} + +#[async_trait] +impl Backend for BackendAdapter { + fn name(&self) -> &'static str { + self.inner.name() + } + fn create_migration_sql(&self, current: &adb::ADB, ops: Vec) -> Result { + self.inner.create_migration_sql(current, ops) + } + async fn connect(&self, conn_str: &str) -> Result { + // create a copy of the backend that can be moved into the closure + let sync_backend: T = self.inner.clone(); + let conn_str2 = conn_str.to_string(); + tokio::task::spawn_blocking(move || { + let connmethods_async = + adapter::AsyncAdapter::new(|| sync_backend.connect(&conn_str2))?; + let conn = Connection { + conn: Box::new(connmethods_async), + }; + Ok(conn) + }) + .await? + } +} diff --git a/butane_core/src/db/connmethods.rs b/butane_core/src/db/connmethods.rs index bdc887de..79cb5b7c 100644 --- a/butane_core/src/db/connmethods.rs +++ b/butane_core/src/db/connmethods.rs @@ -1,7 +1,7 @@ //! Not expected to be called directly by most users. Used by code //! generated by `#[model]`, `query!`, and other macros. -use crate::query::{BoolExpr, Order}; +use crate::query::{BoolExpr, Expr, Order}; use crate::{Result, SqlType, SqlVal, SqlValRef}; use async_trait::async_trait; use std::ops::{Deref, DerefMut}; @@ -15,18 +15,18 @@ mod internal { /// [DataObject][crate::DataObject] or the `query!` macro. This trait is /// implemented by both database connections and transactions. #[maybe_async_cfg::maybe(sync(), async())] - #[async_trait] - pub trait ConnectionMethods: Sync { + #[async_trait(?Send)] + pub trait ConnectionMethods { async fn execute(&self, sql: &str) -> Result<()>; - async fn query<'a, 'b, 'c: 'a>( + async fn query<'c>( &'c self, table: &str, - columns: &'b [Column], + columns: &[Column], expr: Option, limit: Option, offset: Option, sort: Option<&[Order]>, - ) -> Result>; + ) -> Result>; async fn insert_returning_pk( &self, table: &str, @@ -49,16 +49,19 @@ mod internal { pkcol: &Column, values: &[SqlValRef<'_>], ) -> Result<()>; - async fn update<'a>( + async fn update( &self, table: &str, pkcol: Column, - pk: SqlValRef<'a>, + pk: SqlValRef<'_>, columns: &[Column], values: &[SqlValRef<'_>], ) -> Result<()>; - async fn delete(&self, table: &str, pkcol: &'static str, pk: SqlVal) -> Result<()>; - // self.delete_where(table, BoolExpr::Eq(pkcol, Expr::Val(pk))).await?; + async fn delete(&self, table: &str, pkcol: &'static str, pk: SqlVal) -> Result<()> { + self.delete_where(table, BoolExpr::Eq(pkcol, Expr::Val(pk))) + .await?; + Ok(()) + } async fn delete_where(&self, table: &str, expr: BoolExpr) -> Result; /// Tests if a table exists in the database. async fn has_table(&self, table: &str) -> Result; @@ -163,6 +166,17 @@ impl VecRows { VecRows { rows, idx: 0 } } } +#[cfg(feature = "async-adapter")] +pub(crate) fn vec_from_backend_rows<'a>( + mut other: Box, + columns: &[Column], +) -> Result> { + let mut rows: Vec = Vec::new(); + while let Some(row) = other.next()? { + rows.push(VecRow::new(row, columns)?) + } + Ok(VecRows::new(rows)) +} impl BackendRows for VecRows where T: BackendRow, @@ -187,3 +201,46 @@ impl<'a> BackendRows for Box { self.deref().current() } } + +#[derive(Debug)] +pub(crate) struct VecRow { + values: Vec, +} + +#[cfg(feature = "async-adapter")] +impl VecRow { + fn new(original: &(dyn BackendRow), columns: &[Column]) -> Result { + if original.len() != columns.len() { + return Err(crate::Error::BoundsError( + "row length doesn't match columns specifier length".into(), + )); + } + Ok(Self { + values: (0..(columns.len())) + .map(|i| { + original + .get(i, columns[i].ty.clone()) + .map(|valref| valref.into()) + }) + .collect::>>()?, + }) + } +} +impl BackendRow for VecRow { + fn get(&self, idx: usize, ty: SqlType) -> Result { + self.values + .get(idx) + .ok_or_else(|| crate::Error::BoundsError("idx out of bounds".into())) + .and_then(|val| { + if val.is_compatible(&ty, true) { + Ok(val) + } else { + Err(crate::Error::CannotConvertSqlVal(ty.clone(), val.clone())) + } + }) + .map(|val| val.as_ref()) + } + fn len(&self) -> usize { + self.values.len() + } +} diff --git a/butane_core/src/db/macros.rs b/butane_core/src/db/macros.rs index a688a154..dc1a6516 100644 --- a/butane_core/src/db/macros.rs +++ b/butane_core/src/db/macros.rs @@ -6,20 +6,20 @@ macro_rules! connection_method_wrapper { sync(), async() )] - #[async_trait::async_trait] + #[async_trait::async_trait(?Send)] impl ConnectionMethods for $ty { async fn execute(&self, sql: &str) -> Result<()> { ConnectionMethods::execute(self.wrapped_connection_methods()?, sql).await } - async fn query<'a, 'b, 'c: 'a>( + async fn query<'c>( &'c self, table: &str, - columns: &'b [Column], + columns: &[Column], expr: Option, limit: Option, offset: Option, sort: Option<&[$crate::query::Order]>, - ) -> Result> { + ) -> Result> { self.wrapped_connection_methods()? .query(table, columns, expr, limit, offset, sort) .await @@ -56,11 +56,11 @@ macro_rules! connection_method_wrapper { .insert_or_replace(table, columns, pkcol, values) .await } - async fn update<'a>( + async fn update( &self, table: &str, pkcol: Column, - pk: SqlValRef<'a>, + pk: SqlValRef<'_>, columns: &[Column], values: &[SqlValRef<'_>], ) -> Result<()> { diff --git a/butane_core/src/db/mod.rs b/butane_core/src/db/mod.rs index b6b647ea..a37d685e 100644 --- a/butane_core/src/db/mod.rs +++ b/butane_core/src/db/mod.rs @@ -15,6 +15,7 @@ use crate::query::BoolExpr; use crate::{migrations::adb, Error, Result, SqlVal, SqlValRef}; use async_trait::async_trait; +use dyn_clone::DynClone; use serde::{Deserialize, Serialize}; use std::borrow::Cow; use std::fmt::Debug; @@ -23,14 +24,17 @@ use std::io::Write; use std::ops::{Deref, DerefMut}; use std::path::Path; +#[cfg(feature = "async-adapter")] +mod adapter; + mod connmethods; mod helper; mod macros; #[cfg(feature = "pg")] pub mod pg; -// TODO re-enable -//#[cfg(feature = "sqlite")] -//pub mod sqlite; + +#[cfg(feature = "sqlite")] +pub mod sqlite; // TODO re-enable //#[cfg(feature = "r2d2")] @@ -49,17 +53,32 @@ mod internal { use super::*; use connmethods::sync::ConnectionMethods as ConnectionMethodsSync; + #[maybe_async_cfg::maybe(sync())] + pub trait AsyncRequiresSend {} + #[maybe_async_cfg::maybe(idents(AsyncRequiresSend), sync())] + impl AsyncRequiresSend for T {} + + #[maybe_async_cfg::maybe(async())] + pub trait AsyncRequiresSend: Send {} + #[maybe_async_cfg::maybe(idents(AsyncRequiresSend), async())] + impl AsyncRequiresSend for T {} + /// Database connection. #[maybe_async_cfg::maybe( - idents(ConnectionMethods(sync = "ConnectionMethodsSync", async)), + idents( + AsyncRequiresSend, + Backend, + ConnectionMethods(sync = "ConnectionMethodsSync", async), + Transaction(sync = "TransactionSync", async), + ), sync(), async() )] - #[async_trait] - pub trait BackendConnection: ConnectionMethods + Debug + Send + 'static { + #[async_trait(?Send)] + pub trait BackendConnection: ConnectionMethods + Debug + AsyncRequiresSend { /// Begin a database transaction. The transaction object must be - /// used in place of this connection until it is committed and aborted. - async fn transaction(&mut self) -> Result; + /// used in place of this connection until it is committed or aborted. + async fn transaction(&mut self) -> Result>; /// Retrieve the backend backend this connection fn backend(&self) -> Box; fn backend_name(&self) -> &'static str; @@ -70,27 +89,13 @@ mod internal { /// Database connection. May be a connection to any type of database /// as it is a boxed abstraction over a specific connection. - #[maybe_async_cfg::maybe( - idents(BackendConnection( - sync = "BackendConnectionSync", - async = "BackendConnectionAsync" - )), - sync(), - async() - )] + #[maybe_async_cfg::maybe(idents(BackendConnection), sync(), async())] #[derive(Debug)] pub struct Connection { pub(super) conn: Box, } - #[maybe_async_cfg::maybe( - idents(BackendConnection( - sync = "BackendConnectionSync", - async = "BackendConnectionAsync" - )), - sync(), - async() - )] + #[maybe_async_cfg::maybe(idents(BackendConnection), sync(), async())] impl Connection { pub async fn execute(&mut self, sql: impl AsRef) -> Result<()> { self.conn.execute(sql.as_ref()).await @@ -103,14 +108,11 @@ mod internal { } #[maybe_async_cfg::maybe( - idents( - BackendConnection(sync = "BackendConnectionSync", async = "BackendConnectionAsync"), - Connection(sync = "ConnectionSync", async = "ConnectionAsync") - ), + idents(Backend, BackendConnection, Connection, Transaction), sync(), async() )] - #[async_trait] + #[async_trait(?Send)] impl BackendConnection for Connection { async fn transaction(&mut self) -> Result { self.conn.transaction().await @@ -128,12 +130,15 @@ mod internal { connection_method_wrapper!(Connection); #[maybe_async_cfg::maybe( - idents(ConnectionMethods(sync = "ConnectionMethodsSync", async)), + idents( + ConnectionMethods(sync = "ConnectionMethodsSync", async), + AsyncRequiresSend + ), sync(), async() )] - #[async_trait] - pub trait BackendTransaction<'c>: ConnectionMethods + Debug + Send { + #[async_trait(?Send)] + pub trait BackendTransaction<'c>: ConnectionMethods + AsyncRequiresSend + Debug { /// Commit the transaction Unfortunately because we use this as a /// trait object, we can't consume self. It should be understood /// that no methods should be called after commit. This trait is @@ -161,7 +166,7 @@ mod internal { )] #[derive(Debug)] pub struct Transaction<'c> { - trans: Box + 'c>, + pub(super) trans: Box + 'c>, } #[maybe_async_cfg::maybe( @@ -195,21 +200,70 @@ mod internal { } connection_method_wrapper!(Transaction<'_>); + + #[maybe_async_cfg::maybe( + idents( + BackendTransaction, + ConnectionMethods(sync = "ConnectionMethodsSync", async) + ), + sync(), + async() + )] + #[async_trait(?Send)] + impl<'c> BackendTransaction<'c> for Transaction<'c> { + async fn commit(&mut self) -> Result<()> { + self.trans.commit().await + } + async fn rollback(&mut self) -> Result<()> { + self.trans.deref_mut().rollback().await + } + fn connection_methods(&self) -> &dyn ConnectionMethods { + self + } + fn connection_methods_mut(&mut self) -> &mut dyn ConnectionMethods { + self + } + } + + #[maybe_async_cfg::maybe(idents(Connection(sync = "ConnectionSync", async)), sync(), async())] + /// Database backend. A boxed implementation can be returned by name via [get_backend][crate::db::get_backend]. + #[async_trait] + pub trait Backend: Send + Sync + DynClone { + fn name(&self) -> &'static str; + fn create_migration_sql( + &self, + current: &adb::ADB, + ops: Vec, + ) -> Result; + async fn connect(&self, conn_str: &str) -> Result; + } + + dyn_clone::clone_trait_object!(BackendAsync); + dyn_clone::clone_trait_object!(BackendSync); } +pub use internal::BackendAsync as Backend; pub use internal::BackendConnectionAsync as BackendConnection; -use internal::BackendTransactionAsync as BackendTransaction; pub use internal::ConnectionAsync as Connection; pub use internal::TransactionAsync as Transaction; +// unused may occur dependending on backends being compiled +// todo include by feature instead +#[allow(unused)] +use internal::BackendTransactionAsync as BackendTransaction; + pub mod sync { //! Synchronous (non-async versions of traits) pub use super::connmethods::sync::ConnectionMethods; pub use super::internal::BackendConnectionSync as BackendConnection; - use super::internal::BackendTransactionSync as BackendTransaction; + pub use super::internal::BackendSync as Backend; pub use super::internal::ConnectionSync as Connection; pub use super::internal::TransactionSync as Transaction; + + // unused may occur dependending on backends being compiled + #[allow(unused)] + pub(crate) use super::internal::BackendTransactionSync as BackendTransaction; } /// Connection specification. Contains the name of a database backend @@ -255,14 +309,6 @@ fn conn_complete_if_dir(path: &Path) -> Cow { } } -/// Database backend. A boxed implementation can be returned by name via [get_backend][crate::db::get_backend]. -#[async_trait] -pub trait Backend: Sync { - fn name(&self) -> &'static str; - fn create_migration_sql(&self, current: &adb::ADB, ops: Vec) -> Result; - async fn connect(&self, conn_str: &str) -> Result; -} - #[async_trait] impl Backend for Box { fn name(&self) -> &'static str { @@ -276,15 +322,40 @@ impl Backend for Box { } } +impl sync::Backend for Box { + fn name(&self) -> &'static str { + self.deref().name() + } + fn create_migration_sql(&self, current: &adb::ADB, ops: Vec) -> Result { + self.deref().create_migration_sql(current, ops) + } + fn connect(&self, conn_str: &str) -> Result { + self.deref().connect(conn_str) + } +} + /// Find a backend by name. pub fn get_backend(name: &str) -> Option> { match name { + #[cfg(feature = "sqlite")] + sqlite::BACKEND_NAME => Some(Box::new(adapter::BackendAdapter::new( + sqlite::SQLiteBackend::new(), + ))), #[cfg(feature = "pg")] pg::BACKEND_NAME => Some(Box::new(pg::PgBackend::new())), _ => None, } } +pub fn get_backend_sync(name: &str) -> Option> { + match name { + #[cfg(feature = "sqlite")] + sqlite::BACKEND_NAME => Some(Box::new(sqlite::SQLiteBackend::new())), + // todo wrap PG + _ => None, + } +} + /// Connect to a database. For non-boxed connections, see individual /// [Backend][crate::db::Backend] implementations. pub async fn connect(spec: &ConnectionSpec) -> Result { diff --git a/butane_core/src/db/pg.rs b/butane_core/src/db/pg.rs index c07ff818..6c973464 100644 --- a/butane_core/src/db/pg.rs +++ b/butane_core/src/db/pg.rs @@ -21,7 +21,7 @@ use tokio_postgres::GenericClient; pub const BACKEND_NAME: &str = "pg"; /// Pg [Backend][crate::db::Backend] implementation. -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct PgBackend {} impl PgBackend { pub fn new() -> PgBackend { @@ -100,7 +100,7 @@ impl PgConnectionLike for PgConnection { } } -#[async_trait] +#[async_trait(?Send)] impl BackendConnection for PgConnection { async fn transaction(&mut self) -> Result> { let trans: postgres::Transaction<'_> = self.client.transaction().await?; @@ -145,7 +145,7 @@ pub trait PgConnectionLike { fn client(&self) -> Result<&Self::Client>; } -#[async_trait] +#[async_trait(?Send)] impl ConnectionMethods for T where T: PgConnectionLike + std::marker::Sync, @@ -160,15 +160,15 @@ where Ok(()) } - async fn query<'a, 'b, 'c: 'a>( + async fn query<'c>( &'c self, table: &str, - columns: &'b [Column], + columns: &[Column], expr: Option, limit: Option, offset: Option, order: Option<&[query::Order]>, - ) -> Result> { + ) -> Result> { let mut sqlquery = String::new(); helper::sql_select(columns, table, &mut sqlquery); let mut values: Vec = Vec::new(); @@ -279,11 +279,11 @@ where future.await?; Ok(()) } - async fn update<'a>( + async fn update( &self, table: &str, pkcol: Column, - pk: SqlValRef<'a>, + pk: SqlValRef<'_>, columns: &[Column], values: &[SqlValRef<'_>], ) -> Result<()> { @@ -378,7 +378,7 @@ impl<'c> PgConnectionLike for PgTransaction<'c> { } } -#[async_trait] +#[async_trait(?Send)] impl<'c> BackendTransaction<'c> for PgTransaction<'c> { async fn commit(&mut self) -> Result<()> { match self.trans.take() { diff --git a/butane_core/src/db/sqlite.rs b/butane_core/src/db/sqlite.rs index 42684dff..a09560ff 100644 --- a/butane_core/src/db/sqlite.rs +++ b/butane_core/src/db/sqlite.rs @@ -2,22 +2,23 @@ #[cfg(feature = "log")] use std::sync::Once; -use super::helper; -use super::*; +use super::sync::{ + Backend, BackendConnection, BackendTransaction, Connection, ConnectionMethods, Transaction, +}; +use super::{helper, BackendRow, Column, RawQueryResult}; use crate::db::connmethods::BackendRows; -use crate::debug; use crate::migrations::adb::{AColumn, ATable, Operation, TypeIdentifier, ADB}; -use crate::query; -use crate::query::Order; -use crate::{Result, SqlType, SqlVal, SqlValRef}; +use crate::query::{BoolExpr, Order}; +use crate::{debug, query, Error, Result, SqlType, SqlVal, SqlValRef}; #[cfg(feature = "datetime")] use chrono::naive::NaiveDateTime; use fallible_streaming_iterator::FallibleStreamingIterator; use pin_project::pin_project; use std::borrow::Cow; use std::fmt::Write; +use std::ops::Deref; +use std::path::Path; use std::pin::Pin; -use std::sync::{Mutex, MutexGuard}; #[cfg(feature = "datetime")] const SQLITE_DT_FORMAT: &str = "%Y-%m-%d %H:%M:%S"; @@ -44,7 +45,7 @@ fn log_callback(error_code: std::ffi::c_int, message: &str) { } /// SQLite [Backend][crate::db::Backend] implementation. -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct SQLiteBackend {} impl SQLiteBackend { pub fn new() -> SQLiteBackend { @@ -84,7 +85,7 @@ impl Backend for SQLiteBackend { /// SQLite database connection. #[derive(Debug)] pub struct SQLiteConnection { - conn: Mutex, + conn: rusqlite::Connection, } impl SQLiteConnection { fn open(path: impl AsRef) -> Result { @@ -97,23 +98,81 @@ impl SQLiteConnection { }); rusqlite::Connection::open(path) - .map(|conn| SQLiteConnection { - conn: Mutex::new(conn), - }) + .map(|conn| SQLiteConnection { conn }) .map_err(|e| e.into()) } // For use with connection_method_wrapper macro #[allow(clippy::unnecessary_wraps)] - fn wrapped_connection_methods(&self) -> Result<&Mutex> { + fn wrapped_connection_methods(&self) -> Result<&rusqlite::Connection> { Ok(&self.conn) } } -connection_method_wrapper!(SQLiteConnection); +impl ConnectionMethods for SQLiteConnection { + fn execute(&self, sql: &str) -> Result<()> { + ConnectionMethods::execute(self.wrapped_connection_methods()?, sql) + } + fn query<'a, 'b, 'c>( + &'c self, + table: &str, + columns: &'b [Column], + expr: Option, + limit: Option, + offset: Option, + sort: Option<&[crate::query::Order]>, + ) -> Result> { + self.wrapped_connection_methods()? + .query(table, columns, expr, limit, offset, sort) + } + fn insert_returning_pk( + &self, + table: &str, + columns: &[Column], + pkcol: &Column, + values: &[SqlValRef<'_>], + ) -> Result { + self.wrapped_connection_methods()? + .insert_returning_pk(table, columns, pkcol, values) + } + fn insert_only(&self, table: &str, columns: &[Column], values: &[SqlValRef<'_>]) -> Result<()> { + self.wrapped_connection_methods()? + .insert_only(table, columns, values) + } + fn insert_or_replace( + &self, + table: &str, + columns: &[Column], + pkcol: &Column, + values: &[SqlValRef<'_>], + ) -> Result<()> { + self.wrapped_connection_methods()? + .insert_or_replace(table, columns, pkcol, values) + } + fn update<'a>( + &self, + table: &str, + pkcol: Column, + pk: SqlValRef<'a>, + columns: &[Column], + values: &[SqlValRef<'_>], + ) -> Result<()> { + self.wrapped_connection_methods()? + .update(table, pkcol, pk, columns, values) + } + fn delete(&self, table: &str, pkcol: &'static str, pk: SqlVal) -> Result<()> { + self.wrapped_connection_methods()?.delete(table, pkcol, pk) + } + fn delete_where(&self, table: &str, expr: BoolExpr) -> Result { + self.wrapped_connection_methods()?.delete_where(table, expr) + } + fn has_table(&self, table: &str) -> Result { + self.wrapped_connection_methods()?.has_table(table) + } +} impl BackendConnection for SQLiteConnection { fn transaction(&mut self) -> Result> { - let trans: rusqlite::Transaction<'_> = self.conn.lock()?.transaction()?; + let trans: rusqlite::Transaction<'_> = self.conn.transaction()?; let trans = Box::new(SqliteTransaction::new(trans)); Ok(Transaction::new(trans)) } @@ -128,176 +187,16 @@ impl BackendConnection for SQLiteConnection { } } -fn rs_conn_execute(conn: &rusqlite::Connection, sql: &str) -> Result<()> { - if cfg!(feature = "log") { - debug!("execute sql {}", sql); - } - conn.execute_batch(sql.as_ref())?; - Ok(()) -} - -fn rs_conn_query<'a, 'b, 'c: 'a>( - conn: &'c rusqlite::Connection, - table: &str, - columns: &'b [Column], - expr: Option, - limit: Option, - offset: Option, - order: Option<&[Order]>, -) -> Result> { - let mut sqlquery = String::new(); - helper::sql_select(columns, table, &mut sqlquery); - let mut values: Vec = Vec::new(); - if let Some(expr) = expr { - sqlquery.write_str(" WHERE ").unwrap(); - sql_for_expr( - query::Expr::Condition(Box::new(expr)), - &mut values, - &mut SQLitePlaceholderSource::new(), - &mut sqlquery, - ); - } - - if let Some(order) = order { - helper::sql_order(order, &mut sqlquery) - } - - if let Some(limit) = limit { - helper::sql_limit(limit, &mut sqlquery) - } - - if let Some(offset) = offset { - if limit.is_none() { - // Sqlite only supports offset in conjunction with - // limit, so add a max limit if we don't have one - // already. - helper::sql_limit(i32::MAX, &mut sqlquery) - } - helper::sql_offset(offset, &mut sqlquery) - } - - debug!("query sql {}", sqlquery); - let stmt = conn.prepare(&sqlquery)?; - // TODO - /*let rows = stmt.query().mapped(|row| { - for col in columns { - - } - - }*/ - let adapter = QueryAdapter::new(stmt, rusqlite::params_from_iter(values))?; - Ok(Box::new(adapter)) -} - -fn rs_conn_insert_returning_pk( - conn: &rusqlite::Connection, - table: &str, - columns: &[Column], - pkcol: &Column, - values: &[SqlValRef<'_>], -) -> Result { - let mut sql = String::new(); - helper::sql_insert_with_placeholders( - table, - columns, - &mut SQLitePlaceholderSource::new(), - &mut sql, - ); - if cfg!(feature = "log") { - debug!("insert sql {}", sql); - } - conn.execute(&sql, rusqlite::params_from_iter(values))?; - let pk: SqlVal = conn.query_row_and_then( - &format!( - "SELECT {} FROM {} WHERE ROWID = last_insert_rowid()", - pkcol.name(), - table - ), - [], - |row| sql_val_from_rusqlite(row.get_ref_unwrap(0), pkcol), - )?; - Ok(pk) -} -fn rs_conn_insert_only( - conn: &rusqlite::Connection, - table: &str, - columns: &[Column], - values: &[SqlValRef<'_>], -) -> Result<()> { - let mut sql = String::new(); - helper::sql_insert_with_placeholders( - table, - columns, - &mut SQLitePlaceholderSource::new(), - &mut sql, - ); - if cfg!(feature = "log") { - debug!("insert sql {}", sql); - } - conn.execute(&sql, rusqlite::params_from_iter(values))?; - Ok(()) -} -fn rs_conn_insert_or_replace( - conn: &rusqlite::Connection, - table: &str, - columns: &[Column], - _pkcol: &Column, - values: &[SqlValRef], -) -> Result<()> { - let mut sql = String::new(); - sql_insert_or_update(table, columns, &mut sql); - conn.execute(&sql, rusqlite::params_from_iter(values))?; - Ok(()) -} -fn rs_conn_update( - conn: &rusqlite::Connection, - table: &str, - pkcol: Column, - pk: SqlValRef, - columns: &[Column], - values: &[SqlValRef<'_>], -) -> Result<()> { - let mut sql = String::new(); - helper::sql_update_with_placeholders( - table, - pkcol, - columns, - &mut SQLitePlaceholderSource::new(), - &mut sql, - ); - let placeholder_values = [values, &[pk]].concat(); - if cfg!(feature = "log") { - debug!("update sql {}", sql); - } - conn.execute(&sql, rusqlite::params_from_iter(placeholder_values))?; - Ok(()) -} -fn rs_conn_delete_where(conn: &rusqlite::Connection, table: &str, expr: BoolExpr) -> Result { - let mut sql = String::new(); - let mut values: Vec = Vec::new(); - write!(&mut sql, "DELETE FROM {table} WHERE ").unwrap(); - sql_for_expr( - query::Expr::Condition(Box::new(expr)), - &mut values, - &mut SQLitePlaceholderSource::new(), - &mut sql, - ); - let cnt = self.execute(&sql, rusqlite::params_from_iter(values))?; - Ok(cnt) -} -fn rs_conn_has_table(conn: &rusqlite::Connection, table: &str) -> Result { - let mut stmt = conn.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?;")?; - let mut rows = stmt.query([table])?; - Ok(rows.next()?.is_some()) -} - -#[async_trait] -impl ConnectionMethods for Mutex { +impl ConnectionMethods for rusqlite::Connection { fn execute(&self, sql: &str) -> Result<()> { - rs_conn_execute(self.lock()?.deref(), sql) + if cfg!(feature = "log") { + debug!("execute sql {}", sql); + } + self.execute_batch(sql.as_ref())?; + Ok(()) } - async fn query<'a, 'b, 'c: 'a>( + fn query<'b, 'c>( &'c self, table: &str, columns: &'b [Column], @@ -305,16 +204,45 @@ impl ConnectionMethods for Mutex { limit: Option, offset: Option, order: Option<&[Order]>, - ) -> Result> { - rs_conn_query( - self.lock()?.deref(), - table, - columns, - expr, - limit, - offset, - order, - ) + ) -> Result> { + let mut sqlquery = String::new(); + helper::sql_select(columns, table, &mut sqlquery); + let mut values: Vec = Vec::new(); + if let Some(expr) = expr { + sqlquery.write_str(" WHERE ").unwrap(); + sql_for_expr( + query::Expr::Condition(Box::new(expr)), + &mut values, + &mut SQLitePlaceholderSource::new(), + &mut sqlquery, + ); + } + + if let Some(order) = order { + helper::sql_order(order, &mut sqlquery) + } + + if let Some(limit) = limit { + helper::sql_limit(limit, &mut sqlquery) + } + + if let Some(offset) = offset { + if limit.is_none() { + // Sqlite only supports offset in conjunction with + // limit, so add a max limit if we don't have one + // already. + helper::sql_limit(i32::MAX, &mut sqlquery) + } + helper::sql_offset(offset, &mut sqlquery) + } + + debug!("query sql {}", sqlquery); + #[cfg(feature = "debug")] + debug!("values {:?}", values); + + let stmt = self.prepare(&sqlquery)?; + let adapter = QueryAdapter::new(stmt, rusqlite::params_from_iter(values))?; + Ok(Box::new(adapter)) } fn insert_returning_pk( &self, @@ -323,10 +251,45 @@ impl ConnectionMethods for Mutex { pkcol: &Column, values: &[SqlValRef<'_>], ) -> Result { - rs_conn_insert_returning_pk(self.lock()?.deref(), table, columns, pkcol, values) + let mut sql = String::new(); + helper::sql_insert_with_placeholders( + table, + columns, + &mut SQLitePlaceholderSource::new(), + &mut sql, + ); + if cfg!(feature = "log") { + debug!("insert sql {}", sql); + #[cfg(feature = "debug")] + debug!("values {:?}", values); + } + self.execute(&sql, rusqlite::params_from_iter(values))?; + let pk: SqlVal = self.query_row_and_then( + &format!( + "SELECT {} FROM {} WHERE ROWID = last_insert_rowid()", + pkcol.name(), + table + ), + [], + |row| sql_val_from_rusqlite(row.get_ref_unwrap(0), pkcol), + )?; + Ok(pk) } fn insert_only(&self, table: &str, columns: &[Column], values: &[SqlValRef<'_>]) -> Result<()> { - rs_conn_insert_only(self.lock()?.deref(), table, columns, values) + let mut sql = String::new(); + helper::sql_insert_with_placeholders( + table, + columns, + &mut SQLitePlaceholderSource::new(), + &mut sql, + ); + if cfg!(feature = "log") { + debug!("insert sql {}", sql); + #[cfg(feature = "debug")] + debug!("values {:?}", values); + } + self.execute(&sql, rusqlite::params_from_iter(values))?; + Ok(()) } fn insert_or_replace( &self, @@ -335,7 +298,10 @@ impl ConnectionMethods for Mutex { pkcol: &Column, values: &[SqlValRef], ) -> Result<()> { - rs_conn_insert_or_replace(self.lock()?.deref(), table, columns, pkcol, values) + let mut sql = String::new(); + sql_insert_or_update(table, columns, pkcol, &mut sql); + self.execute(&sql, rusqlite::params_from_iter(values))?; + Ok(()) } fn update( &self, @@ -345,64 +311,90 @@ impl ConnectionMethods for Mutex { columns: &[Column], values: &[SqlValRef<'_>], ) -> Result<()> { - rs_conn_update(self.lock()?.deref(), table, pkcol, pk, columns, values) + let mut sql = String::new(); + helper::sql_update_with_placeholders( + table, + pkcol, + columns, + &mut SQLitePlaceholderSource::new(), + &mut sql, + ); + let placeholder_values = [values, &[pk]].concat(); + if cfg!(feature = "log") { + debug!("update sql {}", sql); + #[cfg(feature = "debug")] + debug!("placeholders {:?}", placeholder_values); + } + self.execute(&sql, rusqlite::params_from_iter(placeholder_values))?; + Ok(()) } fn delete_where(&self, table: &str, expr: BoolExpr) -> Result { - rs_conn_delete_where(self.lock()?.deref(), table, expr) + let mut sql = String::new(); + let mut values: Vec = Vec::new(); + write!( + &mut sql, + "DELETE FROM {} WHERE ", + helper::quote_reserved_word(table) + ) + .unwrap(); + sql_for_expr( + query::Expr::Condition(Box::new(expr)), + &mut values, + &mut SQLitePlaceholderSource::new(), + &mut sql, + ); + if cfg!(feature = "log") { + debug!("delete where sql {}", sql); + #[cfg(feature = "debug")] + debug!("placeholders {:?}", values); + } + let cnt = self.execute(&sql, rusqlite::params_from_iter(values))?; + Ok(cnt) } fn has_table(&self, table: &str) -> Result { - rs_conn_has_table(&self.lock()?.deref(), table) + let mut stmt = + self.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?;")?; + let mut rows = stmt.query([table])?; + Ok(rows.next()?.is_some()) } } #[derive(Debug)] struct SqliteTransaction<'c> { - trans: Option>>, + trans: Option>, } impl<'c> SqliteTransaction<'c> { fn new(trans: rusqlite::Transaction<'c>) -> Self { - SqliteTransaction { - trans: Some(Mutex::new(trans)), - } + SqliteTransaction { trans: Some(trans) } } - fn get(&self) -> Result<&Mutex>> { + fn get(&self) -> Result<&rusqlite::Transaction<'c>> { match &self.trans { None => Err(Self::already_consumed()), Some(trans) => Ok(trans), } } - fn wrapped_connection_methods(&self) -> Result<&Mutex>> { - Ok(self.get()?) + fn wrapped_connection_methods(&self) -> Result<&rusqlite::Connection> { + Ok(self.get()?.deref()) } fn already_consumed() -> Error { Error::Internal("transaction has already been consumed".to_string()) } } - -#[async_trait] impl ConnectionMethods for SqliteTransaction<'_> { fn execute(&self, sql: &str) -> Result<()> { - rs_conn_execute(self.get()?.lock()?.deref(), sql) + ConnectionMethods::execute(self.wrapped_connection_methods()?, sql) } - - async fn query<'a, 'b, 'c: 'a>( + fn query<'b, 'c>( &'c self, table: &str, columns: &'b [Column], expr: Option, limit: Option, offset: Option, - order: Option<&[Order]>, - ) -> Result> { - rs_conn_query( - self.get()?.lock()?.deref(), - table, - columns, - expr, - limit, - offset, - order, - ) + sort: Option<&[crate::query::Order]>, + ) -> Result> { + self.wrapped_connection_methods()? + .query(table, columns, expr, limit, offset, sort) } fn insert_returning_pk( &self, @@ -411,46 +403,45 @@ impl ConnectionMethods for SqliteTransaction<'_> { pkcol: &Column, values: &[SqlValRef<'_>], ) -> Result { - rs_conn_insert_returning_pk(self.get()?.lock()?.deref(), table, columns, pkcol, values) + self.wrapped_connection_methods()? + .insert_returning_pk(table, columns, pkcol, values) } fn insert_only(&self, table: &str, columns: &[Column], values: &[SqlValRef<'_>]) -> Result<()> { - rs_conn_insert_only(self.get()?.lock()?.deref(), table, columns, values) + self.wrapped_connection_methods()? + .insert_only(table, columns, values) } fn insert_or_replace( &self, table: &str, columns: &[Column], pkcol: &Column, - values: &[SqlValRef], + values: &[SqlValRef<'_>], ) -> Result<()> { - rs_conn_insert_or_replace(self.get()?.lock()?.deref(), table, columns, pkcol, values) + self.wrapped_connection_methods()? + .insert_or_replace(table, columns, pkcol, values) } - fn update( + fn update<'a>( &self, table: &str, pkcol: Column, - pk: SqlValRef, + pk: SqlValRef<'a>, columns: &[Column], values: &[SqlValRef<'_>], ) -> Result<()> { - rs_conn_update( - self.get()?.lock()?.deref(), - table, - pkcol, - pk, - columns, - values, - ) + self.wrapped_connection_methods()? + .update(table, pkcol, pk, columns, values) + } + fn delete(&self, table: &str, pkcol: &'static str, pk: SqlVal) -> Result<()> { + self.wrapped_connection_methods()?.delete(table, pkcol, pk) } fn delete_where(&self, table: &str, expr: BoolExpr) -> Result { - rs_conn_delete_where(self.get()?.lock()?.deref(), table, expr) + self.wrapped_connection_methods()?.delete_where(table, expr) } fn has_table(&self, table: &str) -> Result { - rs_conn_has_table(&self.get()?.lock()?.deref(), table) + self.wrapped_connection_methods()?.has_table(table) } } -#[async_trait] impl<'c> BackendTransaction<'c> for SqliteTransaction<'c> { fn commit(&mut self) -> Result<()> { match self.trans.take() { @@ -458,7 +449,7 @@ impl<'c> BackendTransaction<'c> for SqliteTransaction<'c> { Some(trans) => Ok(trans.commit()?), } } - async fn rollback(&mut self) -> Result<()> { + fn rollback(&mut self) -> Result<()> { match self.trans.take() { None => Err(Self::already_consumed()), Some(trans) => Ok(trans.rollback()?), diff --git a/butane_core/src/lib.rs b/butane_core/src/lib.rs index c603ba63..bad964e8 100644 --- a/butane_core/src/lib.rs +++ b/butane_core/src/lib.rs @@ -84,7 +84,7 @@ pub trait DataResult: Sized { /// /// Rather than implementing this type manually, use the /// `#[model]` attribute. -#[async_trait] +#[async_trait(?Send)] pub trait DataObject: DataResult + Sync { /// The type of the primary key field. type PKType: PrimaryKeyType; @@ -213,6 +213,12 @@ pub enum Error { TLS(#[from] native_tls::Error), #[error("Generic error {0}")] Generic(#[from] Box), + #[error("Tokio join error {0}")] + TokioJoin(#[from] tokio::task::JoinError), + #[error("Tokio recv error {0}")] + TokioRecv(#[from] tokio::sync::oneshot::error::RecvError), + #[error("Crossbeam cannot send/recv, channel disconnected")] + CrossbeamChannel, } #[cfg(feature = "sqlite")] @@ -231,6 +237,18 @@ impl From for Error { } } +impl From> for Error { + fn from(_e: crossbeam_channel::SendError) -> Self { + Self::CrossbeamChannel + } +} + +impl From for Error { + fn from(_e: crossbeam_channel::RecvError) -> Self { + Self::CrossbeamChannel + } +} + /// Enumeration of the types a database value may take. /// /// See also [`SqlVal`][crate::SqlVal]. diff --git a/butane_core/src/migrations/fsmigrations.rs b/butane_core/src/migrations/fsmigrations.rs index db299f14..bf0daa21 100644 --- a/butane_core/src/migrations/fsmigrations.rs +++ b/butane_core/src/migrations/fsmigrations.rs @@ -296,7 +296,7 @@ impl Migrations for FsMigrations { } } -#[async_trait::async_trait] +#[async_trait::async_trait(?Send)] impl MigrationsMut for FsMigrations { fn current(&mut self) -> &mut Self::M { &mut self.current @@ -336,7 +336,8 @@ impl MigrationsMut for FsMigrations { std::fs::remove_file(entry.path())?; } } - conn.delete_where(super::ButaneMigration::TABLE, crate::query::BoolExpr::True).await?; + conn.delete_where(super::ButaneMigration::TABLE, crate::query::BoolExpr::True) + .await?; Ok(()) } } diff --git a/butane_core/src/migrations/memmigrations.rs b/butane_core/src/migrations/memmigrations.rs index 90c772b4..99bc0e12 100644 --- a/butane_core/src/migrations/memmigrations.rs +++ b/butane_core/src/migrations/memmigrations.rs @@ -128,7 +128,7 @@ impl Migrations for MemMigrations { } } -#[async_trait::async_trait] +#[async_trait::async_trait(?Send)] impl MigrationsMut for MemMigrations { fn current(&mut self) -> &mut Self::M { &mut self.current @@ -161,7 +161,8 @@ impl MigrationsMut for MemMigrations { async fn clear_migrations(&mut self, conn: &impl ConnectionMethods) -> Result<()> { self.migrations.clear(); self.latest = None; - conn.delete_where(ButaneMigration::TABLE, BoolExpr::True).await?; + conn.delete_where(ButaneMigration::TABLE, BoolExpr::True) + .await?; Ok(()) } } diff --git a/butane_core/src/migrations/migration.rs b/butane_core/src/migrations/migration.rs index 7c9b170c..9e647e8a 100644 --- a/butane_core/src/migrations/migration.rs +++ b/butane_core/src/migrations/migration.rs @@ -14,7 +14,7 @@ use std::fmt::Debug; /// /// A Migration cannot be constructed directly, only retrieved from /// [Migrations][crate::migrations::Migrations]. -#[async_trait::async_trait] +#[async_trait::async_trait(?Send)] pub trait Migration: Debug + PartialEq { /// Retrieves the full abstract database state describing all tables fn db(&self) -> Result; @@ -59,7 +59,8 @@ pub trait Migration: Debug + PartialEq { ButaneMigration::TABLE, ButaneMigration::COLUMNS, &[self.name().as_ref().to_sql_ref()], - ).await + ) + .await } /// Un-apply (downgrade) the migration to a database @@ -77,7 +78,8 @@ pub trait Migration: Debug + PartialEq { tx.delete_where( ButaneMigration::TABLE, BoolExpr::Eq(ButaneMigration::PKCOL, Expr::Val(nameval)), - ).await?; + ) + .await?; tx.commit().await } } diff --git a/butane_core/src/migrations/mod.rs b/butane_core/src/migrations/mod.rs index 031ebcd3..b3792824 100644 --- a/butane_core/src/migrations/mod.rs +++ b/butane_core/src/migrations/mod.rs @@ -23,7 +23,7 @@ use async_trait::async_trait; pub use memmigrations::{MemMigration, MemMigrations}; /// A collection of migrations. -#[async_trait] +#[async_trait(?Send)] pub trait Migrations { type M: Migration; @@ -112,7 +112,7 @@ pub trait Migrations { } } -#[async_trait] +#[async_trait(?Send)] pub trait MigrationsMut: Migrations where Self::M: MigrationMut, @@ -264,7 +264,7 @@ impl DataResult for ButaneMigration { } } -#[async_trait] +#[async_trait(?Send)] impl DataObject for ButaneMigration { type PKType = String; type Fields = (); // we don't need Fields as we never filter diff --git a/butane_test_helper/Cargo.toml b/butane_test_helper/Cargo.toml index 62b72e9d..8f845963 100644 --- a/butane_test_helper/Cargo.toml +++ b/butane_test_helper/Cargo.toml @@ -13,7 +13,7 @@ documentation = "https://docs.rs/butane/" [dependencies] # todo re-enable sqlite -butane_core = { features = ["pg"], workspace = true } +butane_core = { features = ["pg", "sqlite"], workspace = true } libc = "0.2" once_cell = { workspace = true } tokio-postgres = { features = ["with-geo-types-0_7"], workspace = true } diff --git a/butane_test_helper/src/lib.rs b/butane_test_helper/src/lib.rs index 360ee822..58835f25 100644 --- a/butane_test_helper/src/lib.rs +++ b/butane_test_helper/src/lib.rs @@ -1,6 +1,4 @@ use butane_core::db::{connect, get_backend, pg, Backend, Connection, ConnectionSpec}; -//todo -//use butane_core::db::sqlite; use butane_core::migrations::{self, MemMigrations, Migration, Migrations, MigrationsMut}; use once_cell::sync::Lazy; use std::io::{BufRead, BufReader, Read, Write}; @@ -10,6 +8,8 @@ use std::process::{ChildStderr, Command, Stdio}; use std::sync::Mutex; use uuid::Uuid; +use butane_core::db::sqlite; + pub async fn pg_connection() -> (Connection, PgSetupData) { let backend = get_backend(pg::BACKEND_NAME).unwrap(); let data = pg_setup().await; @@ -190,10 +190,9 @@ pub async fn setup_db(backend: Box, conn: &mut Connection, migrate: } } -// todo -/*pub fn sqlite_connection() -> Connection { +pub async fn sqlite_connection() -> Connection { let backend = get_backend(sqlite::BACKEND_NAME).unwrap(); - backend.connect(":memory:").unwrap() + backend.connect(":memory:").await.unwrap() } pub fn sqlite_connspec() -> ConnectionSpec { @@ -202,7 +201,6 @@ pub fn sqlite_connspec() -> ConnectionSpec { pub fn sqlite_setup() {} pub fn sqlite_teardown(_: ()) {} -*/ #[macro_export] macro_rules! maketest { From 339b59ae394000af7b146db507acdeb92b87ec89 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Wed, 2 Aug 2023 10:52:17 -0400 Subject: [PATCH 25/78] Sqlite re-enabled in tests, tests passing --- butane_core/src/db/adapter.rs | 11 ++++++++--- butane_core/tests/connection.rs | 5 +++-- butane_test_helper/Cargo.toml | 1 - butane_test_helper/src/lib.rs | 12 +++++------- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/butane_core/src/db/adapter.rs b/butane_core/src/db/adapter.rs index f544f93a..78c9ef53 100644 --- a/butane_core/src/db/adapter.rs +++ b/butane_core/src/db/adapter.rs @@ -147,7 +147,6 @@ impl SendPtrMut { } unsafe impl Send for SendPtrMut {} -#[derive(Debug)] struct SyncSendPtrMut { inner: *mut T, } @@ -159,7 +158,7 @@ impl SyncSendPtrMut { } impl From for SyncSendPtrMut where - T: Sized, + T: Debug + Sized, { fn from(val: T) -> Self { Self { @@ -167,9 +166,15 @@ where } // todo should this be unsafe } } -unsafe impl Send for SyncSendPtrMut {} +unsafe impl Send for SyncSendPtrMut {} unsafe impl Sync for SyncSendPtrMut {} +impl Debug for SyncSendPtrMut { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + unsafe { (*self.inner).fmt(f) } + } +} + #[derive(Debug)] pub(super) struct AsyncAdapter { env: Arc, diff --git a/butane_core/tests/connection.rs b/butane_core/tests/connection.rs index f3e21c3e..67e5bb7e 100644 --- a/butane_core/tests/connection.rs +++ b/butane_core/tests/connection.rs @@ -64,10 +64,11 @@ async fn unreachable_pg_connection() { async fn debug_connection(conn: Connection) { let backend_name = conn.backend_name().clone(); + let debug_str = format!("{:?}", conn); if backend_name == "pg" { - assert!(format!("{:?}", conn).contains("conn: true")); + assert!(debug_str.contains("conn: true")); } else { - assert!(format!("{:?}", conn).contains("path: Some(\"\")")); + assert!(debug_str.contains("path: Some(\"\")")); } } testall_no_migrate!(debug_connection); diff --git a/butane_test_helper/Cargo.toml b/butane_test_helper/Cargo.toml index 8f845963..5758c79f 100644 --- a/butane_test_helper/Cargo.toml +++ b/butane_test_helper/Cargo.toml @@ -12,7 +12,6 @@ repository = "https://github.com/Electron100/butane" documentation = "https://docs.rs/butane/" [dependencies] -# todo re-enable sqlite butane_core = { features = ["pg", "sqlite"], workspace = true } libc = "0.2" once_cell = { workspace = true } diff --git a/butane_test_helper/src/lib.rs b/butane_test_helper/src/lib.rs index 58835f25..b0b8e341 100644 --- a/butane_test_helper/src/lib.rs +++ b/butane_test_helper/src/lib.rs @@ -199,7 +199,7 @@ pub fn sqlite_connspec() -> ConnectionSpec { ConnectionSpec::new(sqlite::BACKEND_NAME, ":memory:") } -pub fn sqlite_setup() {} +pub async fn sqlite_setup() {} pub fn sqlite_teardown(_: ()) {} #[macro_export] @@ -237,12 +237,11 @@ macro_rules! maketest_pg { #[macro_export] macro_rules! testall { ($fname:ident) => { - // TODO re-enable - /*cfg_if::cfg_if! { + cfg_if::cfg_if! { if #[cfg(feature = "sqlite")] { maketest!($fname, sqlite, &format!(":memory:"), setup_data, true); } - }*/ + } cfg_if::cfg_if! { if #[cfg(feature = "pg")] { maketest_pg!($fname, true); @@ -254,12 +253,11 @@ macro_rules! testall { #[macro_export] macro_rules! testall_no_migrate { ($fname:ident) => { - // TODO re-enable - /*cfg_if::cfg_if! { + cfg_if::cfg_if! { if #[cfg(feature = "sqlite")] { maketest!($fname, sqlite, &format!(":memory:"), setup_data, false); } - }*/ + } cfg_if::cfg_if! { if #[cfg(feature = "pg")] { maketest_pg!($fname, false); From 0049a42e5f5e13eeeda2e7475077f008998bee4e Mon Sep 17 00:00:00 2001 From: James Oakley Date: Wed, 2 Aug 2023 10:57:15 -0400 Subject: [PATCH 26/78] Clippy fixes --- butane_core/src/db/adapter.rs | 4 ++-- butane_core/src/db/sqlite.rs | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/butane_core/src/db/adapter.rs b/butane_core/src/db/adapter.rs index 78c9ef53..33f7156f 100644 --- a/butane_core/src/db/adapter.rs +++ b/butane_core/src/db/adapter.rs @@ -66,7 +66,7 @@ impl AsyncAdapterEnv { // https://stackoverflow.com/questions/52424449/is-there-a-way-to-express-same-generic-type-with-different-lifetime-bound //https://docs.rs/crossbeam/0.8.2/crossbeam/fn.scope.html // TODO ensure soundness and document why - Ok(rx.await??) + rx.await? } async fn invoke_mut<'c, 's, 'result, F, T, U>( @@ -93,7 +93,7 @@ impl AsyncAdapterEnv { // https://stackoverflow.com/questions/52424449/is-there-a-way-to-express-same-generic-type-with-different-lifetime-bound //https://docs.rs/crossbeam/0.8.2/crossbeam/fn.scope.html // TODO ensure soundness and document why - Ok(rx.await??) + rx.await? } fn invoke_blocking<'c, 's, 'result, F, T, U>(&'s self, context: *const T, func: F) -> Result diff --git a/butane_core/src/db/sqlite.rs b/butane_core/src/db/sqlite.rs index a09560ff..1c201aed 100644 --- a/butane_core/src/db/sqlite.rs +++ b/butane_core/src/db/sqlite.rs @@ -112,10 +112,10 @@ impl ConnectionMethods for SQLiteConnection { fn execute(&self, sql: &str) -> Result<()> { ConnectionMethods::execute(self.wrapped_connection_methods()?, sql) } - fn query<'a, 'b, 'c>( + fn query<'a, 'c>( &'c self, table: &str, - columns: &'b [Column], + columns: &[Column], expr: Option, limit: Option, offset: Option, @@ -148,11 +148,11 @@ impl ConnectionMethods for SQLiteConnection { self.wrapped_connection_methods()? .insert_or_replace(table, columns, pkcol, values) } - fn update<'a>( + fn update( &self, table: &str, pkcol: Column, - pk: SqlValRef<'a>, + pk: SqlValRef<'_>, columns: &[Column], values: &[SqlValRef<'_>], ) -> Result<()> { @@ -196,10 +196,10 @@ impl ConnectionMethods for rusqlite::Connection { Ok(()) } - fn query<'b, 'c>( + fn query<'c>( &'c self, table: &str, - columns: &'b [Column], + columns: &[Column], expr: Option, limit: Option, offset: Option, @@ -384,10 +384,10 @@ impl ConnectionMethods for SqliteTransaction<'_> { fn execute(&self, sql: &str) -> Result<()> { ConnectionMethods::execute(self.wrapped_connection_methods()?, sql) } - fn query<'b, 'c>( + fn query<'c>( &'c self, table: &str, - columns: &'b [Column], + columns: &[Column], expr: Option, limit: Option, offset: Option, @@ -420,11 +420,11 @@ impl ConnectionMethods for SqliteTransaction<'_> { self.wrapped_connection_methods()? .insert_or_replace(table, columns, pkcol, values) } - fn update<'a>( + fn update( &self, table: &str, pkcol: Column, - pk: SqlValRef<'a>, + pk: SqlValRef<'_>, columns: &[Column], values: &[SqlValRef<'_>], ) -> Result<()> { From 753edaf997ab7380640b41c1f1fff9a5d1748275 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Wed, 2 Aug 2023 11:00:07 -0400 Subject: [PATCH 27/78] Re-enable cli test test_migrate_and_query --- example/tests/test_cli.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/example/tests/test_cli.rs b/example/tests/test_cli.rs index acb3b274..2de9211c 100644 --- a/example/tests/test_cli.rs +++ b/example/tests/test_cli.rs @@ -1,7 +1,5 @@ use assert_cmd::Command; -// todo either switch this to postgres or re-enable sqlite -/* #[test] fn test_migrate_and_query() { let db = "db.sqlite"; @@ -55,4 +53,3 @@ fn test_migrate_and_query() { .assert() .success(); } -*/ From 6ac8167ea4d39e1e0ec23a6389d99d398cf83db6 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Thu, 3 Aug 2023 15:52:39 -0400 Subject: [PATCH 28/78] Re-enable cargo check with sqlite --- Makefile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Makefile b/Makefile index d10b66f1..b70d7adb 100644 --- a/Makefile +++ b/Makefile @@ -6,8 +6,7 @@ build : # build some intermediate configuration to test different feature combinations cd butane && cargo check --features pg cd butane && cargo check --features pg,datetime - # async todo re-enable sqlite - # cd butane && cargo check --features sqlite + cd butane && cargo check --features sqlite cargo build --all-features lint : From aaa4301af50330fc34fb91b8f6705308ea526d63 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Thu, 3 Aug 2023 15:53:32 -0400 Subject: [PATCH 29/78] Re-enable sqlite in butane cli --- butane_cli/Cargo.toml | 6 ++---- example/Cargo.toml | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/butane_cli/Cargo.toml b/butane_cli/Cargo.toml index 799d59e7..cfedb25b 100644 --- a/butane_cli/Cargo.toml +++ b/butane_cli/Cargo.toml @@ -18,13 +18,11 @@ path = "src/main.rs" doc = false [features] -# todo re-enable sqlite -#sqlite-bundled = ["butane/sqlite-bundled"] +sqlite-bundled = ["butane/sqlite-bundled"] [dependencies] anyhow = "1.0" -# todo re-enable sqlite -butane = { features = ["default", "pg"], workspace = true } +butane = { features = ["default", "pg", "sqlite"], workspace = true } chrono = { workspace = true } clap = { version = "4.1" } quote = { workspace = true } diff --git a/example/Cargo.toml b/example/Cargo.toml index 407429e9..ccaecf14 100644 --- a/example/Cargo.toml +++ b/example/Cargo.toml @@ -7,8 +7,7 @@ publish = false build = "build.rs" [dependencies] -# todo re-enable sqlite -butane = { features = ["pg"], workspace = true } +butane = { features = ["pg", "sqlite"], workspace = true } tokio = { workspace = true, features = ["macros"] } [dev-dependencies] From 7e235c7935f9ed6b4245a66dae2b432dcc1f3b1c Mon Sep 17 00:00:00 2001 From: James Oakley Date: Thu, 3 Aug 2023 15:57:00 -0400 Subject: [PATCH 30/78] Re-enable sqlite in example --- examples/getting_started/Cargo.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/getting_started/Cargo.toml b/examples/getting_started/Cargo.toml index 09ffcc05..5b11061c 100644 --- a/examples/getting_started/Cargo.toml +++ b/examples/getting_started/Cargo.toml @@ -21,8 +21,7 @@ doc = false doc = false [dependencies] -# todo re-enable sqlite -butane = { features = ["default", "pg"], workspace = true } +butane = { features = ["default", "pg", "sqlite"], workspace = true } tokio = { workspace = true, features = ["macros"] } From 5e99cc280ff207a3566eb5f76e3f2c97bf2bb840 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Sun, 7 Apr 2024 16:23:26 -0400 Subject: [PATCH 31/78] Allow `Query` and `Many` to operate either sync or async This is achieved by introducing "Op" traits -- QueryOpSync, QueryOpAsync, ManyOpSync, ManyOpAsync --- butane/src/lib.rs | 10 +- butane/tests/fake.rs | 1 + butane_core/src/db/adapter.rs | 2 +- butane_core/src/lib.rs | 2 + butane_core/src/many.rs | 224 ++++++++++++------ butane_core/src/query/mod.rs | 121 +++++++--- .../getting_started/src/bin/delete_post.rs | 1 + .../getting_started/src/bin/show_posts.rs | 1 + 8 files changed, 265 insertions(+), 97 deletions(-) diff --git a/butane/src/lib.rs b/butane/src/lib.rs index 18976eba..bf064a86 100644 --- a/butane/src/lib.rs +++ b/butane/src/lib.rs @@ -163,9 +163,8 @@ macro_rules! colname { #[macro_export] macro_rules! find { ($dbobj:ident, $filter:expr, $conn:expr) => { - butane::query!($dbobj, $filter) - .limit(1) - .load($conn) + // todo sync version + butane::query::QueryOpAsync::load(butane::query!($dbobj, $filter).limit(1), $conn) .await .and_then(|mut results| results.pop().ok_or(butane::Error::NoSuchObject)) }; @@ -183,6 +182,11 @@ pub mod prelude { pub use crate::DataObject; #[doc(no_inline)] pub use crate::DataResult; + + // todo should it be sync or async in prelude + // (can't be both or call sites will give "multiple applicable items in scope" + pub use butane_core::many::ManyOpAsync; + pub use butane_core::query::QueryOpAsync; } pub mod internal { diff --git a/butane/tests/fake.rs b/butane/tests/fake.rs index 392ebd8b..da644e46 100644 --- a/butane/tests/fake.rs +++ b/butane/tests/fake.rs @@ -1,4 +1,5 @@ use butane::db::Connection; +use butane::prelude::*; use butane::{find, DataObject, ForeignKey}; use butane_test_helper::*; use fake::{Fake, Faker}; diff --git a/butane_core/src/db/adapter.rs b/butane_core/src/db/adapter.rs index e68287ce..f734aed7 100644 --- a/butane_core/src/db/adapter.rs +++ b/butane_core/src/db/adapter.rs @@ -233,7 +233,7 @@ impl AsyncAdapter { Self: Sized, F: FnOnce() -> Result + Send, { - // TOOD execute the create context function on the thread + // TODO execute the create context function on the thread let context = create_context()?; Ok(Self { env: Arc::new(AsyncAdapterEnv::new()), diff --git a/butane_core/src/lib.rs b/butane_core/src/lib.rs index bdff2054..2ecb1a70 100644 --- a/butane_core/src/lib.rs +++ b/butane_core/src/lib.rs @@ -90,6 +90,8 @@ pub trait DataObject: DataResult + Sync { where Self: Sized, { + // todo make sync and async variants + use crate::query::QueryOpAsync; Ok(::query() .filter(query::BoolExpr::Eq( Self::PKCOL, diff --git a/butane_core/src/many.rs b/butane_core/src/many.rs index 9767cda9..93619a1f 100644 --- a/butane_core/src/many.rs +++ b/butane_core/src/many.rs @@ -1,5 +1,6 @@ //! Implementation of many-to-many relationships between models. #![deny(missing_docs)] +use crate::db::sync::ConnectionMethods as ConnectionMethodsSync; use crate::db::{Column, ConnectionMethods}; use crate::query::{BoolExpr, Expr, OrderDirection, Query}; use crate::{sqlval::PrimaryKeyType, DataObject, Error, FieldType, Result, SqlType, SqlVal, ToSql}; @@ -21,6 +22,8 @@ fn default_oc() -> OnceCell> { /// many-to-many relationship with U, owner type is T::PKType, has is /// U::PKType. Table name is T_foo_Many where foo is the name of /// the Many field +/// +/// See [`ManyOpSync`] and [`ManyOpAsync`] for operations requiring a live database connection. // #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Many @@ -100,9 +103,141 @@ where .map(|v| v.iter()) } - // todo support save and load for sync too + /// Query the values referred to by this many relationship from the + /// database if necessary and returns a reference to them. + fn query(&self) -> Result> { + let owner: &SqlVal = match &self.owner { + Some(o) => o, + None => return Err(Error::NotInitialized), + }; + Ok(T::query().filter(BoolExpr::Subquery { + col: T::PKCOL, + tbl2: self.item_table.clone(), + tbl2_col: "has", + expr: Box::new(BoolExpr::Eq("owner", Expr::Val(owner.clone()))), + })) + } + + /// Describes the columns of the Many table + pub fn columns(&self) -> [Column; 2] { + [ + Column::new("owner", self.owner_type.clone()), + Column::new("has", ::SQLTYPE), + ] + } +} + +#[maybe_async_cfg::maybe( + idents(ConnectionMethods(sync = "ConnectionMethodsSync", async), QueryOp), + sync(), + async() +)] +/// Loads the values referred to by this many relationship from a +/// database query if necessary and returns a reference to them. +async fn load_query_uncached<'a, T: DataObject>( + many: &'a Many, + conn: &impl ConnectionMethods, + query: Query, +) -> Result> +where + T: 'a, +{ + use crate::query::QueryOp; + let mut vals: Vec = query.load(conn).await?; + // Now add in the values for things not saved to the db yet + if !many.new_values.is_empty() { + vals.append( + &mut T::query() + .filter(BoolExpr::In(T::PKCOL, many.new_values.clone())) + .load(conn) + .await?, + ); + } + Ok(vals) +} + +/// Loads the values referred to by this many relationship from a +/// database query if necessary and returns a reference to them. +async fn load_query_async<'a, T: DataObject>( + many: &'a Many, + conn: &impl ConnectionMethods, + query: Query, +) -> Result> +where + T: 'a, +{ + many.all_values + .get_or_try_init(|| load_query_uncached_async(many, conn, query)) + .await + .map(|v| v.iter()) +} + +/// Loads the values referred to by this many relationship from a +/// database query if necessary and returns a reference to them. +fn load_query_sync<'a, T: DataObject>( + many: &'a Many, + conn: &impl ConnectionMethodsSync, + query: Query, +) -> Result> +where + T: 'a, +{ + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + let future = many + .all_values + .get_or_try_init(|| async { load_query_uncached_sync(many, conn, query) }); + rt.block_on(future).map(|v| v.iter()) +} + +/// [`Many`] operations which require a `Connection` +#[allow(async_fn_in_trait)] // Not intended to be implemented outside Butane +#[maybe_async_cfg::maybe( + idents(ConnectionMethods(sync = "ConnectionMethodsSync", async),), + sync(), + async() +)] +pub trait ManyOp { /// Used by macro-generated code. You do not need to call this directly. - pub async fn save(&mut self, conn: &impl crate::ConnectionMethods) -> Result<()> { + async fn save(&mut self, conn: &impl ConnectionMethods) -> Result<()>; + + /// Delete all references from the database, and any unsaved additions. + async fn delete(&mut self, conn: &impl ConnectionMethods) -> Result<()>; + + /// Loads the values referred to by this many relationship from the + /// database if necessary and returns a reference to them. + async fn load<'a>( + &'a self, + conn: &impl ConnectionMethods, + ) -> Result> + where + T: 'a; + + /// Loads and orders the values referred to by this many relationship from a + /// database if necessary and returns a reference to them. + async fn load_ordered<'a>( + &'a self, + conn: &impl ConnectionMethods, + order: OrderDirection, + ) -> Result> + where + T: 'a; +} + +#[maybe_async_cfg::maybe( + idents( + ConnectionMethods(sync = "ConnectionMethodsSync", async), + ManyOpInternal, + ManyOp, + load_query(sync = "load_query_sync", async = "load_query_async"), + ), + keep_self, + sync(), + async() +)] +impl ManyOp for Many { + async fn save(&mut self, conn: &impl ConnectionMethods) -> Result<()> { let owner = self.owner.as_ref().ok_or(Error::NotInitialized)?; while !self.new_values.is_empty() { conn.insert_only( @@ -126,8 +261,7 @@ where Ok(()) } - /// Delete all references from the database, and any unsaved additions. - pub async fn delete(&mut self, conn: &impl ConnectionMethods) -> Result<()> { + async fn delete(&mut self, conn: &impl ConnectionMethods) -> Result<()> { let owner = self.owner.as_ref().ok_or(Error::NotInitialized)?; conn.delete_where( &self.item_table, @@ -141,88 +275,46 @@ where Ok(()) } - /// Loads the values referred to by this many relationship from the - /// database if necessary and returns a reference to them. - pub async fn load(&self, conn: &impl ConnectionMethods) -> Result> { + async fn load<'a>( + &'a self, + conn: &impl ConnectionMethods, + ) -> Result> + where + T: 'a, + { let query = self.query(); // If not initialised then there are no values let vals: Result> = if query.is_err() { Ok(Vec::new()) } else { - Ok(self.load_query(conn, query.unwrap()).await?.collect()) + Ok(load_query(self, conn, query.unwrap()).await?.collect()) }; vals.map(|v| v.into_iter()) } - /// Query the values referred to by this many relationship from the - /// database if necessary and returns a reference to them. - fn query(&self) -> Result> { - let owner: &SqlVal = match &self.owner { - Some(o) => o, - None => return Err(Error::NotInitialized), - }; - Ok(T::query().filter(BoolExpr::Subquery { - col: T::PKCOL, - tbl2: self.item_table.clone(), - tbl2_col: "has", - expr: Box::new(BoolExpr::Eq("owner", Expr::Val(owner.clone()))), - })) - } - - /// Loads the values referred to by this many relationship from a - /// database query if necessary and returns a reference to them. - async fn load_query( - &self, - conn: &impl ConnectionMethods, - query: Query, - ) -> Result> { - let vals: Result<&Vec> = self - .all_values - .get_or_try_init(|| async { - let mut vals: Vec = query.load(conn).await?; - // Now add in the values for things not saved to the db yet - if !self.new_values.is_empty() { - vals.append( - &mut T::query() - .filter(BoolExpr::In(T::PKCOL, self.new_values.clone())) - .load(conn) - .await?, - ); - } - Ok(vals) - }) - .await; - vals.map(|v| v.iter()) - } - - /// Loads and orders the values referred to by this many relationship from a - /// database if necessary and returns a reference to them. - pub async fn load_ordered( - &self, + async fn load_ordered<'a>( + &'a self, conn: &impl ConnectionMethods, order: OrderDirection, - ) -> Result> { + ) -> Result> + where + T: 'a, + { let query = self.query(); // If not initialised then there are no values let vals: Result> = if query.is_err() { Ok(Vec::new()) } else { - Ok(self - .load_query(conn, query.unwrap().order(T::PKCOL, order)) - .await? - .collect()) + Ok( + load_query(self, conn, query.unwrap().order(T::PKCOL, order)) + .await? + .collect(), + ) }; vals.map(|v| v.into_iter()) } - - /// Describes the columns of the Many table - pub fn columns(&self) -> [Column; 2] { - [ - Column::new("owner", self.owner_type.clone()), - Column::new("has", ::SQLTYPE), - ] - } } + impl PartialEq> for Many { fn eq(&self, other: &Many) -> bool { (self.owner == other.owner) && (self.item_table == other.item_table) diff --git a/butane_core/src/query/mod.rs b/butane_core/src/query/mod.rs index 2acad3bb..28b2404f 100644 --- a/butane_core/src/query/mod.rs +++ b/butane_core/src/query/mod.rs @@ -9,6 +9,7 @@ use std::marker::PhantomData; use fallible_iterator::FallibleIterator; +use crate::db::sync::ConnectionMethods as ConnectionMethodsSync; use crate::db::{BackendRows, ConnectionMethods, QueryResult}; use crate::{DataResult, Result, SqlVal}; @@ -117,6 +118,7 @@ impl Column { } /// Representation of a database query. +/// See [`QueryOpSync`] and [`QueryOpAsync`] for operations requiring a live database connection. #[derive(Clone, Debug)] pub struct Query { table: TblName, @@ -181,41 +183,106 @@ impl Query { pub fn order_desc(self, column: &'static str) -> Query { self.order(column, OrderDirection::Descending) } +} + +mod private { + use super::*; - async fn fetch( - self, - conn: &impl ConnectionMethods, - limit: Option, - ) -> Result> { - let sort = if self.sort.is_empty() { - None - } else { - Some(self.sort.as_slice()) - }; - conn.query( - &self.table, - T::COLUMNS, - self.filter, - limit, - self.offset, - sort, - ) - .await + /// Internal QueryOp helpers + #[allow(async_fn_in_trait)] // Not truly a public trait + #[maybe_async_cfg::maybe( + idents(ConnectionMethods(sync = "ConnectionMethodsSync", async)), + sync(), + async() + )] + pub trait QueryOpInternal { + async fn fetch( + self, + conn: &impl ConnectionMethods, + limit: Option, + ) -> Result>; } + #[maybe_async_cfg::maybe( + idents( + ConnectionMethods(sync = "ConnectionMethodsSync", async), + QueryOpInternal + ), + keep_self, + sync(), + async() + )] + impl QueryOpInternal for Query { + async fn fetch( + self, + conn: &impl ConnectionMethods, + limit: Option, + ) -> Result> { + let sort = if self.sort.is_empty() { + None + } else { + Some(self.sort.as_slice()) + }; + conn.query( + &self.table, + T::COLUMNS, + self.filter, + limit, + self.offset, + sort, + ) + .await + } + } +} +use private::QueryOpInternalAsync; +use private::QueryOpInternalSync; +/// [`Query`] operations which require a `Connection` +#[allow(async_fn_in_trait)] // Not intended to be implemented outside Butane +#[maybe_async_cfg::maybe( + idents( + ConnectionMethods(sync = "ConnectionMethodsSync", async), + QueryOpInternal + ), + sync(), + async() +)] +pub trait QueryOp: QueryOpInternal { /// Executes the query against `conn` and returns the first result (if any). - pub async fn load_first(self, conn: &impl ConnectionMethods) -> Result> { - self.fetch(conn, Some(1)).await?.mapped(T::from_row).nth(0) - } + async fn load_first(self, conn: &impl ConnectionMethods) -> Result>; /// Executes the query against `conn`. - pub async fn load(self, conn: &impl ConnectionMethods) -> Result> { - let limit = self.limit.to_owned(); - self.fetch(conn, limit).await?.mapped(T::from_row).collect() - } + async fn load(self, conn: &impl ConnectionMethods) -> Result>; /// Executes the query against `conn` and deletes all matching objects. - pub async fn delete(self, conn: &impl ConnectionMethods) -> Result { + async fn delete(self, conn: &impl ConnectionMethods) -> Result; +} + +#[maybe_async_cfg::maybe( + idents( + ConnectionMethods(sync = "ConnectionMethodsSync", async), + QueryOp, + QueryOpInternal + ), + keep_self, + sync(), + async() +)] +impl QueryOp for Query { + async fn load_first(self, conn: &impl ConnectionMethods) -> Result> { + QueryOpInternal::fetch(self, conn, Some(1)) + .await? + .mapped(T::from_row) + .nth(0) + } + async fn load(self, conn: &impl ConnectionMethods) -> Result> { + let limit = self.limit.to_owned(); + QueryOpInternal::fetch(self, conn, limit) + .await? + .mapped(T::from_row) + .collect() + } + async fn delete(self, conn: &impl ConnectionMethods) -> Result { conn.delete_where(&self.table, self.filter.unwrap_or(BoolExpr::True)) .await } diff --git a/examples/getting_started/src/bin/delete_post.rs b/examples/getting_started/src/bin/delete_post.rs index 68a5000c..c8a167ca 100644 --- a/examples/getting_started/src/bin/delete_post.rs +++ b/examples/getting_started/src/bin/delete_post.rs @@ -1,5 +1,6 @@ use std::env::args; +use butane::prelude::*; use butane::query; use getting_started::models::Post; use getting_started::*; diff --git a/examples/getting_started/src/bin/show_posts.rs b/examples/getting_started/src/bin/show_posts.rs index 2856565f..46f6fcb6 100644 --- a/examples/getting_started/src/bin/show_posts.rs +++ b/examples/getting_started/src/bin/show_posts.rs @@ -1,3 +1,4 @@ +use butane::prelude::*; use butane::query; use getting_started::models::Post; use getting_started::*; From 52eaffb4ef85adf778036c428feb60cf28f9bcfc Mon Sep 17 00:00:00 2001 From: James Oakley Date: Sun, 7 Apr 2024 21:36:05 -0400 Subject: [PATCH 32/78] Add async_checklist.md --- async_checklist.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 async_checklist.md diff --git a/async_checklist.md b/async_checklist.md new file mode 100644 index 00000000..feeab71c --- /dev/null +++ b/async_checklist.md @@ -0,0 +1,10 @@ +* [] Fully support sync too. Using async should not be required +* [] Clean up pattern for sync/async variants. Inconsistent between suffix and module +* [] Tests should run against sync and async +* [] Establish soundness for unsafe sections of AsyncAdapter +* [] Consider publishing `AsyncAdapter` into its own crate +* [] Ensure Postgres works in sync +* [] Ensure sqlite works in async (might already be done) +* [] Re-enable R2D2 for sync and find async alternative (deadpool) +* [] Fix `#[async_trait(?Send)]` to set up Send bound again as it's required for e.g. `tokio::spawn` + From aa8e59d6472177007bc580c314d7bbcefe64dc7f Mon Sep 17 00:00:00 2001 From: James Oakley Date: Sun, 7 Apr 2024 21:39:03 -0400 Subject: [PATCH 33/78] better rendering of checklist --- async_checklist.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/async_checklist.md b/async_checklist.md index feeab71c..adf4b90a 100644 --- a/async_checklist.md +++ b/async_checklist.md @@ -1,10 +1,10 @@ -* [] Fully support sync too. Using async should not be required -* [] Clean up pattern for sync/async variants. Inconsistent between suffix and module -* [] Tests should run against sync and async -* [] Establish soundness for unsafe sections of AsyncAdapter -* [] Consider publishing `AsyncAdapter` into its own crate -* [] Ensure Postgres works in sync -* [] Ensure sqlite works in async (might already be done) -* [] Re-enable R2D2 for sync and find async alternative (deadpool) -* [] Fix `#[async_trait(?Send)]` to set up Send bound again as it's required for e.g. `tokio::spawn` +* [ ] Fully support sync too. Using async should not be required +* [ ] Clean up pattern for sync/async variants. Inconsistent between suffix and module +* [ ] Tests should run against sync and async +* [ ] Establish soundness for unsafe sections of AsyncAdapter +* [ ] Consider publishing `AsyncAdapter` into its own crate +* [ ] Ensure Postgres works in sync +* [ ] Ensure sqlite works in async (might already be done) +* [ ] Re-enable R2D2 for sync and find async alternative (deadpool) +* [ ] Fix `#[async_trait(?Send)]` to set up Send bound again as it's required for e.g. `tokio::spawn` From c7805cf7690a68d4ad0fd6c225565f8cba29df35 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Mon, 8 Apr 2024 21:29:24 -0400 Subject: [PATCH 34/78] Allow choice of sync or async for DataObject load/save methods --- butane/src/lib.rs | 7 ++- butane/tests/fake.rs | 2 +- butane_core/src/codegen/dbobj.rs | 57 ++++++++++++++-------- butane_core/src/lib.rs | 43 ++++++++++++---- butane_core/src/migrations/mod.rs | 14 +++--- examples/getting_started/tests/rollback.rs | 2 +- examples/newtype/tests/rollback.rs | 2 +- 7 files changed, 84 insertions(+), 43 deletions(-) diff --git a/butane/src/lib.rs b/butane/src/lib.rs index 4d59e7e0..291160b6 100644 --- a/butane/src/lib.rs +++ b/butane/src/lib.rs @@ -9,13 +9,15 @@ pub use butane_codegen::{butane_type, dataresult, model, FieldType}; pub use butane_core::custom; pub use butane_core::fkey::ForeignKey; -pub use butane_core::many::Many; +pub use butane_core::many::{Many, ManyOpAsync, ManyOpSync}; pub use butane_core::migrations; pub use butane_core::query; pub use butane_core::{ AsPrimaryKey, AutoPk, DataObject, DataResult, Error, FieldType, FromSql, PrimaryKeyType, Result, SqlType, SqlVal, SqlValRef, ToSql, }; +// todo put ops in a separate package? +pub use butane_core::{DataObjectOpAsync, DataObjectOpSync}; pub mod db { //! Database helpers @@ -69,7 +71,7 @@ pub mod db { /// rank: i32, /// nationality: String /// } -/// # tokio_test::block_on(async { +/// # tokio_test::block_opn(async { /// let e: BoolExpr = filter!(Contestant, nationality == "US" && rank < 42); /// let first_place = 1; /// let e2 = filter!(Contestant, rank == { first_place }); @@ -187,6 +189,7 @@ pub mod prelude { // (can't be both or call sites will give "multiple applicable items in scope" pub use butane_core::many::ManyOpAsync; pub use butane_core::query::QueryOpAsync; + pub use butane_core::DataObjectOpAsync; } pub mod internal { diff --git a/butane/tests/fake.rs b/butane/tests/fake.rs index da644e46..8040599e 100644 --- a/butane/tests/fake.rs +++ b/butane/tests/fake.rs @@ -1,6 +1,6 @@ use butane::db::Connection; use butane::prelude::*; -use butane::{find, DataObject, ForeignKey}; +use butane::{find, ForeignKey}; use butane_test_helper::*; use fake::{Fake, Faker}; diff --git a/butane_core/src/codegen/dbobj.rs b/butane_core/src/codegen/dbobj.rs index e7ccb7bf..9ecede87 100644 --- a/butane_core/src/codegen/dbobj.rs +++ b/butane_core/src/codegen/dbobj.rs @@ -37,24 +37,8 @@ pub fn impl_dbobject(ast_struct: &ItemStruct, config: &Config) -> TokenStream2 { let values_no_pk: Vec = push_values(ast_struct, |f: &Field| f != &pk_field); let insert_cols = columns(ast_struct, |f| !is_auto(f)); - let many_save: TokenStream2 = fields(ast_struct) - .filter(|f| is_many_to_many(f)) - .map(|f| { - let ident = f.ident.clone().expect("Fields must be named for butane"); - let many_table_lit = many_table_lit(ast_struct, f, config); - let pksqltype = - quote!(<::PKType as butane::FieldType>::SQLTYPE); - // Save needs to ensure_initialized - quote!( - self.#ident.ensure_init( - #many_table_lit, - butane::ToSql::to_sql(self.pk()), - #pksqltype, - ); - self.#ident.save(conn).await?; - ) - }) - .collect(); + let many_save_async = impl_many_save(ast_struct, config, true); + let many_save_sync = impl_many_save(ast_struct, config, false); let dataresult = impl_dataresult(ast_struct, tyname, config); // Note the many impls following DataObject can not be generic because they implement for T and &T, @@ -63,7 +47,6 @@ pub fn impl_dbobject(ast_struct: &ItemStruct, config: &Config) -> TokenStream2 { quote!( #dataresult - #[butane::internal::async_trait(?Send)] impl butane::internal::DataObjectInternal for #tyname { const NON_AUTO_COLUMNS: &'static [butane::db::Column] = &[ #insert_cols @@ -72,8 +55,12 @@ pub fn impl_dbobject(ast_struct: &ItemStruct, config: &Config) -> TokenStream2 { fn pk_mut(&mut self) -> &mut impl butane::PrimaryKeyType { &mut self.#pkident } - async fn save_many_to_many(&mut self, conn: &impl butane::db::ConnectionMethods) -> butane::Result<()> { - #many_save + async fn save_many_to_many_async(&mut self, conn: &impl butane::db::ConnectionMethods) -> butane::Result<()> { + #many_save_async + Ok(()) + } + fn save_many_to_many_sync(&mut self, conn: &impl butane::db::sync::ConnectionMethods) -> butane::Result<()> { + #many_save_sync Ok(()) } fn values(&self, include_pk: bool) -> Vec { @@ -422,3 +409,31 @@ where }) .collect() } + +fn impl_many_save(ast_struct: &ItemStruct, config: &Config, is_async: bool) -> TokenStream2 { + return fields(ast_struct) + .filter(|f| is_many_to_many(f)) + .map(|f| { + let ident = f.ident.clone().expect("Fields must be named for butane"); + let many_table_lit = many_table_lit(ast_struct, f, config); + let pksqltype = + quote!(<::PKType as butane::FieldType>::SQLTYPE); + + let save_with_conn = if is_async { + quote!(butane::ManyOpAsync::save(&mut self.#ident, conn).await?;) + } else { + quote!(butane::ManyOpSync::save(&mut self.#ident, conn)?;) + }; + + // Save needs to ensure_initialized + quote!( + self.#ident.ensure_init( + #many_table_lit, + butane::ToSql::to_sql(self.pk()), + #pksqltype, + ); + #save_with_conn + ) + }) + .collect(); +} diff --git a/butane_core/src/lib.rs b/butane_core/src/lib.rs index 9018dc80..75cf6e79 100644 --- a/butane_core/src/lib.rs +++ b/butane_core/src/lib.rs @@ -7,7 +7,6 @@ use std::borrow::Borrow; use std::cmp::{Eq, PartialEq}; -use async_trait::async_trait; use serde::{Deserialize, Serialize}; use thiserror::Error as ThisError; @@ -27,6 +26,7 @@ pub mod uuid; mod autopk; pub use autopk::AutoPk; use custom::SqlTypeCustom; +use db::sync::ConnectionMethods as ConnectionMethodsSync; use db::{BackendRow, Column, ConnectionMethods}; pub use query::Query; pub use sqlval::{AsPrimaryKey, FieldType, FromSql, PrimaryKeyType, SqlVal, SqlValRef, ToSql}; @@ -41,7 +41,6 @@ pub type Result = std::result::Result; /// object type. The purpose of a result type which is not also an /// object type is to allow a query to retrieve a subset of an /// object's columns. -#[async_trait] pub trait DataResult: Sized { /// Corresponding object type. type DBO: DataObject; @@ -66,7 +65,7 @@ pub mod internal { /// Methods implemented by Butane codegen and called by other /// parts of Butane. You do not need to call these directly /// WARNING: Semver exempt - #[async_trait(?Send)] + #[allow(async_fn_in_trait)] // Not really a public trait pub trait DataObjectInternal: DataResult { /// Like [DataResult::COLUMNS] but omits [AutoPk]. const NON_AUTO_COLUMNS: &'static [Column]; @@ -76,7 +75,11 @@ pub mod internal { /// Saves many-to-many relationships pointed to by fields on this model. /// Performed automatically by `save`. You do not need to call this directly. - async fn save_many_to_many(&mut self, conn: &impl ConnectionMethods) -> Result<()>; + async fn save_many_to_many_async(&mut self, conn: &impl ConnectionMethods) -> Result<()>; + + /// Saves many-to-many relationships pointed to by fields on this model. + /// Performed automatically by `save`. You do not need to call this directly. + fn save_many_to_many_sync(&mut self, conn: &impl ConnectionMethodsSync) -> Result<()>; /// Returns the Sql values of all columns. Used internally. You are /// unlikely to need to call this directly. @@ -88,7 +91,7 @@ pub mod internal { /// /// Rather than implementing this type manually, use the /// `#[model]` attribute. -#[async_trait(?Send)] +#[allow(async_fn_in_trait)] // Implementation is intended to be through procmacro pub trait DataObject: DataResult + internal::DataObjectInternal + Sync { /// The type of the primary key field. type PKType: PrimaryKeyType; @@ -133,9 +136,24 @@ pub trait DataObject: DataResult + internal::DataObjectInternal + Sy .into_iter() .nth(0)) } +} +/// [`DataObject`] operations that require a live database connection. +#[allow(async_fn_in_trait)] // Implementation is intended to be through procmacro +#[maybe_async_cfg::maybe( + idents( + ConnectionMethods(sync = "ConnectionMethodsSync", async), + save_many_to_many(sync = "save_many_to_many_sync", async = "save_many_to_many_async") + ), + sync(), + async() +)] +pub trait DataObjectOp { /// Save the object to the database. - async fn save(&mut self, conn: &impl ConnectionMethods) -> Result<()> { + async fn save(&mut self, conn: &impl ConnectionMethods) -> Result<()> + where + Self: DataObject, + { let pkcol = Column::new(Self::PKCOL, ::SQLTYPE); if Self::AUTO_PK && ::COLUMNS.len() == 1 { @@ -182,18 +200,23 @@ pub trait DataObject: DataResult + internal::DataObjectInternal + Sy .await?; } - self.save_many_to_many(conn).await?; + Self::save_many_to_many(self, conn).await?; Ok(()) } /// Delete the object from the database. - async fn delete(&self, conn: &impl ConnectionMethods) -> Result<()> { - conn.delete(Self::TABLE, Self::PKCOL, self.pk().to_sql()) - .await + async fn delete(&self, conn: &impl ConnectionMethods) -> Result<()> + where + Self: DataObject, + { + conn.delete(T::TABLE, T::PKCOL, self.pk().to_sql()).await } } +impl DataObjectOpSync for T where T: DataObject {} +impl DataObjectOpAsync for T where T: DataObject {} + /// ASYNC TODO is this still necessary pub trait ModelTyped { /// ASYNC TODO diff --git a/butane_core/src/migrations/mod.rs b/butane_core/src/migrations/mod.rs index 27e190e5..2369d1d5 100644 --- a/butane_core/src/migrations/mod.rs +++ b/butane_core/src/migrations/mod.rs @@ -308,7 +308,6 @@ impl DataResult for ButaneMigration { } } -#[async_trait(?Send)] impl DataObject for ButaneMigration { type PKType = String; type Fields = (); // we don't need Fields as we never filter @@ -318,13 +317,8 @@ impl DataObject for ButaneMigration { fn pk(&self) -> &String { &self.name } - async fn delete(&self, conn: &impl ConnectionMethods) -> Result<()> { - conn.delete(Self::TABLE, Self::PKCOL, self.pk().to_sql()) - .await - } } -#[async_trait(?Send)] impl crate::internal::DataObjectInternal for ButaneMigration { const NON_AUTO_COLUMNS: &'static [Column] = Self::COLUMNS; fn pk_mut(&mut self) -> &mut impl PrimaryKeyType { @@ -337,7 +331,13 @@ impl crate::internal::DataObjectInternal for ButaneMigration { } values } - async fn save_many_to_many(&mut self, _conn: &impl ConnectionMethods) -> Result<()> { + async fn save_many_to_many_async(&mut self, _conn: &impl ConnectionMethods) -> Result<()> { + Ok(()) // no-op + } + fn save_many_to_many_sync( + &mut self, + _conn: &impl crate::db::sync::ConnectionMethods, + ) -> Result<()> { Ok(()) // no-op } } diff --git a/examples/getting_started/tests/rollback.rs b/examples/getting_started/tests/rollback.rs index e6a29ad7..2da3fd85 100644 --- a/examples/getting_started/tests/rollback.rs +++ b/examples/getting_started/tests/rollback.rs @@ -1,6 +1,6 @@ use butane::db::{BackendConnection, Connection}; use butane::migrations::{Migration, Migrations}; -use butane::DataObject; +use butane::DataObjectOpAsync; use butane_test_helper::*; use getting_started::models::{Blog, Post, Tag}; diff --git a/examples/newtype/tests/rollback.rs b/examples/newtype/tests/rollback.rs index 1bc8a7ad..4532a8dd 100644 --- a/examples/newtype/tests/rollback.rs +++ b/examples/newtype/tests/rollback.rs @@ -1,6 +1,6 @@ use butane::db::{BackendConnection, Connection}; use butane::migrations::{Migration, Migrations}; -use butane::DataObject; +use butane::DataObjectOpAsync; use butane_test_helper::*; use newtype::models::{Blog, Post, Tags}; From e6314e7f84c1ba0a458851aade64727ae3e67f94 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Sun, 14 Jul 2024 21:13:19 -0400 Subject: [PATCH 35/78] Migrations are now sync instead of async --- async_checklist.md | 2 + butane_cli/src/lib.rs | 24 +- butane_core/src/db/adapter.rs | 20 +- butane_core/src/db/connmethods.rs | 6 +- butane_core/src/db/dummy.rs | 140 ++++++++++++ butane_core/src/db/mod.rs | 279 +++++++++++++++++++++++- butane_core/src/db/pg.rs | 2 +- butane_core/src/db/sync_adapter.rs | 186 ++++++++++++++++ butane_core/src/lib.rs | 4 +- butane_core/src/migrations/migration.rs | 27 +-- butane_core/src/migrations/mod.rs | 50 +++-- butane_test_helper/src/lib.rs | 22 +- example/src/main.rs | 2 +- examples/newtype/src/lib.rs | 11 +- 14 files changed, 699 insertions(+), 76 deletions(-) create mode 100644 butane_core/src/db/dummy.rs create mode 100644 butane_core/src/db/sync_adapter.rs diff --git a/async_checklist.md b/async_checklist.md index adf4b90a..6499aacd 100644 --- a/async_checklist.md +++ b/async_checklist.md @@ -7,4 +7,6 @@ * [ ] Ensure sqlite works in async (might already be done) * [ ] Re-enable R2D2 for sync and find async alternative (deadpool) * [ ] Fix `#[async_trait(?Send)]` to set up Send bound again as it's required for e.g. `tokio::spawn` +* [ ] Separate sync and async examples +* [ ] Should async_adapter be under a separate feature? Do we need it for migrations? diff --git a/butane_cli/src/lib.rs b/butane_cli/src/lib.rs index 5c7bff51..0beee948 100644 --- a/butane_cli/src/lib.rs +++ b/butane_cli/src/lib.rs @@ -56,14 +56,14 @@ pub fn default_name() -> String { } pub async fn init(base_dir: &PathBuf, name: &str, connstr: &str, connect: bool) -> Result<()> { - if db::get_backend(name).is_none() { + if db::get_async_backend(name).is_none() { eprintln!("Unknown backend {name}"); std::process::exit(1); }; let spec = db::ConnectionSpec::new(name, connstr); if connect { - db::connect(&spec).await?; + db::connect_async(&spec).await?; } std::fs::create_dir_all(base_dir)?; spec.save(base_dir)?; @@ -221,7 +221,7 @@ pub fn describe_migration(base_dir: &Path, name: &String) -> Result<()> { /// Detach the latest migration from the list of migrations, /// leaving the migration on the filesystem. -pub async fn detach_latest_migration(base_dir: &PathBuf) -> Result<()> { +pub fn detach_latest_migration(base_dir: &PathBuf) -> Result<()> { let mut ms = get_migrations(base_dir)?; let all_migrations = ms.all_migrations().unwrap_or_else(|e| { eprintln!("Error: {e}"); @@ -237,8 +237,8 @@ pub async fn detach_latest_migration(base_dir: &PathBuf) -> Result<()> { std::process::exit(1); } if let Ok(spec) = db::ConnectionSpec::load(base_dir) { - let conn = db::connect(&spec).await?; - if let Some(top_applied_migration) = ms.last_applied_migration(&conn).await? { + let conn = db::connect(&spec)?; + if let Some(top_applied_migration) = ms.last_applied_migration(&conn)? { if top_applied_migration == top_migration { eprintln!("Can not detach an applied migration"); std::process::exit(1); @@ -261,7 +261,7 @@ pub async fn detach_latest_migration(base_dir: &PathBuf) -> Result<()> { pub async fn migrate(base_dir: &PathBuf, name: Option) -> Result<()> { let spec = load_connspec(base_dir)?; - let mut conn = db::connect(&spec).await?; + let mut conn = db::connect_async(&spec).await?; let to_apply = get_migrations(base_dir)? .unapplied_migrations(&conn) .await?; @@ -281,7 +281,7 @@ pub async fn migrate(base_dir: &PathBuf, name: Option) -> Result<()> { pub async fn rollback(base_dir: &PathBuf, name: Option) -> Result<()> { let spec = load_connspec(base_dir)?; - let conn = butane::db::connect(&spec).await?; + let conn = butane::db::connect_async(&spec).await?; match name { Some(to) => rollback_to(base_dir, conn, &to).await, @@ -439,8 +439,8 @@ pub fn add_backend(base_dir: &Path, backend_name: &str) -> Result<()> { } } - let backend = - db::get_backend(backend_name).ok_or(anyhow::anyhow!("Backend {backend_name} not found"))?; + let backend = db::get_async_backend(backend_name) + .ok_or(anyhow::anyhow!("Backend {backend_name} not found"))?; let migrations = get_migrations(base_dir)?; let migration_list = migrations.all_migrations()?; @@ -478,8 +478,8 @@ pub fn remove_backend(base_dir: &Path, backend_name: &str) -> Result<()> { return Err(anyhow::anyhow!("Can not remove the last backend.")); } - let backend = - db::get_backend(backend_name).ok_or(anyhow::anyhow!("Backend {backend_name} not found"))?; + let backend = db::get_async_backend(backend_name) + .ok_or(anyhow::anyhow!("Backend {backend_name} not found"))?; let migrations = get_migrations(base_dir)?; let migration_list = migrations.all_migrations()?; @@ -543,7 +543,7 @@ pub fn load_latest_migration_backends(base_dir: &Path) -> Result AsyncAdapter +where + T: sync::BackendConnection + 'static, +{ + pub fn into_connection(self) -> Connection { + Connection { + conn: Box::new(self), + } + } +} + #[async_trait(?Send)] impl BackendTransaction<'c> for AsyncAdapter where @@ -418,10 +433,7 @@ impl Backend for BackendAdapter { tokio::task::spawn_blocking(move || { let connmethods_async = adapter::AsyncAdapter::new(|| sync_backend.connect(&conn_str2))?; - let conn = Connection { - conn: Box::new(connmethods_async), - }; - Ok(conn) + Ok(connmethods_async.into_connection()) }) .await? } diff --git a/butane_core/src/db/connmethods.rs b/butane_core/src/db/connmethods.rs index 56223e2e..67bb2ea5 100644 --- a/butane_core/src/db/connmethods.rs +++ b/butane_core/src/db/connmethods.rs @@ -160,7 +160,8 @@ impl VecRows { VecRows { rows, idx: 0 } } } -#[cfg(feature = "async-adapter")] +//todo should we still have this feature +//#[cfg(feature = "async-adapter")] pub(crate) fn vec_from_backend_rows<'a>( mut other: Box, columns: &[Column], @@ -201,7 +202,8 @@ pub(crate) struct VecRow { values: Vec, } -#[cfg(feature = "async-adapter")] +//todo should this still be a separate feature +//#[cfg(feature = "async-adapter")] impl VecRow { fn new(original: &(dyn BackendRow), columns: &[Column]) -> Result { if original.len() != columns.len() { diff --git a/butane_core/src/db/dummy.rs b/butane_core/src/db/dummy.rs new file mode 100644 index 00000000..8c9eba72 --- /dev/null +++ b/butane_core/src/db/dummy.rs @@ -0,0 +1,140 @@ +#![allow(unused)] +// TODO needs module comment + +use async_trait::async_trait; + +use super::connmethods::sync::ConnectionMethods as ConnectionMethodsSync; +use super::sync::Backend as BackendSync; +use super::sync::BackendConnection as BackendConnectionSync; +use super::sync::Connection as ConnectionSync; +use super::sync::Transaction as TransactionSync; +use super::Transaction as TransactionAsync; +use super::{ + connmethods::Column, Backend, BackendConnection, Connection, ConnectionMethods, RawQueryResult, + Transaction, +}; +use crate::migrations::adb; +use crate::query::{BoolExpr, Order}; +use crate::{Error, Result, SqlVal, SqlValRef}; +use BackendConnection as BackendConnectionAsync; + +#[derive(Clone, Debug)] +struct DummyBackend {} + +#[maybe_async_cfg::maybe( + idents( + Backend(sync = "BackendSync", async), + Connection(sync = "ConnectionSync", async) + ), + keep_self, + sync(), + async() +)] +#[async_trait] +impl Backend for DummyBackend { + fn name(&self) -> &'static str { + "dummy" + } + fn create_migration_sql(&self, current: &adb::ADB, ops: Vec) -> Result { + Err(Error::PoisonedConnection) + } + async fn connect(&self, conn_str: &str) -> Result { + Err(Error::PoisonedConnection) + } +} + +#[derive(Clone, Debug)] +pub(crate) struct DummyConnection {} +impl DummyConnection { + pub fn new() -> Self { + Self {} + } +} + +#[maybe_async_cfg::maybe( + idents(ConnectionMethods(sync = "ConnectionMethodsSync", async)), + keep_self, + sync(), + async() +)] +#[async_trait(?Send)] +impl ConnectionMethods for DummyConnection { + async fn execute(&self, sql: &str) -> Result<()> { + Err(Error::PoisonedConnection) + } + async fn query<'c>( + &'c self, + table: &str, + columns: &[Column], + expr: Option, + limit: Option, + offset: Option, + sort: Option<&[Order]>, + ) -> Result> { + Err(Error::PoisonedConnection) + } + async fn insert_returning_pk( + &self, + table: &str, + columns: &[Column], + pkcol: &Column, + values: &[SqlValRef<'_>], + ) -> Result { + Err(Error::PoisonedConnection) + } + async fn insert_only( + &self, + table: &str, + columns: &[Column], + values: &[SqlValRef<'_>], + ) -> Result<()> { + Err(Error::PoisonedConnection) + } + async fn insert_or_replace( + &self, + table: &str, + columns: &[Column], + pkcol: &Column, + values: &[SqlValRef<'_>], + ) -> Result<()> { + Err(Error::PoisonedConnection) + } + async fn update( + &self, + table: &str, + pkcol: Column, + pk: SqlValRef<'_>, + columns: &[Column], + values: &[SqlValRef<'_>], + ) -> Result<()> { + Err(Error::PoisonedConnection) + } + async fn delete_where(&self, table: &str, expr: BoolExpr) -> Result { + Err(Error::PoisonedConnection) + } + async fn has_table(&self, table: &str) -> Result { + Err(Error::PoisonedConnection) + } +} + +#[maybe_async_cfg::maybe( + idents(Backend(sync = "BackendSync", async), BackendConnection, Transaction), + keep_self, + sync(), + async() +)] +#[async_trait(?Send)] +impl BackendConnection for DummyConnection { + async fn transaction(&mut self) -> Result> { + Err(Error::PoisonedConnection) + } + fn backend(&self) -> Box { + Box::new(DummyBackend {}) + } + fn backend_name(&self) -> &'static str { + "dummy" + } + fn is_closed(&self) -> bool { + true + } +} diff --git a/butane_core/src/db/mod.rs b/butane_core/src/db/mod.rs index 07de6a39..f142092a 100644 --- a/butane_core/src/db/mod.rs +++ b/butane_core/src/db/mod.rs @@ -25,11 +25,16 @@ use async_trait::async_trait; use dyn_clone::DynClone; use serde::{Deserialize, Serialize}; -use crate::query::BoolExpr; +use crate::query::{BoolExpr, Order}; use crate::{migrations::adb, Error, Result, SqlVal, SqlValRef}; -#[cfg(feature = "async-adapter")] +// todo figure this out +//#[cfg(feature = "async-adapter")] mod adapter; +pub(crate) mod dummy; +use dummy::DummyConnection; +mod sync_adapter; +pub use sync_adapter::SyncAdapter; mod connmethods; pub use connmethods::{ @@ -78,7 +83,7 @@ mod internal { async() )] #[async_trait(?Send)] - pub trait BackendConnection: ConnectionMethods + Debug + AsyncRequiresSend { + pub trait BackendConnection: ConnectionMethods + Debug + Send { /// Begin a database transaction. The transaction object must be /// used in place of this connection until it is committed or aborted. async fn transaction(&mut self) -> Result>; @@ -90,6 +95,103 @@ mod internal { fn is_closed(&self) -> bool; } + #[maybe_async_cfg::maybe( + idents(Backend, BackendConnection, Connection, Transaction), + keep_self, + sync(), + async() + )] + #[async_trait(?Send)] + impl BackendConnection for Box { + async fn transaction(&mut self) -> Result { + self.deref_mut().transaction().await + } + fn backend(&self) -> Box { + self.deref().backend() + } + fn backend_name(&self) -> &'static str { + self.deref().backend_name() + } + fn is_closed(&self) -> bool { + self.deref().is_closed() + } + } + + #[maybe_async_cfg::maybe( + idents( + BackendConnection, + ConnectionMethods(sync = "ConnectionMethodsSync", async) + ), + keep_self, + sync(), + async() + )] + #[async_trait(?Send)] + impl ConnectionMethods for Box { + async fn execute(&self, sql: &str) -> Result<()> { + self.deref().execute(sql).await + } + async fn query<'c>( + &'c self, + table: &str, + columns: &[Column], + expr: Option, + limit: Option, + offset: Option, + sort: Option<&[Order]>, + ) -> Result> { + self.deref() + .query(table, columns, expr, limit, offset, sort) + .await + } + async fn insert_returning_pk( + &self, + table: &str, + columns: &[Column], + pkcol: &Column, + values: &[SqlValRef<'_>], + ) -> Result { + self.deref() + .insert_returning_pk(table, columns, pkcol, values) + .await + } + async fn insert_only( + &self, + table: &str, + columns: &[Column], + values: &[SqlValRef<'_>], + ) -> Result<()> { + self.deref().insert_only(table, columns, values).await + } + async fn insert_or_replace( + &self, + table: &str, + columns: &[Column], + pkcol: &Column, + values: &[SqlValRef<'_>], + ) -> Result<()> { + self.deref() + .insert_or_replace(table, columns, pkcol, values) + .await + } + async fn update( + &self, + table: &str, + pkcol: Column, + pk: SqlValRef<'_>, + columns: &[Column], + values: &[SqlValRef<'_>], + ) -> Result<()> { + self.deref().update(table, pkcol, pk, columns, values).await + } + async fn delete_where(&self, table: &str, expr: BoolExpr) -> Result { + self.deref().delete_where(table, expr).await + } + async fn has_table(&self, table: &str) -> Result { + self.deref().has_table(table).await + } + } + /// Database connection. May be a connection to any type of database /// as it is a boxed abstraction over a specific connection. #[maybe_async_cfg::maybe(idents(BackendConnection), sync(), async())] @@ -100,6 +202,9 @@ mod internal { #[maybe_async_cfg::maybe(idents(BackendConnection), sync(), async())] impl Connection { + pub fn new(conn: Box) -> Self { + Self { conn } + } pub async fn execute(&mut self, sql: impl AsRef) -> Result<()> { self.conn.execute(sql.as_ref()).await } @@ -108,6 +213,47 @@ mod internal { fn wrapped_connection_methods(&self) -> Result<&dyn BackendConnection> { Ok(self.conn.as_ref()) } + + #[maybe_async_cfg::only_if(key = "async")] + pub fn into_sync(self) -> Result { + Ok(SyncAdapter::new(self)?.into_connection()) + } + + #[maybe_async_cfg::only_if(key = "sync")] + pub fn into_async(self) -> Result { + Ok(adapter::AsyncAdapter::new(|| Ok(self))?.into_connection()) + } + + /// Runs the provided function with a synchronous wrapper around this + /// asynchronous connection. + /// Because this relies on some (safe) memory gymnastics, + /// there is a small but nonzero risk that if tokio fails at + /// the wrong place the the connection will be poisoned -- all subsequent calls + /// to all methods will fail. + #[maybe_async_cfg::only_if(key = "async")] + pub async fn with_sync(&mut self, f: F) -> Result + where + F: FnOnce(&mut SyncAdapter) -> Result + Send + 'static, + T: Send + 'static, + { + let mut conn2 = Connection::new(Box::new(DummyConnection::new())); + std::mem::swap(&mut conn2, self); + let ret: Result<(Result, Connection)> = tokio::task::spawn_blocking(|| { + let mut sync_conn = SyncAdapter::new(conn2)?; + let f_ret = f(&mut sync_conn); + let async_conn = sync_conn.into_inner(); + Ok((f_ret, async_conn)) + }) + .await?; + match ret { + Ok((inner_ret, mut conn)) => { + std::mem::swap(&mut conn, self); + inner_ret + } + // Self is poisoned + Err(e) => Err(e), + } + } } #[maybe_async_cfg::maybe( @@ -224,8 +370,106 @@ mod internal { } } + #[maybe_async_cfg::maybe( + idents( + BackendTransaction, + ConnectionMethods(sync = "ConnectionMethodsSync", async) + ), + keep_self, + sync(), + async() + )] + #[async_trait(?Send)] + impl<'c> BackendTransaction<'c> for Box + 'c> { + async fn commit(&mut self) -> Result<()> { + self.deref_mut().commit().await + } + async fn rollback(&mut self) -> Result<()> { + self.deref_mut().rollback().await + } + fn connection_methods(&self) -> &dyn ConnectionMethods { + self + } + } + + #[maybe_async_cfg::maybe( + idents( + BackendTransaction, + ConnectionMethods(sync = "ConnectionMethodsSync", async) + ), + keep_self, + sync(), + async() + )] + #[async_trait(?Send)] + impl<'bt> ConnectionMethods for Box + 'bt> { + async fn execute(&self, sql: &str) -> Result<()> { + self.deref().execute(sql).await + } + async fn query<'c>( + &'c self, + table: &str, + columns: &[Column], + expr: Option, + limit: Option, + offset: Option, + sort: Option<&[Order]>, + ) -> Result> { + self.deref() + .query(table, columns, expr, limit, offset, sort) + .await + } + async fn insert_returning_pk( + &self, + table: &str, + columns: &[Column], + pkcol: &Column, + values: &[SqlValRef<'_>], + ) -> Result { + self.deref() + .insert_returning_pk(table, columns, pkcol, values) + .await + } + async fn insert_only( + &self, + table: &str, + columns: &[Column], + values: &[SqlValRef<'_>], + ) -> Result<()> { + self.deref().insert_only(table, columns, values).await + } + async fn insert_or_replace( + &self, + table: &str, + columns: &[Column], + pkcol: &Column, + values: &[SqlValRef<'_>], + ) -> Result<()> { + self.deref() + .insert_or_replace(table, columns, pkcol, values) + .await + } + async fn update( + &self, + table: &str, + pkcol: Column, + pk: SqlValRef<'_>, + columns: &[Column], + values: &[SqlValRef<'_>], + ) -> Result<()> { + self.deref().update(table, pkcol, pk, columns, values).await + } + async fn delete_where(&self, table: &str, expr: BoolExpr) -> Result { + self.deref().delete_where(table, expr).await + } + async fn has_table(&self, table: &str) -> Result { + self.deref().has_table(table).await + } + } + #[maybe_async_cfg::maybe(idents(Connection(sync = "ConnectionSync", async)), sync(), async())] /// Database backend. A boxed implementation can be returned by name via [get_backend][crate::db::get_backend]. + // todo do we really need two versions of this? Can we give it two connect methods instead? #[async_trait] pub trait Backend: Send + Sync + DynClone { fn name(&self) -> &'static str; @@ -249,7 +493,7 @@ pub use internal::TransactionAsync as Transaction; // unused may occur dependending on backends being compiled // todo include by feature instead #[allow(unused)] -use internal::BackendTransactionAsync as BackendTransaction; +pub(crate) use internal::BackendTransactionAsync as BackendTransaction; pub mod sync { //! Synchronous (non-async versions of traits) @@ -293,8 +537,14 @@ impl ConnectionSpec { let path = conn_complete_if_dir(path.as_ref()); serde_json::from_reader(fs::File::open(path)?).map_err(|e| e.into()) } - pub fn get_backend(&self) -> Result> { - match get_backend(&self.backend_name) { + pub fn get_sync_backend(&self) -> Result> { + match get_sync_backend(&self.backend_name) { + Some(backend) => Ok(backend), + None => Err(crate::Error::UnknownBackend(self.backend_name.clone())), + } + } + pub fn get_async_backend(&self) -> Result> { + match get_async_backend(&self.backend_name) { Some(backend) => Ok(backend), None => Err(crate::Error::UnknownBackend(self.backend_name.clone())), } @@ -336,7 +586,7 @@ impl sync::Backend for Box { } /// Find a backend by name. -pub fn get_backend(name: &str) -> Option> { +pub fn get_async_backend(name: &str) -> Option> { match name { #[cfg(feature = "sqlite")] sqlite::BACKEND_NAME => Some(Box::new(adapter::BackendAdapter::new( @@ -348,7 +598,8 @@ pub fn get_backend(name: &str) -> Option> { } } -pub fn get_backend_sync(name: &str) -> Option> { +/// Find a backend by name. +pub fn get_sync_backend(name: &str) -> Option> { match name { #[cfg(feature = "sqlite")] sqlite::BACKEND_NAME => Some(Box::new(sqlite::SQLiteBackend::new())), @@ -359,8 +610,16 @@ pub fn get_backend_sync(name: &str) -> Option> { /// Connect to a database. For non-boxed connections, see individual /// [`Backend`] implementations. -pub async fn connect(spec: &ConnectionSpec) -> Result { - get_backend(&spec.backend_name) +pub fn connect(spec: &ConnectionSpec) -> Result { + get_sync_backend(&spec.backend_name) + .ok_or_else(|| Error::UnknownBackend(spec.backend_name.clone()))? + .connect(&spec.conn_str) +} + +/// Connect to a database. For non-boxed connections, see individual +/// [`Backend`] implementations. +pub async fn connect_async(spec: &ConnectionSpec) -> Result { + get_async_backend(&spec.backend_name) .ok_or_else(|| Error::UnknownBackend(spec.backend_name.clone()))? .connect(&spec.conn_str) .await diff --git a/butane_core/src/db/pg.rs b/butane_core/src/db/pg.rs index 27d508ef..259fb261 100644 --- a/butane_core/src/db/pg.rs +++ b/butane_core/src/db/pg.rs @@ -144,7 +144,7 @@ fn sqlvalref_for_pg_query<'a>(v: &'a SqlValRef<'a>) -> &'a dyn postgres::types:: /// Shared functionality between connection and /// transaction. Implementation detail. Semver exempt. -pub trait PgConnectionLike { +trait PgConnectionLike { type Client: postgres::GenericClient + Send; fn client(&self) -> Result<&Self::Client>; } diff --git a/butane_core/src/db/sync_adapter.rs b/butane_core/src/db/sync_adapter.rs new file mode 100644 index 00000000..d49a964b --- /dev/null +++ b/butane_core/src/db/sync_adapter.rs @@ -0,0 +1,186 @@ +use crate::db::RawQueryResult; +use crate::migrations::adb; +use crate::query::{BoolExpr, Order}; +use crate::{Column, Result, SqlVal, SqlValRef}; + +use std::future::Future; + +#[derive(Debug)] +pub struct SyncAdapter { + runtime_handle: tokio::runtime::Handle, + _runtime: Option, + inner: T, +} + +impl SyncAdapter { + // TODO needs an inner new that preserves the handle + pub fn new(inner: T) -> Result { + // TODO needs to check that the existing runtime isn't a current_thread + // if it is, handle.block_on can't drive IO. + // We can create a new runtime in that case, but not on the same thread. + match tokio::runtime::Handle::try_current() { + Ok(handle) => Ok(Self { + runtime_handle: handle, + _runtime: None, + inner, + }), + Err(_) => { + let runtime = tokio::runtime::Builder::new_multi_thread() + .worker_threads(1) + .enable_all() + .build()?; + Ok(Self { + runtime_handle: runtime.handle().clone(), + _runtime: Some(runtime), + inner, + }) + } + } + } + + fn block_on(&self, future: F) -> F::Output { + self.runtime_handle.block_on(future) + } + + pub fn into_inner(self) -> T { + self.inner + } +} + +impl Clone for SyncAdapter +where + T: Clone, +{ + fn clone(&self) -> Self { + SyncAdapter { + runtime_handle: self.runtime_handle.clone(), + _runtime: None, + inner: self.inner.clone(), + } + } +} + +impl crate::db::sync::ConnectionMethods for SyncAdapter +where + T: crate::db::ConnectionMethods, +{ + fn execute(&self, sql: &str) -> Result<()> { + self.block_on(self.inner.execute(sql)) + } + fn query<'c>( + &'c self, + table: &str, + columns: &[Column], + expr: Option, + limit: Option, + offset: Option, + sort: Option<&[Order]>, + ) -> Result> { + self.block_on(self.inner.query(table, columns, expr, limit, offset, sort)) + } + fn insert_returning_pk( + &self, + table: &str, + columns: &[Column], + pkcol: &Column, + values: &[SqlValRef<'_>], + ) -> Result { + self.block_on( + self.inner + .insert_returning_pk(table, columns, pkcol, values), + ) + } + fn insert_only(&self, table: &str, columns: &[Column], values: &[SqlValRef<'_>]) -> Result<()> { + self.block_on(self.inner.insert_only(table, columns, values)) + } + fn insert_or_replace( + &self, + table: &str, + columns: &[Column], + pkcol: &Column, + values: &[SqlValRef<'_>], + ) -> Result<()> { + self.block_on(self.inner.insert_or_replace(table, columns, pkcol, values)) + } + fn update( + &self, + table: &str, + pkcol: Column, + pk: SqlValRef<'_>, + columns: &[Column], + values: &[SqlValRef<'_>], + ) -> Result<()> { + self.block_on(self.inner.update(table, pkcol, pk, columns, values)) + } + fn delete_where(&self, table: &str, expr: BoolExpr) -> Result { + self.block_on(self.inner.delete_where(table, expr)) + } + fn has_table(&self, table: &str) -> Result { + self.block_on(self.inner.has_table(table)) + } +} + +impl crate::db::sync::BackendConnection for SyncAdapter +where + T: crate::db::BackendConnection, +{ + fn transaction(&mut self) -> Result> { + let transaction: crate::db::Transaction = + self.runtime_handle.block_on(self.inner.transaction())?; + let transaction_adapter = SyncAdapter::new(transaction.trans)?; + Ok(crate::db::sync::Transaction::new(Box::new( + transaction_adapter, + ))) + } + fn backend(&self) -> Box { + Box::new(SyncAdapter::new(self.inner.backend()).unwrap()) + } + fn backend_name(&self) -> &'static str { + self.inner.backend_name() + } + fn is_closed(&self) -> bool { + self.inner.is_closed() + } +} + +impl SyncAdapter +where + T: crate::db::BackendConnection + 'static, +{ + pub fn into_connection(self) -> crate::db::sync::Connection { + crate::db::sync::Connection::new(Box::new(self)) + } +} + +impl<'c, T> crate::db::sync::BackendTransaction<'c> for SyncAdapter +where + T: crate::db::BackendTransaction<'c>, +{ + fn commit(&mut self) -> Result<()> { + self.runtime_handle.block_on(self.inner.commit()) + } + fn rollback(&mut self) -> Result<()> { + self.runtime_handle.block_on(self.inner.rollback()) + } + fn connection_methods(&self) -> &dyn crate::db::sync::ConnectionMethods { + self + } +} + +impl crate::db::sync::Backend for SyncAdapter +where + T: crate::db::Backend + Clone, +{ + fn name(&self) -> &'static str { + self.inner.name() + } + fn create_migration_sql(&self, current: &adb::ADB, ops: Vec) -> Result { + self.inner.create_migration_sql(current, ops) + } + fn connect(&self, conn_str: &str) -> Result { + let conn_async = self.block_on(self.inner.connect(conn_str))?; + Ok(crate::db::sync::Connection { + conn: Box::new(SyncAdapter::new(conn_async.conn)?), + }) + } +} diff --git a/butane_core/src/lib.rs b/butane_core/src/lib.rs index 75cf6e79..6a2a0a8f 100644 --- a/butane_core/src/lib.rs +++ b/butane_core/src/lib.rs @@ -20,10 +20,10 @@ pub mod migrations; pub mod query; pub mod sqlval; +mod autopk; #[cfg(feature = "uuid")] pub mod uuid; -mod autopk; pub use autopk::AutoPk; use custom::SqlTypeCustom; use db::sync::ConnectionMethods as ConnectionMethodsSync; @@ -271,6 +271,8 @@ pub enum Error { LiteralForCustomUnsupported(custom::SqlValCustom), #[error("This DataObject doesn't support determining whether it has been saved.")] SaveDeterminationNotSupported, + #[error("This is a dummy poisoned connection.")] + PoisonedConnection, #[error("(De)serialization error {0}")] SerdeJson(#[from] serde_json::Error), #[error("IO error {0}")] diff --git a/butane_core/src/migrations/migration.rs b/butane_core/src/migrations/migration.rs index 5a7c6bf3..a78d26a5 100644 --- a/butane_core/src/migrations/migration.rs +++ b/butane_core/src/migrations/migration.rs @@ -3,7 +3,7 @@ use std::fmt::Debug; use super::adb::{ATable, DeferredSqlType, TypeKey, ADB}; use super::ButaneMigration; -use crate::db::ConnectionMethods; +use crate::db::sync::ConnectionMethods; use crate::query::{BoolExpr, Expr}; use crate::{db, sqlval::ToSql, DataObject, DataResult, Error, Result}; @@ -14,7 +14,6 @@ use crate::{db, sqlval::ToSql, DataObject, DataResult, Error, Result}; /// /// A Migration cannot be constructed directly, only retrieved from /// [Migrations][crate::migrations::Migrations]. -#[async_trait::async_trait(?Send)] pub trait Migration: Debug + PartialEq { /// Retrieves the full abstract database state describing all tables fn db(&self) -> Result; @@ -39,48 +38,46 @@ pub trait Migration: Debug + PartialEq { /// Apply the migration to a database connection. The connection /// must be for the same type of database as this and the database /// must be in the state of the migration prior to this one - async fn apply(&self, conn: &mut impl db::BackendConnection) -> Result<()> { + fn apply(&self, conn: &mut impl db::sync::BackendConnection) -> Result<()> { let backend_name = conn.backend_name(); - let tx = conn.transaction().await?; + let tx = conn.transaction()?; let sql = self .up_sql(backend_name)? .ok_or_else(|| Error::UnknownBackend(backend_name.to_string()))?; - tx.execute(&sql).await?; - self.mark_applied(&tx).await?; - tx.commit().await + tx.execute(&sql)?; + self.mark_applied(&tx)?; + tx.commit() } /// Mark the migration as being applied without doing any /// work. Use carefully -- the caller must ensure that the /// database schema already matches that expected by this /// migration. - async fn mark_applied(&self, conn: &impl db::ConnectionMethods) -> Result<()> { + fn mark_applied(&self, conn: &impl db::sync::ConnectionMethods) -> Result<()> { conn.insert_only( ButaneMigration::TABLE, ButaneMigration::COLUMNS, &[self.name().as_ref().to_sql_ref()], ) - .await } /// Un-apply (downgrade) the migration to a database /// connection. The connection must be for the same type of /// database as this and this must be the latest migration applied /// to the database. - async fn downgrade(&self, conn: &mut impl db::BackendConnection) -> Result<()> { + fn downgrade(&self, conn: &mut impl db::sync::BackendConnection) -> Result<()> { let backend_name = conn.backend_name(); - let tx = conn.transaction().await?; + let tx = conn.transaction()?; let sql = self .down_sql(backend_name)? .ok_or_else(|| Error::UnknownBackend(backend_name.to_string()))?; - tx.execute(&sql).await?; + tx.execute(&sql)?; let nameval = self.name().as_ref().to_sql(); tx.delete_where( ButaneMigration::TABLE, BoolExpr::Eq(ButaneMigration::PKCOL, Expr::Val(nameval)), - ) - .await?; - tx.commit().await + )?; + tx.commit() } } diff --git a/butane_core/src/migrations/mod.rs b/butane_core/src/migrations/mod.rs index 2369d1d5..875191c1 100644 --- a/butane_core/src/migrations/mod.rs +++ b/butane_core/src/migrations/mod.rs @@ -8,8 +8,7 @@ use std::path::Path; use fallible_iterator::FallibleIterator; use nonempty::NonEmpty; -use crate::db::BackendRows; -use crate::db::{Column, ConnectionMethods}; +use crate::db::{BackendRows, Column, ConnectionMethods}; use crate::sqlval::{FromSql, SqlValRef, ToSql}; use crate::{db, query, DataObject, DataResult, Error, PrimaryKeyType, Result, SqlType}; @@ -28,7 +27,6 @@ use async_trait::async_trait; pub use memmigrations::{MemMigration, MemMigrations}; /// A collection of migrations. -#[async_trait(?Send)] pub trait Migrations { type M: Migration; @@ -73,8 +71,11 @@ pub trait Migrations { } /// Get migrations which have not yet been applied to the database - async fn unapplied_migrations(&self, conn: &impl ConnectionMethods) -> Result> { - match self.last_applied_migration(conn).await? { + fn unapplied_migrations( + &self, + conn: &impl crate::db::sync::ConnectionMethods, + ) -> Result> { + match self.last_applied_migration(conn)? { None => self.all_migrations(), Some(m) => self.migrations_since(&m), } @@ -82,11 +83,11 @@ pub trait Migrations { /// Get the last migration that has been applied to the database or None /// if no migrations have been applied - async fn last_applied_migration( + fn last_applied_migration( &self, - conn: &impl ConnectionMethods, + conn: &impl crate::db::sync::ConnectionMethods, ) -> Result> { - if !conn.has_table(ButaneMigration::TABLE).await? { + if !conn.has_table(ButaneMigration::TABLE)? { return Ok(None); } let migrations: Vec = conn @@ -97,8 +98,7 @@ pub trait Migrations { None, None, None, - ) - .await? + )? .mapped(ButaneMigration::from_row) .collect()?; @@ -117,8 +117,31 @@ pub trait Migrations { } } +pub fn apply_unapplied_migrations( + migrations: &impl Migrations, + conn: &mut impl crate::db::sync::BackendConnection, +) -> Result<()> { + let to_apply = migrations.unapplied_migrations(conn)?; + for migration in to_apply { + crate::info!("Applying migration {}", migration.name()); + migration.apply(conn)?; + } + Ok(()) +} + +pub async fn apply_unapplied_migrations_async( + migrations: M, + conn: &mut crate::db::Connection, +) -> Result<()> { + conn.with_sync(|conn| { + let m2 = migrations; // temp variable to force pass-by-value into the closure to satisfy Send + apply_unapplied_migrations(&m2, conn)?; + Ok(()) + }) + .await +} + /// Extension of [`Migrations`] to modify the series of migrations. -#[async_trait(?Send)] pub trait MigrationsMut: Migrations where Self::M: MigrationMut, @@ -142,10 +165,9 @@ where /// any storage backing it) and deleting the record of their /// existence/application from the database. The database schema /// is not modified, nor is any other data removed. Use carefully. - async fn clear_migrations(&mut self, conn: &impl ConnectionMethods) -> Result<()> { + fn clear_migrations(&mut self, conn: &impl crate::db::sync::ConnectionMethods) -> Result<()> { self.delete_migrations()?; - conn.delete_where(ButaneMigration::TABLE, query::BoolExpr::True) - .await?; + conn.delete_where(ButaneMigration::TABLE, query::BoolExpr::True)?; Ok(()) } diff --git a/butane_test_helper/src/lib.rs b/butane_test_helper/src/lib.rs index 5717ced5..5a3d25f1 100644 --- a/butane_test_helper/src/lib.rs +++ b/butane_test_helper/src/lib.rs @@ -2,8 +2,10 @@ //! Macros depend on [`butane_core`], `env_logger` and [`log`]. #![deny(missing_docs)] -use butane_core::db::{connect, get_backend, pg, sqlite, Backend, Connection, ConnectionSpec}; -use butane_core::migrations::{self, MemMigrations, Migration, Migrations, MigrationsMut}; +use butane_core::db::{ + connect_async, get_async_backend, pg, sqlite, Backend, Connection, ConnectionSpec, +}; +use butane_core::migrations::{self, MemMigrations, Migration, MigrationsMut}; use once_cell::sync::Lazy; use std::io::{BufRead, BufReader, Read, Write}; @@ -17,7 +19,7 @@ use uuid::Uuid; /// Create a postgres [`Connection`]. pub async fn pg_connection() -> (Connection, PgSetupData) { - let backend = get_backend(pg::BACKEND_NAME).unwrap(); + let backend = get_async_backend(pg::BACKEND_NAME).unwrap(); let data = pg_setup().await; (backend.connect(&pg_connstr(&data)).await.unwrap(), data) } @@ -162,7 +164,9 @@ pub async fn pg_setup() -> PgSetupData { let new_dbname = format!("butane_test_{}", Uuid::new_v4().simple()); log::info!("new db is `{}`", &new_dbname); - let mut conn = connect(&ConnectionSpec::new("pg", &connstr)).await.unwrap(); + let mut conn = connect_async(&ConnectionSpec::new("pg", &connstr)) + .await + .unwrap(); conn.execute(format!("CREATE DATABASE {new_dbname};")) .await .unwrap(); @@ -211,16 +215,14 @@ pub async fn setup_db(backend: Box, conn: &mut Connection, migrate: "expected to create migration" ); log::info!("created current migration"); - let to_apply = mem_migrations.unapplied_migrations(conn).await.unwrap(); - for m in to_apply { - log::info!("Applying migration {}", m.name()); - m.apply(conn).await.unwrap(); - } + migrations::apply_unapplied_migrations_async(mem_migrations.clone(), conn) + .await + .unwrap(); } /// Create a sqlite [`Connection`]. pub async fn sqlite_connection() -> Connection { - let backend = get_backend(sqlite::BACKEND_NAME).unwrap(); + let backend = get_async_backend(sqlite::BACKEND_NAME).unwrap(); backend.connect(":memory:").await.unwrap() } diff --git a/example/src/main.rs b/example/src/main.rs index f81b4798..635aca3f 100644 --- a/example/src/main.rs +++ b/example/src/main.rs @@ -94,7 +94,7 @@ async fn establish_connection() -> Result { let mut cwd = std::env::current_dir()?; cwd.push(".butane"); let spec = ConnectionSpec::load(cwd)?; - let conn = butane::db::connect(&spec).await?; + let conn = butane::db::connect_async(&spec).await?; Ok(conn) } diff --git a/examples/newtype/src/lib.rs b/examples/newtype/src/lib.rs index d457277c..3bf17bd4 100644 --- a/examples/newtype/src/lib.rs +++ b/examples/newtype/src/lib.rs @@ -6,7 +6,7 @@ pub mod butane_migrations; pub mod models; use butane::db::{Connection, ConnectionSpec}; -use butane::migrations::{Migration, Migrations}; +use butane::migrations; use butane::prelude::*; use models::{Blog, Post}; @@ -15,14 +15,13 @@ use models::{Blog, Post}; /// Load a [Connection]. pub async fn establish_connection() -> Connection { let mut connection = - butane::db::connect(&ConnectionSpec::load(".butane/connection.json").unwrap()) + butane::db::connect_async(&ConnectionSpec::load(".butane/connection.json").unwrap()) .await .unwrap(); let migrations = butane_migrations::get_migrations().unwrap(); - let to_apply = migrations.unapplied_migrations(&connection).await.unwrap(); - for migration in to_apply { - migration.apply(&mut connection).await.unwrap(); - } + migrations::apply_unapplied_migrations_async(migrations, connection) + .await + .unwrap(); connection } From 13cdcd7acab466ace61c136bd79b508731468c46 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Sun, 14 Jul 2024 21:13:49 -0400 Subject: [PATCH 36/78] Add getting_started_async Regular getting_started is now sync --- Cargo.lock | 14 + examples/getting_started/Cargo.toml | 1 - examples/getting_started/README.md | 2 + .../getting_started/src/bin/delete_post.rs | 6 +- .../getting_started/src/bin/publish_post.rs | 6 +- .../getting_started/src/bin/show_posts.rs | 6 +- .../getting_started/src/bin/write_post.rs | 11 +- examples/getting_started/src/lib.rs | 22 +- .../.butane/clistate.json | 3 + .../20201229_144636751_init/Blog.table | 27 + .../20201229_144636751_init/Post.table | 71 +++ .../Post_tags_Many.table | 27 + .../20201229_144636751_init/Tag.table | 16 + .../20201229_144636751_init/info.json | 6 + .../migrations/20201229_144636751_init/lock | 0 .../20201229_144636751_init/pg_down.sql | 4 + .../20201229_144636751_init/pg_up.sql | 22 + .../20201229_144636751_init/sqlite_down.sql | 4 + .../20201229_144636751_init/sqlite_up.sql | 22 + .../20201229_171630604_likes/Post.table | 82 +++ .../20201229_171630604_likes/info.json | 12 + .../migrations/20201229_171630604_likes/lock | 0 .../20201229_171630604_likes/pg_down.sql | 1 + .../20201229_171630604_likes/pg_up.sql | 1 + .../20201229_171630604_likes/sqlite_down.sql | 11 + .../20201229_171630604_likes/sqlite_up.sql | 1 + .../Post.table | 102 ++++ .../Post_tags_Many.table | 43 ++ .../info.json | 11 + .../20240115_023841384_dbconstraints/lock | 0 .../pg_down.sql | 27 + .../pg_up.sql | 30 + .../sqlite_down.sql | 27 + .../sqlite_up.sql | 30 + .../.butane/migrations/current/.gitignore | 1 + .../.butane/migrations/current/Blog.table | 31 + .../.butane/migrations/current/Post.table | 99 ++++ .../migrations/current/Post_tags_Many.table | 40 ++ .../.butane/migrations/current/Tag.table | 18 + .../.butane/migrations/state.json | 3 + examples/getting_started_async/.gitignore | 2 + examples/getting_started_async/Cargo.toml | 44 ++ examples/getting_started_async/README.md | 13 + .../src/bin/delete_post.rs | 19 + .../src/bin/publish_post.rs | 24 + .../src/bin/show_posts.rs | 20 + .../src/bin/write_post.rs | 42 ++ .../src/butane_migrations.rs | 555 ++++++++++++++++++ examples/getting_started_async/src/lib.rs | 43 ++ examples/getting_started_async/src/models.rs | 76 +++ .../getting_started_async/tests/rollback.rs | 71 +++ 51 files changed, 1718 insertions(+), 31 deletions(-) create mode 100644 examples/getting_started_async/.butane/clistate.json create mode 100644 examples/getting_started_async/.butane/migrations/20201229_144636751_init/Blog.table create mode 100644 examples/getting_started_async/.butane/migrations/20201229_144636751_init/Post.table create mode 100644 examples/getting_started_async/.butane/migrations/20201229_144636751_init/Post_tags_Many.table create mode 100644 examples/getting_started_async/.butane/migrations/20201229_144636751_init/Tag.table create mode 100644 examples/getting_started_async/.butane/migrations/20201229_144636751_init/info.json create mode 100644 examples/getting_started_async/.butane/migrations/20201229_144636751_init/lock create mode 100644 examples/getting_started_async/.butane/migrations/20201229_144636751_init/pg_down.sql create mode 100644 examples/getting_started_async/.butane/migrations/20201229_144636751_init/pg_up.sql create mode 100644 examples/getting_started_async/.butane/migrations/20201229_144636751_init/sqlite_down.sql create mode 100644 examples/getting_started_async/.butane/migrations/20201229_144636751_init/sqlite_up.sql create mode 100644 examples/getting_started_async/.butane/migrations/20201229_171630604_likes/Post.table create mode 100644 examples/getting_started_async/.butane/migrations/20201229_171630604_likes/info.json create mode 100644 examples/getting_started_async/.butane/migrations/20201229_171630604_likes/lock create mode 100644 examples/getting_started_async/.butane/migrations/20201229_171630604_likes/pg_down.sql create mode 100644 examples/getting_started_async/.butane/migrations/20201229_171630604_likes/pg_up.sql create mode 100644 examples/getting_started_async/.butane/migrations/20201229_171630604_likes/sqlite_down.sql create mode 100644 examples/getting_started_async/.butane/migrations/20201229_171630604_likes/sqlite_up.sql create mode 100644 examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/Post.table create mode 100644 examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/Post_tags_Many.table create mode 100644 examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/info.json create mode 100644 examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/lock create mode 100644 examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/pg_down.sql create mode 100644 examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/pg_up.sql create mode 100644 examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/sqlite_down.sql create mode 100644 examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/sqlite_up.sql create mode 100644 examples/getting_started_async/.butane/migrations/current/.gitignore create mode 100644 examples/getting_started_async/.butane/migrations/current/Blog.table create mode 100644 examples/getting_started_async/.butane/migrations/current/Post.table create mode 100644 examples/getting_started_async/.butane/migrations/current/Post_tags_Many.table create mode 100644 examples/getting_started_async/.butane/migrations/current/Tag.table create mode 100644 examples/getting_started_async/.butane/migrations/state.json create mode 100644 examples/getting_started_async/.gitignore create mode 100644 examples/getting_started_async/Cargo.toml create mode 100644 examples/getting_started_async/README.md create mode 100644 examples/getting_started_async/src/bin/delete_post.rs create mode 100644 examples/getting_started_async/src/bin/publish_post.rs create mode 100644 examples/getting_started_async/src/bin/show_posts.rs create mode 100644 examples/getting_started_async/src/bin/write_post.rs create mode 100644 examples/getting_started_async/src/butane_migrations.rs create mode 100644 examples/getting_started_async/src/lib.rs create mode 100644 examples/getting_started_async/src/models.rs create mode 100644 examples/getting_started_async/tests/rollback.rs diff --git a/Cargo.lock b/Cargo.lock index 0336631c..4d759973 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -969,6 +969,20 @@ dependencies = [ [[package]] name = "getting_started" version = "0.1.0" +dependencies = [ + "butane", + "butane_cli", + "butane_core", + "butane_test_helper", + "cfg-if", + "env_logger", + "log", + "paste", +] + +[[package]] +name = "getting_started_async" +version = "0.1.0" dependencies = [ "butane", "butane_cli", diff --git a/examples/getting_started/Cargo.toml b/examples/getting_started/Cargo.toml index 454301e6..da7ace86 100644 --- a/examples/getting_started/Cargo.toml +++ b/examples/getting_started/Cargo.toml @@ -29,7 +29,6 @@ sqlite-bundled = ["butane/sqlite-bundled"] [dependencies] butane.workspace = true -tokio = { workspace = true, features = ["macros"] } [dev-dependencies] butane_cli.workspace = true diff --git a/examples/getting_started/README.md b/examples/getting_started/README.md index 9501031c..0bebe087 100644 --- a/examples/getting_started/README.md +++ b/examples/getting_started/README.md @@ -1,5 +1,7 @@ # butane `getting_started` example +This is the sync version of this example. There is also [an async version](../getting_started_async) + To use this example, build the entire project using `cargo build` in the project root, and then run these commands in this directory: diff --git a/examples/getting_started/src/bin/delete_post.rs b/examples/getting_started/src/bin/delete_post.rs index c8a167ca..845c34ef 100644 --- a/examples/getting_started/src/bin/delete_post.rs +++ b/examples/getting_started/src/bin/delete_post.rs @@ -5,15 +5,13 @@ use butane::query; use getting_started::models::Post; use getting_started::*; -#[tokio::main(flavor = "current_thread")] -async fn main() { +fn main() { let target = args().nth(1).expect("Expected a target to match against"); let pattern = format!("%{target}%"); - let conn = establish_connection().await; + let conn = establish_connection() let cnt = query!(Post, title.like({ pattern })) .delete(&conn) - .await .expect("error deleting posts"); println!("Deleted {cnt} posts"); } diff --git a/examples/getting_started/src/bin/publish_post.rs b/examples/getting_started/src/bin/publish_post.rs index 2bb49c27..8cb59929 100644 --- a/examples/getting_started/src/bin/publish_post.rs +++ b/examples/getting_started/src/bin/publish_post.rs @@ -5,20 +5,18 @@ use butane::prelude::*; use getting_started::models::Post; use getting_started::*; -#[tokio::main(flavor = "current_thread")] async fn main() { let id = args() .nth(1) .expect("publish_post requires a post id") .parse::() .expect("Invalid ID"); - let conn = establish_connection().await; + let conn = establish_connection() let mut post = Post::get(&conn, id) - .await .unwrap_or_else(|_| panic!("Unable to find post {id}")); // Just a normal Rust assignment, no fancy set methods post.published = true; - post.save(&conn).await.unwrap(); + post.save(&conn).unwrap(); println!("Published post {}", post.title); } diff --git a/examples/getting_started/src/bin/show_posts.rs b/examples/getting_started/src/bin/show_posts.rs index 46f6fcb6..c3aff322 100644 --- a/examples/getting_started/src/bin/show_posts.rs +++ b/examples/getting_started/src/bin/show_posts.rs @@ -3,13 +3,11 @@ use butane::query; use getting_started::models::Post; use getting_started::*; -#[tokio::main(flavor = "current_thread")] -async fn main() { - let conn = establish_connection().await; +fn main() { + let conn = establish_connection(); let results = query!(Post, published == true) .limit(5) .load(&conn) - .await .expect("Error loading posts"); println!("Displaying {} posts", results.len()); for post in results { diff --git a/examples/getting_started/src/bin/write_post.rs b/examples/getting_started/src/bin/write_post.rs index a38a5746..ad58b7cf 100644 --- a/examples/getting_started/src/bin/write_post.rs +++ b/examples/getting_started/src/bin/write_post.rs @@ -2,16 +2,15 @@ use std::io::{stdin, Read}; use getting_started::*; -#[tokio::main(flavor = "current_thread")] -async fn main() { - let conn = establish_connection().await; +fn main() { + let conn = establish_connection(); - let blog = match existing_blog(&conn).await { + let blog = match existing_blog(&conn) { Some(blog) => blog, None => { println!("Enter blog name"); let name = readline(); - create_blog(&conn, name).await + create_blog(&conn, name) } }; @@ -21,7 +20,7 @@ async fn main() { let mut body = String::new(); stdin().read_to_string(&mut body).unwrap(); - let post = create_post(&conn, &blog, title, body).await; + let post = create_post(&conn, &blog, title, body); println!( "\nSaved unpublished post {} with id {}", post.title, post.id diff --git a/examples/getting_started/src/lib.rs b/examples/getting_started/src/lib.rs index ecb6470d..dcacc12c 100644 --- a/examples/getting_started/src/lib.rs +++ b/examples/getting_started/src/lib.rs @@ -11,34 +11,32 @@ use butane::prelude::*; use models::{Blog, Post}; /// Load a [Connection]. -pub async fn establish_connection() -> Connection { +pub fn establish_connection() -> Connection { let mut connection = - butane::db::connect(&ConnectionSpec::load(".butane/connection.json").unwrap()) - .await - .unwrap(); + butane::db::connect(&ConnectionSpec::load(".butane/connection.json").unwrap()).unwrap(); let migrations = butane_migrations::get_migrations().unwrap(); - let to_apply = migrations.unapplied_migrations(&connection).await.unwrap(); + let to_apply = migrations.unapplied_migrations(&connection).unwrap(); for migration in to_apply { - migration.apply(&mut connection).await.unwrap(); + migration.apply(&mut connection).unwrap(); } connection } /// Create a [Blog]. -pub async fn create_blog(conn: &Connection, name: impl Into) -> Blog { +pub fn create_blog(conn: &Connection, name: impl Into) -> Blog { let mut blog = Blog::new(name); - blog.save(conn).await.unwrap(); + blog.save(conn).unwrap(); blog } /// Create a [Post]. -pub async fn create_post(conn: &Connection, blog: &Blog, title: String, body: String) -> Post { +pub fn create_post(conn: &Connection, blog: &Blog, title: String, body: String) -> Post { let mut new_post = Post::new(blog, title, body); - new_post.save(conn).await.unwrap(); + new_post.save(conn).unwrap(); new_post } /// Fetch the first existing [Blog] if one exists. -pub async fn existing_blog(conn: &Connection) -> Option { - Blog::query().load_first(conn).await.unwrap() +pub fn existing_blog(conn: &Connection) -> Option { + Blog::query().load_first(conn).unwrap() } diff --git a/examples/getting_started_async/.butane/clistate.json b/examples/getting_started_async/.butane/clistate.json new file mode 100644 index 00000000..a8f08861 --- /dev/null +++ b/examples/getting_started_async/.butane/clistate.json @@ -0,0 +1,3 @@ +{ + "embedded": true +} diff --git a/examples/getting_started_async/.butane/migrations/20201229_144636751_init/Blog.table b/examples/getting_started_async/.butane/migrations/20201229_144636751_init/Blog.table new file mode 100644 index 00000000..8fc11406 --- /dev/null +++ b/examples/getting_started_async/.butane/migrations/20201229_144636751_init/Blog.table @@ -0,0 +1,27 @@ +{ + "name": "Blog", + "columns": [ + { + "name": "id", + "sqltype": { + "Known": "BigInt" + }, + "nullable": false, + "pk": true, + "auto": true, + "unique": false, + "default": null + }, + { + "name": "name", + "sqltype": { + "Known": "Text" + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + } + ] +} diff --git a/examples/getting_started_async/.butane/migrations/20201229_144636751_init/Post.table b/examples/getting_started_async/.butane/migrations/20201229_144636751_init/Post.table new file mode 100644 index 00000000..1a45ba7a --- /dev/null +++ b/examples/getting_started_async/.butane/migrations/20201229_144636751_init/Post.table @@ -0,0 +1,71 @@ +{ + "name": "Post", + "columns": [ + { + "name": "id", + "sqltype": { + "Known": "Int" + }, + "nullable": false, + "pk": true, + "auto": true, + "unique": false, + "default": null + }, + { + "name": "title", + "sqltype": { + "Known": "Text" + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "body", + "sqltype": { + "Known": "Text" + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "published", + "sqltype": { + "Known": "Bool" + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "blog", + "sqltype": { + "Known": "BigInt" + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "byline", + "sqltype": { + "Known": "Text" + }, + "nullable": true, + "pk": false, + "auto": false, + "unique": false, + "default": null + } + ] +} diff --git a/examples/getting_started_async/.butane/migrations/20201229_144636751_init/Post_tags_Many.table b/examples/getting_started_async/.butane/migrations/20201229_144636751_init/Post_tags_Many.table new file mode 100644 index 00000000..de7e3b2a --- /dev/null +++ b/examples/getting_started_async/.butane/migrations/20201229_144636751_init/Post_tags_Many.table @@ -0,0 +1,27 @@ +{ + "name": "Post_tags_Many", + "columns": [ + { + "name": "owner", + "sqltype": { + "Known": "Int" + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "has", + "sqltype": { + "Known": "Text" + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + } + ] +} diff --git a/examples/getting_started_async/.butane/migrations/20201229_144636751_init/Tag.table b/examples/getting_started_async/.butane/migrations/20201229_144636751_init/Tag.table new file mode 100644 index 00000000..1d3b1e5b --- /dev/null +++ b/examples/getting_started_async/.butane/migrations/20201229_144636751_init/Tag.table @@ -0,0 +1,16 @@ +{ + "name": "Tag", + "columns": [ + { + "name": "tag", + "sqltype": { + "Known": "Text" + }, + "nullable": false, + "pk": true, + "auto": false, + "unique": false, + "default": null + } + ] +} diff --git a/examples/getting_started_async/.butane/migrations/20201229_144636751_init/info.json b/examples/getting_started_async/.butane/migrations/20201229_144636751_init/info.json new file mode 100644 index 00000000..beabd46e --- /dev/null +++ b/examples/getting_started_async/.butane/migrations/20201229_144636751_init/info.json @@ -0,0 +1,6 @@ +{ + "backends": [ + "sqlite", + "pg" + ] +} diff --git a/examples/getting_started_async/.butane/migrations/20201229_144636751_init/lock b/examples/getting_started_async/.butane/migrations/20201229_144636751_init/lock new file mode 100644 index 00000000..e69de29b diff --git a/examples/getting_started_async/.butane/migrations/20201229_144636751_init/pg_down.sql b/examples/getting_started_async/.butane/migrations/20201229_144636751_init/pg_down.sql new file mode 100644 index 00000000..a565ac28 --- /dev/null +++ b/examples/getting_started_async/.butane/migrations/20201229_144636751_init/pg_down.sql @@ -0,0 +1,4 @@ +DROP TABLE Blog; +DROP TABLE Post; +DROP TABLE Post_tags_Many; +DROP TABLE Tag; diff --git a/examples/getting_started_async/.butane/migrations/20201229_144636751_init/pg_up.sql b/examples/getting_started_async/.butane/migrations/20201229_144636751_init/pg_up.sql new file mode 100644 index 00000000..00d311c8 --- /dev/null +++ b/examples/getting_started_async/.butane/migrations/20201229_144636751_init/pg_up.sql @@ -0,0 +1,22 @@ +CREATE TABLE Blog ( +id BIGSERIAL NOT NULL PRIMARY KEY, +"name" TEXT NOT NULL +); +CREATE TABLE Post ( +id SERIAL NOT NULL PRIMARY KEY, +title TEXT NOT NULL, +body TEXT NOT NULL, +published BOOLEAN NOT NULL, +blog BIGINT NOT NULL, +byline TEXT +); +CREATE TABLE Post_tags_Many ( +owner INTEGER NOT NULL, +has TEXT NOT NULL +); +CREATE TABLE Tag ( +tag TEXT NOT NULL PRIMARY KEY +); +CREATE TABLE IF NOT EXISTS butane_migrations ( +"name" TEXT NOT NULL PRIMARY KEY +); diff --git a/examples/getting_started_async/.butane/migrations/20201229_144636751_init/sqlite_down.sql b/examples/getting_started_async/.butane/migrations/20201229_144636751_init/sqlite_down.sql new file mode 100644 index 00000000..a565ac28 --- /dev/null +++ b/examples/getting_started_async/.butane/migrations/20201229_144636751_init/sqlite_down.sql @@ -0,0 +1,4 @@ +DROP TABLE Blog; +DROP TABLE Post; +DROP TABLE Post_tags_Many; +DROP TABLE Tag; diff --git a/examples/getting_started_async/.butane/migrations/20201229_144636751_init/sqlite_up.sql b/examples/getting_started_async/.butane/migrations/20201229_144636751_init/sqlite_up.sql new file mode 100644 index 00000000..1b1f3feb --- /dev/null +++ b/examples/getting_started_async/.butane/migrations/20201229_144636751_init/sqlite_up.sql @@ -0,0 +1,22 @@ +CREATE TABLE Blog ( +id INTEGER NOT NULL PRIMARY KEY, +"name" TEXT NOT NULL +); +CREATE TABLE Post ( +id INTEGER NOT NULL PRIMARY KEY, +title TEXT NOT NULL, +body TEXT NOT NULL, +published INTEGER NOT NULL, +blog INTEGER NOT NULL, +byline TEXT +); +CREATE TABLE Post_tags_Many ( +owner INTEGER NOT NULL, +has TEXT NOT NULL +); +CREATE TABLE Tag ( +tag TEXT NOT NULL PRIMARY KEY +); +CREATE TABLE IF NOT EXISTS butane_migrations ( +"name" TEXT NOT NULL PRIMARY KEY +); diff --git a/examples/getting_started_async/.butane/migrations/20201229_171630604_likes/Post.table b/examples/getting_started_async/.butane/migrations/20201229_171630604_likes/Post.table new file mode 100644 index 00000000..2427ac30 --- /dev/null +++ b/examples/getting_started_async/.butane/migrations/20201229_171630604_likes/Post.table @@ -0,0 +1,82 @@ +{ + "name": "Post", + "columns": [ + { + "name": "id", + "sqltype": { + "Known": "Int" + }, + "nullable": false, + "pk": true, + "auto": true, + "unique": false, + "default": null + }, + { + "name": "title", + "sqltype": { + "Known": "Text" + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "body", + "sqltype": { + "Known": "Text" + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "published", + "sqltype": { + "Known": "Bool" + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "blog", + "sqltype": { + "Known": "BigInt" + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "byline", + "sqltype": { + "Known": "Text" + }, + "nullable": true, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "likes", + "sqltype": { + "Known": "Int" + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + } + ] +} diff --git a/examples/getting_started_async/.butane/migrations/20201229_171630604_likes/info.json b/examples/getting_started_async/.butane/migrations/20201229_171630604_likes/info.json new file mode 100644 index 00000000..473354cf --- /dev/null +++ b/examples/getting_started_async/.butane/migrations/20201229_171630604_likes/info.json @@ -0,0 +1,12 @@ +{ + "from_name": "20201229_144636751_init", + "table_bases": { + "Blog": "20201229_144636751_init", + "Post_tags_Many": "20201229_144636751_init", + "Tag": "20201229_144636751_init" + }, + "backends": [ + "sqlite", + "pg" + ] +} diff --git a/examples/getting_started_async/.butane/migrations/20201229_171630604_likes/lock b/examples/getting_started_async/.butane/migrations/20201229_171630604_likes/lock new file mode 100644 index 00000000..e69de29b diff --git a/examples/getting_started_async/.butane/migrations/20201229_171630604_likes/pg_down.sql b/examples/getting_started_async/.butane/migrations/20201229_171630604_likes/pg_down.sql new file mode 100644 index 00000000..d887cd1c --- /dev/null +++ b/examples/getting_started_async/.butane/migrations/20201229_171630604_likes/pg_down.sql @@ -0,0 +1 @@ +ALTER TABLE Post DROP COLUMN likes; diff --git a/examples/getting_started_async/.butane/migrations/20201229_171630604_likes/pg_up.sql b/examples/getting_started_async/.butane/migrations/20201229_171630604_likes/pg_up.sql new file mode 100644 index 00000000..48012f20 --- /dev/null +++ b/examples/getting_started_async/.butane/migrations/20201229_171630604_likes/pg_up.sql @@ -0,0 +1 @@ +ALTER TABLE Post ADD COLUMN likes INTEGER NOT NULL DEFAULT 0; diff --git a/examples/getting_started_async/.butane/migrations/20201229_171630604_likes/sqlite_down.sql b/examples/getting_started_async/.butane/migrations/20201229_171630604_likes/sqlite_down.sql new file mode 100644 index 00000000..deb10dc1 --- /dev/null +++ b/examples/getting_started_async/.butane/migrations/20201229_171630604_likes/sqlite_down.sql @@ -0,0 +1,11 @@ +CREATE TABLE Post__butane_tmp ( +id INTEGER NOT NULL PRIMARY KEY, +title TEXT NOT NULL, +body TEXT NOT NULL, +published INTEGER NOT NULL, +blog INTEGER NOT NULL, +byline TEXT +); +INSERT INTO Post__butane_tmp SELECT id, title, body, published, blog, byline FROM Post; +DROP TABLE Post; +ALTER TABLE Post__butane_tmp RENAME TO Post; diff --git a/examples/getting_started_async/.butane/migrations/20201229_171630604_likes/sqlite_up.sql b/examples/getting_started_async/.butane/migrations/20201229_171630604_likes/sqlite_up.sql new file mode 100644 index 00000000..48012f20 --- /dev/null +++ b/examples/getting_started_async/.butane/migrations/20201229_171630604_likes/sqlite_up.sql @@ -0,0 +1 @@ +ALTER TABLE Post ADD COLUMN likes INTEGER NOT NULL DEFAULT 0; diff --git a/examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/Post.table b/examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/Post.table new file mode 100644 index 00000000..c6be4646 --- /dev/null +++ b/examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/Post.table @@ -0,0 +1,102 @@ +{ + "name": "Post", + "columns": [ + { + "name": "id", + "sqltype": { + "KnownId": { + "Ty": "Int" + } + }, + "nullable": false, + "pk": true, + "auto": true, + "unique": false, + "default": null + }, + { + "name": "title", + "sqltype": { + "KnownId": { + "Ty": "Text" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "body", + "sqltype": { + "KnownId": { + "Ty": "Text" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "published", + "sqltype": { + "KnownId": { + "Ty": "Bool" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "blog", + "sqltype": { + "KnownId": { + "Ty": "BigInt" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null, + "reference": { + "Literal": { + "table_name": "Blog", + "column_name": "id" + } + } + }, + { + "name": "byline", + "sqltype": { + "KnownId": { + "Ty": "Text" + } + }, + "nullable": true, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "likes", + "sqltype": { + "KnownId": { + "Ty": "Int" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + } + ] +} diff --git a/examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/Post_tags_Many.table b/examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/Post_tags_Many.table new file mode 100644 index 00000000..d2a855fe --- /dev/null +++ b/examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/Post_tags_Many.table @@ -0,0 +1,43 @@ +{ + "name": "Post_tags_Many", + "columns": [ + { + "name": "owner", + "sqltype": { + "KnownId": { + "Ty": "Int" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null, + "reference": { + "Literal": { + "table_name": "Post", + "column_name": "id" + } + } + }, + { + "name": "has", + "sqltype": { + "KnownId": { + "Ty": "Text" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null, + "reference": { + "Literal": { + "table_name": "Tag", + "column_name": "tag" + } + } + } + ] +} diff --git a/examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/info.json b/examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/info.json new file mode 100644 index 00000000..570cea0e --- /dev/null +++ b/examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/info.json @@ -0,0 +1,11 @@ +{ + "from_name": "20201229_171630604_likes", + "table_bases": { + "Blog": "20201229_144636751_init", + "Tag": "20201229_144636751_init" + }, + "backends": [ + "sqlite", + "pg" + ] +} diff --git a/examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/lock b/examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/lock new file mode 100644 index 00000000..e69de29b diff --git a/examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/pg_down.sql b/examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/pg_down.sql new file mode 100644 index 00000000..b62d930f --- /dev/null +++ b/examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/pg_down.sql @@ -0,0 +1,27 @@ +CREATE TABLE Post__butane_tmp ( +id SERIAL NOT NULL PRIMARY KEY, +title TEXT NOT NULL, +body TEXT NOT NULL, +published BOOLEAN NOT NULL, +blog BIGINT NOT NULL, +byline TEXT , +likes INTEGER NOT NULL +); +INSERT INTO Post__butane_tmp SELECT id, title, body, published, blog, byline, likes FROM Post; +DROP TABLE Post; +ALTER TABLE Post__butane_tmp RENAME TO Post; +CREATE TABLE Post_tags_Many__butane_tmp ( +owner INTEGER NOT NULL, +has TEXT NOT NULL +); +ALTER TABLE Post_tags_Many__butane_tmp ADD FOREIGN KEY (owner) REFERENCES Post(id); +INSERT INTO Post_tags_Many__butane_tmp SELECT owner, has FROM Post_tags_Many; +DROP TABLE Post_tags_Many; +ALTER TABLE Post_tags_Many__butane_tmp RENAME TO Post_tags_Many; +CREATE TABLE Post_tags_Many__butane_tmp ( +owner INTEGER NOT NULL, +has TEXT NOT NULL +); +INSERT INTO Post_tags_Many__butane_tmp SELECT owner, has FROM Post_tags_Many; +DROP TABLE Post_tags_Many; +ALTER TABLE Post_tags_Many__butane_tmp RENAME TO Post_tags_Many; diff --git a/examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/pg_up.sql b/examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/pg_up.sql new file mode 100644 index 00000000..a023e1e0 --- /dev/null +++ b/examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/pg_up.sql @@ -0,0 +1,30 @@ +CREATE TABLE Post__butane_tmp ( +id SERIAL NOT NULL PRIMARY KEY, +title TEXT NOT NULL, +body TEXT NOT NULL, +published BOOLEAN NOT NULL, +blog BIGINT NOT NULL, +byline TEXT , +likes INTEGER NOT NULL +); +ALTER TABLE Post__butane_tmp ADD FOREIGN KEY (blog) REFERENCES Blog(id); +INSERT INTO Post__butane_tmp SELECT id, title, body, published, blog, byline, likes FROM Post; +DROP TABLE Post; +ALTER TABLE Post__butane_tmp RENAME TO Post; +CREATE TABLE Post_tags_Many__butane_tmp ( +owner INTEGER NOT NULL, +has TEXT NOT NULL +); +ALTER TABLE Post_tags_Many__butane_tmp ADD FOREIGN KEY (has) REFERENCES Tag(tag); +INSERT INTO Post_tags_Many__butane_tmp SELECT owner, has FROM Post_tags_Many; +DROP TABLE Post_tags_Many; +ALTER TABLE Post_tags_Many__butane_tmp RENAME TO Post_tags_Many; +CREATE TABLE Post_tags_Many__butane_tmp ( +owner INTEGER NOT NULL, +has TEXT NOT NULL +); +ALTER TABLE Post_tags_Many__butane_tmp ADD FOREIGN KEY (owner) REFERENCES Post(id); +ALTER TABLE Post_tags_Many__butane_tmp ADD FOREIGN KEY (has) REFERENCES Tag(tag); +INSERT INTO Post_tags_Many__butane_tmp SELECT owner, has FROM Post_tags_Many; +DROP TABLE Post_tags_Many; +ALTER TABLE Post_tags_Many__butane_tmp RENAME TO Post_tags_Many; diff --git a/examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/sqlite_down.sql b/examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/sqlite_down.sql new file mode 100644 index 00000000..31106ede --- /dev/null +++ b/examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/sqlite_down.sql @@ -0,0 +1,27 @@ +CREATE TABLE Post__butane_tmp ( +id INTEGER NOT NULL PRIMARY KEY, +title TEXT NOT NULL, +body TEXT NOT NULL, +published INTEGER NOT NULL, +blog INTEGER NOT NULL, +byline TEXT, +likes INTEGER NOT NULL +); +INSERT INTO Post__butane_tmp SELECT id, title, body, published, blog, byline, likes FROM Post; +DROP TABLE Post; +ALTER TABLE Post__butane_tmp RENAME TO Post; +CREATE TABLE Post_tags_Many__butane_tmp ( +owner INTEGER NOT NULL, +has TEXT NOT NULL, +FOREIGN KEY (owner) REFERENCES Post(id) +); +INSERT INTO Post_tags_Many__butane_tmp SELECT owner, has FROM Post_tags_Many; +DROP TABLE Post_tags_Many; +ALTER TABLE Post_tags_Many__butane_tmp RENAME TO Post_tags_Many; +CREATE TABLE Post_tags_Many__butane_tmp ( +owner INTEGER NOT NULL, +has TEXT NOT NULL +); +INSERT INTO Post_tags_Many__butane_tmp SELECT owner, has FROM Post_tags_Many; +DROP TABLE Post_tags_Many; +ALTER TABLE Post_tags_Many__butane_tmp RENAME TO Post_tags_Many; diff --git a/examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/sqlite_up.sql b/examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/sqlite_up.sql new file mode 100644 index 00000000..a4065c6e --- /dev/null +++ b/examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/sqlite_up.sql @@ -0,0 +1,30 @@ +CREATE TABLE Post__butane_tmp ( +id INTEGER NOT NULL PRIMARY KEY, +title TEXT NOT NULL, +body TEXT NOT NULL, +published INTEGER NOT NULL, +blog INTEGER NOT NULL, +byline TEXT, +likes INTEGER NOT NULL, +FOREIGN KEY (blog) REFERENCES Blog(id) +); +INSERT INTO Post__butane_tmp SELECT id, title, body, published, blog, byline, likes FROM Post; +DROP TABLE Post; +ALTER TABLE Post__butane_tmp RENAME TO Post; +CREATE TABLE Post_tags_Many__butane_tmp ( +owner INTEGER NOT NULL, +has TEXT NOT NULL, +FOREIGN KEY (has) REFERENCES Tag(tag) +); +INSERT INTO Post_tags_Many__butane_tmp SELECT owner, has FROM Post_tags_Many; +DROP TABLE Post_tags_Many; +ALTER TABLE Post_tags_Many__butane_tmp RENAME TO Post_tags_Many; +CREATE TABLE Post_tags_Many__butane_tmp ( +owner INTEGER NOT NULL, +has TEXT NOT NULL, +FOREIGN KEY (owner) REFERENCES Post(id) +FOREIGN KEY (has) REFERENCES Tag(tag) +); +INSERT INTO Post_tags_Many__butane_tmp SELECT owner, has FROM Post_tags_Many; +DROP TABLE Post_tags_Many; +ALTER TABLE Post_tags_Many__butane_tmp RENAME TO Post_tags_Many; diff --git a/examples/getting_started_async/.butane/migrations/current/.gitignore b/examples/getting_started_async/.butane/migrations/current/.gitignore new file mode 100644 index 00000000..4b67b6a0 --- /dev/null +++ b/examples/getting_started_async/.butane/migrations/current/.gitignore @@ -0,0 +1 @@ +lock diff --git a/examples/getting_started_async/.butane/migrations/current/Blog.table b/examples/getting_started_async/.butane/migrations/current/Blog.table new file mode 100644 index 00000000..6dbe6909 --- /dev/null +++ b/examples/getting_started_async/.butane/migrations/current/Blog.table @@ -0,0 +1,31 @@ +{ + "name": "Blog", + "columns": [ + { + "name": "id", + "sqltype": { + "KnownId": { + "Ty": "BigInt" + } + }, + "nullable": false, + "pk": true, + "auto": true, + "unique": false, + "default": null + }, + { + "name": "name", + "sqltype": { + "KnownId": { + "Ty": "Text" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + } + ] +} diff --git a/examples/getting_started_async/.butane/migrations/current/Post.table b/examples/getting_started_async/.butane/migrations/current/Post.table new file mode 100644 index 00000000..fc624a57 --- /dev/null +++ b/examples/getting_started_async/.butane/migrations/current/Post.table @@ -0,0 +1,99 @@ +{ + "name": "Post", + "columns": [ + { + "name": "id", + "sqltype": { + "KnownId": { + "Ty": "Int" + } + }, + "nullable": false, + "pk": true, + "auto": true, + "unique": false, + "default": null + }, + { + "name": "title", + "sqltype": { + "KnownId": { + "Ty": "Text" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "body", + "sqltype": { + "KnownId": { + "Ty": "Text" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "published", + "sqltype": { + "KnownId": { + "Ty": "Bool" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "blog", + "sqltype": { + "Deferred": "PK:Blog" + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null, + "reference": { + "Deferred": { + "Deferred": "PK:Blog" + } + } + }, + { + "name": "byline", + "sqltype": { + "KnownId": { + "Ty": "Text" + } + }, + "nullable": true, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "likes", + "sqltype": { + "KnownId": { + "Ty": "Int" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + } + ] +} diff --git a/examples/getting_started_async/.butane/migrations/current/Post_tags_Many.table b/examples/getting_started_async/.butane/migrations/current/Post_tags_Many.table new file mode 100644 index 00000000..36afc8ce --- /dev/null +++ b/examples/getting_started_async/.butane/migrations/current/Post_tags_Many.table @@ -0,0 +1,40 @@ +{ + "name": "Post_tags_Many", + "columns": [ + { + "name": "owner", + "sqltype": { + "KnownId": { + "Ty": "Int" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null, + "reference": { + "Literal": { + "table_name": "Post", + "column_name": "id" + } + } + }, + { + "name": "has", + "sqltype": { + "Deferred": "PK:Tag" + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null, + "reference": { + "Deferred": { + "Deferred": "PK:Tag" + } + } + } + ] +} diff --git a/examples/getting_started_async/.butane/migrations/current/Tag.table b/examples/getting_started_async/.butane/migrations/current/Tag.table new file mode 100644 index 00000000..784abd8b --- /dev/null +++ b/examples/getting_started_async/.butane/migrations/current/Tag.table @@ -0,0 +1,18 @@ +{ + "name": "Tag", + "columns": [ + { + "name": "tag", + "sqltype": { + "KnownId": { + "Ty": "Text" + } + }, + "nullable": false, + "pk": true, + "auto": false, + "unique": false, + "default": null + } + ] +} diff --git a/examples/getting_started_async/.butane/migrations/state.json b/examples/getting_started_async/.butane/migrations/state.json new file mode 100644 index 00000000..22f73476 --- /dev/null +++ b/examples/getting_started_async/.butane/migrations/state.json @@ -0,0 +1,3 @@ +{ + "latest": "20240115_023841384_dbconstraints" +} diff --git a/examples/getting_started_async/.gitignore b/examples/getting_started_async/.gitignore new file mode 100644 index 00000000..4ff5f9ef --- /dev/null +++ b/examples/getting_started_async/.gitignore @@ -0,0 +1,2 @@ +/example.db +/.butane/connection.json diff --git a/examples/getting_started_async/Cargo.toml b/examples/getting_started_async/Cargo.toml new file mode 100644 index 00000000..2179cdfc --- /dev/null +++ b/examples/getting_started_async/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "getting_started_async" +version = "0.1.0" +authors = ["James Oakley "] +license.workspace = true +edition.workspace = true +publish = false + +[[bin]] +name = "show_posts" +doc = false + +[[bin]] +name = "write_post" +doc = false + +[[bin]] +name = "delete_post" +doc = false + +[lib] +doc = false + +[features] +default = ["pg", "sqlite", "sqlite-bundled"] +pg = ["butane/pg"] +sqlite = ["butane/sqlite"] +sqlite-bundled = ["butane/sqlite-bundled"] + +[dependencies] +butane.workspace = true +tokio = { workspace = true, features = ["macros"] } + +[dev-dependencies] +butane_cli.workspace = true +butane_core.workspace = true +butane_test_helper.workspace = true +cfg-if.workspace = true +env_logger.workspace = true +log.workspace = true +paste.workspace = true + +[package.metadata.release] +release = false diff --git a/examples/getting_started_async/README.md b/examples/getting_started_async/README.md new file mode 100644 index 00000000..7044d6ee --- /dev/null +++ b/examples/getting_started_async/README.md @@ -0,0 +1,13 @@ +# butane `getting_started` example + +This is the async version of this example. There is also [a synchronous version](../getting_started) + +To use this example, build the entire project using `cargo build` in the project root, +and then run these commands in this directory: + +1. Initialise a Sqlite database using `cargo run -p butane_cli init sqlite db.sqlite` +2. Migrate the new sqlite database using `cargo run -p butane_cli migrate` +3. Run the commands, such as `cargo run --bin write_post` + +See [getting-started.md](https://github.com/Electron100/butane/blob/master/docs/getting-started.md) +for a detailed walkthrough of this example. diff --git a/examples/getting_started_async/src/bin/delete_post.rs b/examples/getting_started_async/src/bin/delete_post.rs new file mode 100644 index 00000000..0eeefbac --- /dev/null +++ b/examples/getting_started_async/src/bin/delete_post.rs @@ -0,0 +1,19 @@ +use std::env::args; + +use butane::prelude::*; +use butane::query; +use getting_started_async::models::Post; +use getting_started_async::*; + +#[tokio::main(flavor = "current_thread")] +async fn main() { + let target = args().nth(1).expect("Expected a target to match against"); + let pattern = format!("%{target}%"); + + let conn = establish_connection().await; + let cnt = query!(Post, title.like({ pattern })) + .delete(&conn) + .await + .expect("error deleting posts"); + println!("Deleted {cnt} posts"); +} diff --git a/examples/getting_started_async/src/bin/publish_post.rs b/examples/getting_started_async/src/bin/publish_post.rs new file mode 100644 index 00000000..cfbe99f8 --- /dev/null +++ b/examples/getting_started_async/src/bin/publish_post.rs @@ -0,0 +1,24 @@ +#![allow(clippy::expect_fun_call)] +use std::env::args; + +use butane::prelude::*; +use getting_started_async::models::Post; +use getting_started_async::*; + +#[tokio::main(flavor = "current_thread")] +async fn main() { + let id = args() + .nth(1) + .expect("publish_post requires a post id") + .parse::() + .expect("Invalid ID"); + let conn = establish_connection().await; + + let mut post = Post::get(&conn, id) + .await + .unwrap_or_else(|_| panic!("Unable to find post {id}")); + // Just a normal Rust assignment, no fancy set methods + post.published = true; + post.save(&conn).await.unwrap(); + println!("Published post {}", post.title); +} diff --git a/examples/getting_started_async/src/bin/show_posts.rs b/examples/getting_started_async/src/bin/show_posts.rs new file mode 100644 index 00000000..ccd67a6c --- /dev/null +++ b/examples/getting_started_async/src/bin/show_posts.rs @@ -0,0 +1,20 @@ +use butane::prelude::*; +use butane::query; +use getting_started_async::models::Post; +use getting_started_async::*; + +#[tokio::main(flavor = "current_thread")] +async fn main() { + let conn = establish_connection().await; + let results = query!(Post, published == true) + .limit(5) + .load(&conn) + .await + .expect("Error loading posts"); + println!("Displaying {} posts", results.len()); + for post in results { + println!("{} ({} likes)", post.title, post.likes); + println!("----------\n"); + println!("{}", post.body); + } +} diff --git a/examples/getting_started_async/src/bin/write_post.rs b/examples/getting_started_async/src/bin/write_post.rs new file mode 100644 index 00000000..c6823a9e --- /dev/null +++ b/examples/getting_started_async/src/bin/write_post.rs @@ -0,0 +1,42 @@ +use std::io::{stdin, Read}; + +use getting_started_async::*; + +#[tokio::main(flavor = "current_thread")] +async fn main() { + let conn = establish_connection().await; + + let blog = match existing_blog(&conn).await { + Some(blog) => blog, + None => { + println!("Enter blog name"); + let name = readline(); + create_blog(&conn, name).await + } + }; + + println!("Enter post title"); + let title = readline(); + println!("\nEnter text for {title} ({EOF} when finished)\n"); + let mut body = String::new(); + stdin().read_to_string(&mut body).unwrap(); + + let post = create_post(&conn, &blog, title, body).await; + println!( + "\nSaved unpublished post {} with id {}", + post.title, post.id + ); +} + +fn readline() -> String { + let mut s = String::new(); + stdin().read_line(&mut s).unwrap(); + s.pop(); // Drop the newline + s +} + +#[cfg(not(windows))] +const EOF: &str = "CTRL+D"; + +#[cfg(windows)] +const EOF: &str = "CTRL+Z"; diff --git a/examples/getting_started_async/src/butane_migrations.rs b/examples/getting_started_async/src/butane_migrations.rs new file mode 100644 index 00000000..5c045ef9 --- /dev/null +++ b/examples/getting_started_async/src/butane_migrations.rs @@ -0,0 +1,555 @@ +//! Butane migrations embedded in Rust. + +use butane::migrations::MemMigrations; + +/// Load the butane migrations embedded in Rust. +pub fn get_migrations() -> Result { + let json = r#"{ + "migrations": { + "20201229_144636751_init": { + "name": "20201229_144636751_init", + "db": { + "tables": { + "Blog": { + "name": "Blog", + "columns": [ + { + "name": "id", + "sqltype": { + "Known": "BigInt" + }, + "nullable": false, + "pk": true, + "auto": true, + "unique": false, + "default": null + }, + { + "name": "name", + "sqltype": { + "Known": "Text" + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + } + ] + }, + "Post": { + "name": "Post", + "columns": [ + { + "name": "id", + "sqltype": { + "Known": "Int" + }, + "nullable": false, + "pk": true, + "auto": true, + "unique": false, + "default": null + }, + { + "name": "title", + "sqltype": { + "Known": "Text" + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "body", + "sqltype": { + "Known": "Text" + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "published", + "sqltype": { + "Known": "Bool" + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "blog", + "sqltype": { + "Known": "BigInt" + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "byline", + "sqltype": { + "Known": "Text" + }, + "nullable": true, + "pk": false, + "auto": false, + "unique": false, + "default": null + } + ] + }, + "Post_tags_Many": { + "name": "Post_tags_Many", + "columns": [ + { + "name": "owner", + "sqltype": { + "Known": "Int" + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "has", + "sqltype": { + "Known": "Text" + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + } + ] + }, + "Tag": { + "name": "Tag", + "columns": [ + { + "name": "tag", + "sqltype": { + "Known": "Text" + }, + "nullable": false, + "pk": true, + "auto": false, + "unique": false, + "default": null + } + ] + } + }, + "extra_types": {} + }, + "from": null, + "up": { + "pg": "CREATE TABLE Blog (\nid BIGSERIAL NOT NULL PRIMARY KEY,\n\"name\" TEXT NOT NULL\n);\nCREATE TABLE Post (\nid SERIAL NOT NULL PRIMARY KEY,\ntitle TEXT NOT NULL,\nbody TEXT NOT NULL,\npublished BOOLEAN NOT NULL,\nblog BIGINT NOT NULL,\nbyline TEXT \n);\nCREATE TABLE Post_tags_Many (\nowner INTEGER NOT NULL,\nhas TEXT NOT NULL\n);\nCREATE TABLE Tag (\ntag TEXT NOT NULL PRIMARY KEY\n);\nCREATE TABLE IF NOT EXISTS butane_migrations (\n\"name\" TEXT NOT NULL PRIMARY KEY\n);\n", + "sqlite": "CREATE TABLE Blog (\nid INTEGER NOT NULL PRIMARY KEY,\n\"name\" TEXT NOT NULL\n);\nCREATE TABLE Post (\nid INTEGER NOT NULL PRIMARY KEY,\ntitle TEXT NOT NULL,\nbody TEXT NOT NULL,\npublished INTEGER NOT NULL,\nblog INTEGER NOT NULL,\nbyline TEXT\n);\nCREATE TABLE Post_tags_Many (\nowner INTEGER NOT NULL,\nhas TEXT NOT NULL\n);\nCREATE TABLE Tag (\ntag TEXT NOT NULL PRIMARY KEY\n);\nCREATE TABLE IF NOT EXISTS butane_migrations (\n\"name\" TEXT NOT NULL PRIMARY KEY\n);\n" + }, + "down": { + "pg": "DROP TABLE Blog;\nDROP TABLE Post;\nDROP TABLE Post_tags_Many;\nDROP TABLE Tag;\n", + "sqlite": "DROP TABLE Blog;\nDROP TABLE Post;\nDROP TABLE Post_tags_Many;\nDROP TABLE Tag;\n" + } + }, + "20201229_171630604_likes": { + "name": "20201229_171630604_likes", + "db": { + "tables": { + "Blog": { + "name": "Blog", + "columns": [ + { + "name": "id", + "sqltype": { + "Known": "BigInt" + }, + "nullable": false, + "pk": true, + "auto": true, + "unique": false, + "default": null + }, + { + "name": "name", + "sqltype": { + "Known": "Text" + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + } + ] + }, + "Post": { + "name": "Post", + "columns": [ + { + "name": "id", + "sqltype": { + "Known": "Int" + }, + "nullable": false, + "pk": true, + "auto": true, + "unique": false, + "default": null + }, + { + "name": "title", + "sqltype": { + "Known": "Text" + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "body", + "sqltype": { + "Known": "Text" + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "published", + "sqltype": { + "Known": "Bool" + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "blog", + "sqltype": { + "Known": "BigInt" + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "byline", + "sqltype": { + "Known": "Text" + }, + "nullable": true, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "likes", + "sqltype": { + "Known": "Int" + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + } + ] + }, + "Post_tags_Many": { + "name": "Post_tags_Many", + "columns": [ + { + "name": "owner", + "sqltype": { + "Known": "Int" + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "has", + "sqltype": { + "Known": "Text" + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + } + ] + }, + "Tag": { + "name": "Tag", + "columns": [ + { + "name": "tag", + "sqltype": { + "Known": "Text" + }, + "nullable": false, + "pk": true, + "auto": false, + "unique": false, + "default": null + } + ] + } + }, + "extra_types": {} + }, + "from": "20201229_144636751_init", + "up": { + "pg": "ALTER TABLE Post ADD COLUMN likes INTEGER NOT NULL DEFAULT 0;\n", + "sqlite": "ALTER TABLE Post ADD COLUMN likes INTEGER NOT NULL DEFAULT 0;\n" + }, + "down": { + "pg": "ALTER TABLE Post DROP COLUMN likes;\n", + "sqlite": "CREATE TABLE Post__butane_tmp (\nid INTEGER NOT NULL PRIMARY KEY,\ntitle TEXT NOT NULL,\nbody TEXT NOT NULL,\npublished INTEGER NOT NULL,\nblog INTEGER NOT NULL,\nbyline TEXT\n);\nINSERT INTO Post__butane_tmp SELECT id, title, body, published, blog, byline FROM Post;\nDROP TABLE Post;\nALTER TABLE Post__butane_tmp RENAME TO Post;\n" + } + }, + "20240115_023841384_dbconstraints": { + "name": "20240115_023841384_dbconstraints", + "db": { + "tables": { + "Blog": { + "name": "Blog", + "columns": [ + { + "name": "id", + "sqltype": { + "Known": "BigInt" + }, + "nullable": false, + "pk": true, + "auto": true, + "unique": false, + "default": null + }, + { + "name": "name", + "sqltype": { + "Known": "Text" + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + } + ] + }, + "Post": { + "name": "Post", + "columns": [ + { + "name": "id", + "sqltype": { + "KnownId": { + "Ty": "Int" + } + }, + "nullable": false, + "pk": true, + "auto": true, + "unique": false, + "default": null + }, + { + "name": "title", + "sqltype": { + "KnownId": { + "Ty": "Text" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "body", + "sqltype": { + "KnownId": { + "Ty": "Text" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "published", + "sqltype": { + "KnownId": { + "Ty": "Bool" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "blog", + "sqltype": { + "KnownId": { + "Ty": "BigInt" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null, + "reference": { + "Literal": { + "table_name": "Blog", + "column_name": "id" + } + } + }, + { + "name": "byline", + "sqltype": { + "KnownId": { + "Ty": "Text" + } + }, + "nullable": true, + "pk": false, + "auto": false, + "unique": false, + "default": null + }, + { + "name": "likes", + "sqltype": { + "KnownId": { + "Ty": "Int" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null + } + ] + }, + "Post_tags_Many": { + "name": "Post_tags_Many", + "columns": [ + { + "name": "owner", + "sqltype": { + "KnownId": { + "Ty": "Int" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null, + "reference": { + "Literal": { + "table_name": "Post", + "column_name": "id" + } + } + }, + { + "name": "has", + "sqltype": { + "KnownId": { + "Ty": "Text" + } + }, + "nullable": false, + "pk": false, + "auto": false, + "unique": false, + "default": null, + "reference": { + "Literal": { + "table_name": "Tag", + "column_name": "tag" + } + } + } + ] + }, + "Tag": { + "name": "Tag", + "columns": [ + { + "name": "tag", + "sqltype": { + "Known": "Text" + }, + "nullable": false, + "pk": true, + "auto": false, + "unique": false, + "default": null + } + ] + } + }, + "extra_types": {} + }, + "from": "20201229_171630604_likes", + "up": { + "pg": "CREATE TABLE Post__butane_tmp (\nid SERIAL NOT NULL PRIMARY KEY,\ntitle TEXT NOT NULL,\nbody TEXT NOT NULL,\npublished BOOLEAN NOT NULL,\nblog BIGINT NOT NULL,\nbyline TEXT ,\nlikes INTEGER NOT NULL\n);\nALTER TABLE Post__butane_tmp ADD FOREIGN KEY (blog) REFERENCES Blog(id);\nINSERT INTO Post__butane_tmp SELECT id, title, body, published, blog, byline, likes FROM Post;\nDROP TABLE Post;\nALTER TABLE Post__butane_tmp RENAME TO Post;\nCREATE TABLE Post_tags_Many__butane_tmp (\nowner INTEGER NOT NULL,\nhas TEXT NOT NULL\n);\nALTER TABLE Post_tags_Many__butane_tmp ADD FOREIGN KEY (has) REFERENCES Tag(tag);\nINSERT INTO Post_tags_Many__butane_tmp SELECT owner, has FROM Post_tags_Many;\nDROP TABLE Post_tags_Many;\nALTER TABLE Post_tags_Many__butane_tmp RENAME TO Post_tags_Many;\nCREATE TABLE Post_tags_Many__butane_tmp (\nowner INTEGER NOT NULL,\nhas TEXT NOT NULL\n);\nALTER TABLE Post_tags_Many__butane_tmp ADD FOREIGN KEY (owner) REFERENCES Post(id);\nALTER TABLE Post_tags_Many__butane_tmp ADD FOREIGN KEY (has) REFERENCES Tag(tag);\nINSERT INTO Post_tags_Many__butane_tmp SELECT owner, has FROM Post_tags_Many;\nDROP TABLE Post_tags_Many;\nALTER TABLE Post_tags_Many__butane_tmp RENAME TO Post_tags_Many;\n", + "sqlite": "CREATE TABLE Post__butane_tmp (\nid INTEGER NOT NULL PRIMARY KEY,\ntitle TEXT NOT NULL,\nbody TEXT NOT NULL,\npublished INTEGER NOT NULL,\nblog INTEGER NOT NULL,\nbyline TEXT,\nlikes INTEGER NOT NULL,\nFOREIGN KEY (blog) REFERENCES Blog(id)\n);\nINSERT INTO Post__butane_tmp SELECT id, title, body, published, blog, byline, likes FROM Post;\nDROP TABLE Post;\nALTER TABLE Post__butane_tmp RENAME TO Post;\nCREATE TABLE Post_tags_Many__butane_tmp (\nowner INTEGER NOT NULL,\nhas TEXT NOT NULL,\nFOREIGN KEY (has) REFERENCES Tag(tag)\n);\nINSERT INTO Post_tags_Many__butane_tmp SELECT owner, has FROM Post_tags_Many;\nDROP TABLE Post_tags_Many;\nALTER TABLE Post_tags_Many__butane_tmp RENAME TO Post_tags_Many;\nCREATE TABLE Post_tags_Many__butane_tmp (\nowner INTEGER NOT NULL,\nhas TEXT NOT NULL,\nFOREIGN KEY (owner) REFERENCES Post(id)\nFOREIGN KEY (has) REFERENCES Tag(tag)\n);\nINSERT INTO Post_tags_Many__butane_tmp SELECT owner, has FROM Post_tags_Many;\nDROP TABLE Post_tags_Many;\nALTER TABLE Post_tags_Many__butane_tmp RENAME TO Post_tags_Many;\n" + }, + "down": { + "pg": "CREATE TABLE Post__butane_tmp (\nid SERIAL NOT NULL PRIMARY KEY,\ntitle TEXT NOT NULL,\nbody TEXT NOT NULL,\npublished BOOLEAN NOT NULL,\nblog BIGINT NOT NULL,\nbyline TEXT ,\nlikes INTEGER NOT NULL\n);\nINSERT INTO Post__butane_tmp SELECT id, title, body, published, blog, byline, likes FROM Post;\nDROP TABLE Post;\nALTER TABLE Post__butane_tmp RENAME TO Post;\nCREATE TABLE Post_tags_Many__butane_tmp (\nowner INTEGER NOT NULL,\nhas TEXT NOT NULL\n);\nALTER TABLE Post_tags_Many__butane_tmp ADD FOREIGN KEY (owner) REFERENCES Post(id);\nINSERT INTO Post_tags_Many__butane_tmp SELECT owner, has FROM Post_tags_Many;\nDROP TABLE Post_tags_Many;\nALTER TABLE Post_tags_Many__butane_tmp RENAME TO Post_tags_Many;\nCREATE TABLE Post_tags_Many__butane_tmp (\nowner INTEGER NOT NULL,\nhas TEXT NOT NULL\n);\nINSERT INTO Post_tags_Many__butane_tmp SELECT owner, has FROM Post_tags_Many;\nDROP TABLE Post_tags_Many;\nALTER TABLE Post_tags_Many__butane_tmp RENAME TO Post_tags_Many;\n", + "sqlite": "CREATE TABLE Post__butane_tmp (\nid INTEGER NOT NULL PRIMARY KEY,\ntitle TEXT NOT NULL,\nbody TEXT NOT NULL,\npublished INTEGER NOT NULL,\nblog INTEGER NOT NULL,\nbyline TEXT,\nlikes INTEGER NOT NULL\n);\nINSERT INTO Post__butane_tmp SELECT id, title, body, published, blog, byline, likes FROM Post;\nDROP TABLE Post;\nALTER TABLE Post__butane_tmp RENAME TO Post;\nCREATE TABLE Post_tags_Many__butane_tmp (\nowner INTEGER NOT NULL,\nhas TEXT NOT NULL,\nFOREIGN KEY (owner) REFERENCES Post(id)\n);\nINSERT INTO Post_tags_Many__butane_tmp SELECT owner, has FROM Post_tags_Many;\nDROP TABLE Post_tags_Many;\nALTER TABLE Post_tags_Many__butane_tmp RENAME TO Post_tags_Many;\nCREATE TABLE Post_tags_Many__butane_tmp (\nowner INTEGER NOT NULL,\nhas TEXT NOT NULL\n);\nINSERT INTO Post_tags_Many__butane_tmp SELECT owner, has FROM Post_tags_Many;\nDROP TABLE Post_tags_Many;\nALTER TABLE Post_tags_Many__butane_tmp RENAME TO Post_tags_Many;\n" + } + } + }, + "current": { + "name": "current", + "db": { + "tables": {}, + "extra_types": {} + }, + "from": null, + "up": {}, + "down": {} + }, + "latest": "20240115_023841384_dbconstraints" +}"#; + MemMigrations::from_json(json) +} diff --git a/examples/getting_started_async/src/lib.rs b/examples/getting_started_async/src/lib.rs new file mode 100644 index 00000000..438867bc --- /dev/null +++ b/examples/getting_started_async/src/lib.rs @@ -0,0 +1,43 @@ +//! Common helpers for the getting_started example CLI. + +#![deny(missing_docs)] + +pub mod butane_migrations; +pub mod models; + +use butane::db::{Connection, ConnectionSpec}; +use butane::migrations; +use butane::prelude::*; +use models::{Blog, Post}; + +/// Load a [Connection]. +pub async fn establish_connection() -> Connection { + let mut connection = + butane::db::connect_async(&ConnectionSpec::load(".butane/connection.json").unwrap()) + .await + .unwrap(); + let migrations = butane_migrations::get_migrations().unwrap(); + migrations::apply_unapplied_migrations_async(migrations, &mut connection) + .await + .unwrap(); + connection +} + +/// Create a [Blog]. +pub async fn create_blog(conn: &Connection, name: impl Into) -> Blog { + let mut blog = Blog::new(name); + blog.save(conn).await.unwrap(); + blog +} + +/// Create a [Post]. +pub async fn create_post(conn: &Connection, blog: &Blog, title: String, body: String) -> Post { + let mut new_post = Post::new(blog, title, body); + new_post.save(conn).await.unwrap(); + new_post +} + +/// Fetch the first existing [Blog] if one exists. +pub async fn existing_blog(conn: &Connection) -> Option { + Blog::query().load_first(conn).await.unwrap() +} diff --git a/examples/getting_started_async/src/models.rs b/examples/getting_started_async/src/models.rs new file mode 100644 index 00000000..c5655bbf --- /dev/null +++ b/examples/getting_started_async/src/models.rs @@ -0,0 +1,76 @@ +//! Models for the getting_started example. + +use butane::prelude::*; +use butane::AutoPk; +use butane::{model, ForeignKey, Many}; + +/// Blog metadata. +#[model] +#[derive(Debug, Default)] +pub struct Blog { + /// Id of the blog. + pub id: AutoPk, + /// Name of the blog. + pub name: String, +} +impl Blog { + /// Create a new Blog. + pub fn new(name: impl Into) -> Self { + Blog { + name: name.into(), + ..Default::default() + } + } +} + +/// Post details, including a [ForeignKey] to [Blog] +/// and a [Many] relationship to [Tag]s. +#[model] +pub struct Post { + /// Id of the blog post. + pub id: AutoPk, + /// Title of the blog post. + pub title: String, + /// Body of the blog post. + pub body: String, + /// Whether the blog post has been published. + pub published: bool, + /// Tags for the blog post. + pub tags: Many, + /// The [Blog] this post is attached to. + pub blog: ForeignKey, + /// Byline of the post. + pub byline: Option, + /// How many likes this post has. + pub likes: i32, +} +impl Post { + /// Create a new Post. + pub fn new(blog: &Blog, title: String, body: String) -> Self { + Post { + id: AutoPk::uninitialized(), + title, + body, + published: false, + tags: Many::default(), + blog: blog.into(), + byline: None, + likes: 0, + } + } +} + +/// Tags to be associated with a [Post]. +#[model] +#[derive(Debug, Default)] +pub struct Tag { + /// Tag name. + #[pk] + pub tag: String, +} +impl Tag { + /// Create a new Tag. + pub fn new(tag: impl Into) -> Self { + Tag { tag: tag.into() } + } +} diff --git a/examples/getting_started_async/tests/rollback.rs b/examples/getting_started_async/tests/rollback.rs new file mode 100644 index 00000000..2da3fd85 --- /dev/null +++ b/examples/getting_started_async/tests/rollback.rs @@ -0,0 +1,71 @@ +use butane::db::{BackendConnection, Connection}; +use butane::migrations::{Migration, Migrations}; +use butane::DataObjectOpAsync; +use butane_test_helper::*; + +use getting_started::models::{Blog, Post, Tag}; + +async fn create_tag(connection: &Connection, name: &str) -> Tag { + let mut tag = Tag::new(name); + tag.save(connection).await.unwrap(); + tag +} + +async fn insert_data(connection: &Connection) { + if connection.backend_name() == "sqlite" { + // https://github.com/Electron100/butane/issues/226 + return; + } + let mut cats_blog = Blog::new("Cats"); + cats_blog.save(connection).await.unwrap(); + + let tag_asia = create_tag(connection, "asia").await; + let tag_danger = create_tag(connection, "danger").await; + + let mut post = Post::new( + &cats_blog, + "The Tiger".to_string(), + "The tiger is a cat which would very much like to eat you.".to_string(), + ); + post.published = true; + post.likes = 4; + post.tags.add(&tag_danger).unwrap(); + post.tags.add(&tag_asia).unwrap(); + post.save(connection).await.unwrap(); +} + +async fn migrate_and_rollback(mut connection: Connection) { + // Migrate forward. + let base_dir = std::path::PathBuf::from(".butane"); + let migrations = butane_cli::get_migrations(&base_dir).unwrap(); + let to_apply = migrations.unapplied_migrations(&connection).await.unwrap(); + for migration in &to_apply { + if connection.backend_name() == "pg" + && migration.name() == "20240115_023841384_dbconstraints" + { + // migration 20240115_023841384_dbconstraints failed: Postgres error db error: + // ERROR: cannot drop table tag because other objects depend on it + // DETAIL: constraint post_tags_many__butane_tmp_has_fkey1 on table post_tags_many depends on table tag + let err = migration.apply(&mut connection).await.unwrap_err(); + eprintln!("Migration {} failed: {err:?}", migration.name()); + return; + } + migration + .apply(&mut connection) + .await + .unwrap_or_else(|err| panic!("migration {} failed: {err}", migration.name())); + eprintln!("Applied {}", migration.name()); + } + + insert_data(&connection).await; + + // Rollback migrations. + for migration in to_apply.iter().rev() { + migration + .downgrade(&mut connection) + .await + .unwrap_or_else(|err| panic!("rollback of {} failed: {err}", migration.name())); + eprintln!("Rolled back {}", migration.name()); + } +} +testall_no_migrate!(migrate_and_rollback); From 5a87d90a25bafee276e6af08271d8d14255fc97a Mon Sep 17 00:00:00 2001 From: James Oakley Date: Sun, 14 Jul 2024 21:37:49 -0400 Subject: [PATCH 37/78] Progress --- butane/src/lib.rs | 33 +++++--- butane_cli/src/lib.rs | 81 ++++++++----------- butane_core/src/fkey.rs | 1 + butane_core/src/lib.rs | 35 ++++---- butane_core/src/migrations/mod.rs | 12 ++- example/src/main.rs | 2 +- .../getting_started/src/bin/delete_post.rs | 2 +- .../getting_started/src/bin/publish_post.rs | 7 +- examples/getting_started/src/lib.rs | 3 +- .../src/bin/delete_post.rs | 2 +- .../src/bin/publish_post.rs | 2 +- .../src/bin/show_posts.rs | 2 +- examples/getting_started_async/src/lib.rs | 2 +- examples/newtype/src/lib.rs | 4 +- 14 files changed, 98 insertions(+), 90 deletions(-) diff --git a/butane/src/lib.rs b/butane/src/lib.rs index 291160b6..e6bdb7d4 100644 --- a/butane/src/lib.rs +++ b/butane/src/lib.rs @@ -172,21 +172,34 @@ macro_rules! find { }; } -pub mod prelude { - //! Prelude module to improve ergonomics. - //! - //! Its use is recommended, but not required. If not used, the use - //! of butane's macros may require some of its re-exports to be - //! used manually. - pub use butane_core::db::BackendConnection; - +mod prelude_common { #[doc(no_inline)] pub use crate::DataObject; #[doc(no_inline)] pub use crate::DataResult; +} + +pub mod prelude { + //! Prelude module to improve ergonomics. Brings certain traits into scope. + //! This module is for sync operation. For asynchronous, see [`prelude_async`]. + //! + //! Its use is recommended, but not required. + + pub use super::prelude_common::*; + + pub use butane_core::db::sync::BackendConnection; + pub use butane_core::many::ManyOpSync; + pub use butane_core::query::QueryOpSync; + pub use butane_core::DataObjectOpSync; +} - // todo should it be sync or async in prelude - // (can't be both or call sites will give "multiple applicable items in scope" +pub mod prelude_async { + //! Prelude module to improve ergonomics in async operation. Brings certain traits into scope. + //! + //! Its use is recommended, but not required. + pub use super::prelude_common::*; + + pub use butane_core::db::BackendConnection; pub use butane_core::many::ManyOpAsync; pub use butane_core::query::QueryOpAsync; pub use butane_core::DataObjectOpAsync; diff --git a/butane_cli/src/lib.rs b/butane_cli/src/lib.rs index 0beee948..2d717dc3 100644 --- a/butane_cli/src/lib.rs +++ b/butane_cli/src/lib.rs @@ -13,13 +13,14 @@ use std::{ path::{Path, PathBuf}, }; +use butane::db::sync::{Backend, Connection, ConnectionMethods}; use butane::migrations::adb; use butane::migrations::adb::{diff, AColumn, ARef, Operation, ADB}; use butane::migrations::{ copy_migration, FsMigrations, MemMigrations, Migration, MigrationMut, Migrations, MigrationsMut, }; use butane::query::BoolExpr; -use butane::{db, db::Connection, db::ConnectionMethods, migrations}; +use butane::{db, migrations}; use cargo_metadata::MetadataCommand; use chrono::Utc; use nonempty::NonEmpty; @@ -55,7 +56,7 @@ pub fn default_name() -> String { Utc::now().format("%Y%m%d_%H%M%S%3f").to_string() } -pub async fn init(base_dir: &PathBuf, name: &str, connstr: &str, connect: bool) -> Result<()> { +pub fn init(base_dir: &PathBuf, name: &str, connstr: &str, connect: bool) -> Result<()> { if db::get_async_backend(name).is_none() { eprintln!("Unknown backend {name}"); std::process::exit(1); @@ -63,7 +64,7 @@ pub async fn init(base_dir: &PathBuf, name: &str, connstr: &str, connect: bool) let spec = db::ConnectionSpec::new(name, connstr); if connect { - db::connect_async(&spec).await?; + db::connect(&spec)?; } std::fs::create_dir_all(base_dir)?; spec.save(base_dir)?; @@ -259,16 +260,14 @@ pub fn detach_latest_migration(base_dir: &PathBuf) -> Result<()> { Ok(()) } -pub async fn migrate(base_dir: &PathBuf, name: Option) -> Result<()> { +pub fn migrate(base_dir: &PathBuf, name: Option) -> Result<()> { let spec = load_connspec(base_dir)?; - let mut conn = db::connect_async(&spec).await?; - let to_apply = get_migrations(base_dir)? - .unapplied_migrations(&conn) - .await?; + let mut conn = db::connect(&spec)?; + let to_apply = get_migrations(base_dir)?.unapplied_migrations(&conn)?; println!("{} migrations to apply", to_apply.len()); for m in to_apply { println!("Applying migration {}", m.name()); - m.apply(&mut conn).await?; + m.apply(&mut conn)?; if let Some(ref name) = name { if name == &m.name().to_string() { println!("Finishing at migration {}", m.name()); @@ -279,17 +278,17 @@ pub async fn migrate(base_dir: &PathBuf, name: Option) -> Result<()> { Ok(()) } -pub async fn rollback(base_dir: &PathBuf, name: Option) -> Result<()> { +pub fn rollback(base_dir: &PathBuf, name: Option) -> Result<()> { let spec = load_connspec(base_dir)?; - let conn = butane::db::connect_async(&spec).await?; + let conn = butane::db::connect(&spec)?; match name { - Some(to) => rollback_to(base_dir, conn, &to).await, - None => rollback_latest(base_dir, conn).await, + Some(to) => rollback_to(base_dir, conn, &to), + None => rollback_latest(base_dir, conn), } } -pub async fn rollback_to(base_dir: &Path, mut conn: Connection, to: &str) -> Result<()> { +pub fn rollback_to(base_dir: &Path, mut conn: Connection, to: &str) -> Result<()> { let ms = get_migrations(base_dir)?; let to_migration = match ms.get_migration(to) { Some(m) => m, @@ -301,7 +300,6 @@ pub async fn rollback_to(base_dir: &Path, mut conn: Connection, to: &str) -> Res let latest = ms .last_applied_migration(&conn) - .await .unwrap_or_else(|err| { eprintln!("Err: {err}"); std::process::exit(1); @@ -334,19 +332,16 @@ If you expected something to happen, try specifying the migration to rollback to for m in to_unapply.into_iter().rev() { println!("Rolling back migration {}", m.name()); - m.downgrade(&mut conn).await?; + m.downgrade(&mut conn)?; } Ok(()) } -pub async fn rollback_latest(base_dir: &Path, mut conn: Connection) -> Result<()> { - match get_migrations(base_dir)? - .last_applied_migration(&conn) - .await? - { +pub fn rollback_latest(base_dir: &Path, mut conn: Connection) -> Result<()> { + match get_migrations(base_dir)?.last_applied_migration(&conn)? { Some(m) => { println!("Rolling back migration {}", m.name()); - m.downgrade(&mut conn).await?; + m.downgrade(&mut conn)?; } None => { eprintln!("No migrations applied!"); @@ -529,7 +524,7 @@ pub fn regenerate_migrations(base_dir: &Path) -> Result<()> { /// Load the [`db::Backend`]s used in the latest migration. /// Error if there are no existing migrations. -pub fn load_latest_migration_backends(base_dir: &Path) -> Result>> { +pub fn load_latest_migration_backends(base_dir: &Path) -> Result>> { if let Ok(ms) = get_migrations(base_dir) { if let Some(latest_migration) = ms.latest() { let backend_names = latest_migration.sql_backends()?; @@ -539,16 +534,16 @@ pub fn load_latest_migration_backends(base_dir: &Path) -> Result> = vec![]; + let mut backends: Vec> = vec![]; for backend_name in backend_names { backends.push( - db::get_async_backend(&backend_name) + db::get_sync_backend(&backend_name) .ok_or(anyhow::anyhow!("Backend {backend_name} not found"))?, ); } - return Ok(NonEmpty::>::from_vec(backends).unwrap()); + return Ok(NonEmpty::>::from_vec(backends).unwrap()); } } Err(anyhow::anyhow!("There are no exiting migrations.")) @@ -556,7 +551,7 @@ pub fn load_latest_migration_backends(base_dir: &Path) -> Result Result>> { +pub fn load_backends(base_dir: &Path) -> Result>> { // Try to use the same backends as the latest migration. let backends = load_latest_migration_backends(base_dir); if backends.is_ok() { @@ -565,7 +560,7 @@ pub fn load_backends(base_dir: &Path) -> Result>> // Otherwise use the backend used during `init`. if let Ok(spec) = db::ConnectionSpec::load(base_dir) { - return Ok(nonempty::nonempty![spec.get_backend().unwrap()]); + return Ok(nonempty::nonempty![spec.get_sync_backend().unwrap()]); } Err(anyhow::anyhow!( @@ -573,11 +568,11 @@ pub fn load_backends(base_dir: &Path) -> Result>> )) } -pub async fn list_migrations(base_dir: &PathBuf) -> Result<()> { +pub fn list_migrations(base_dir: &PathBuf) -> Result<()> { let spec = load_connspec(base_dir)?; - let conn = db::connect(&spec).await?; + let conn = db::connect(&spec)?; let ms = get_migrations(base_dir)?; - let unapplied = ms.unapplied_migrations(&conn).await?; + let unapplied = ms.unapplied_migrations(&conn)?; let all = ms.all_migrations()?; for m in all { let m_state = if unapplied.contains(&m) { @@ -591,10 +586,7 @@ pub async fn list_migrations(base_dir: &PathBuf) -> Result<()> { } /// Collapse multiple applied migrations into a new migration. -pub async fn collapse_migrations( - base_dir: &PathBuf, - new_initial_name: Option<&String>, -) -> Result<()> { +pub fn collapse_migrations(base_dir: &PathBuf, new_initial_name: Option<&String>) -> Result<()> { let name = match new_initial_name { Some(name) => format!("{}_{}", default_name(), name), None => default_name(), @@ -622,18 +614,18 @@ pub async fn collapse_migrations( // TODO: it should also be possible to collapse migrations on the filesystem // when the database hasnt been migrated at all. let spec = load_connspec(base_dir)?; - let conn = db::connect(&spec).await?; - let latest = ms.last_applied_migration(&conn).await?; + let conn = db::connect(&spec)?; + let latest = ms.last_applied_migration(&conn)?; if latest.is_none() { eprintln!("There are no applied migrations to collapse"); std::process::exit(1); } let latest_db = latest.unwrap().db()?; - ms.clear_migrations(&conn).await?; + ms.clear_migrations(&conn)?; ms.create_migration_to(&backends, &name, None, latest_db)?; let new_migration = ms.latest().unwrap(); - new_migration.mark_applied(&conn).await?; + new_migration.mark_applied(&conn)?; update_embedded(base_dir)?; @@ -648,13 +640,10 @@ pub fn delete_table(base_dir: &Path, name: &str) -> Result<()> { Ok(()) } -pub async fn clear_data(base_dir: &PathBuf) -> Result<()> { +pub fn clear_data(base_dir: &PathBuf) -> Result<()> { let spec = load_connspec(base_dir)?; - let conn = db::connect(&spec).await?; - let latest = match get_migrations(base_dir)? - .last_applied_migration(&conn) - .await? - { + let conn = db::connect(&spec)?; + let latest = match get_migrations(base_dir)?.last_applied_migration(&conn)? { Some(m) => m, None => { eprintln!("No migrations have been applied, so no data is recognized."); @@ -663,7 +652,7 @@ pub async fn clear_data(base_dir: &PathBuf) -> Result<()> { }; for table in latest.db()?.tables() { println!("Deleting data from {}", &table.name); - conn.delete_where(&table.name, BoolExpr::True).await?; + conn.delete_where(&table.name, BoolExpr::True)?; } Ok(()) } diff --git a/butane_core/src/fkey.rs b/butane_core/src/fkey.rs index 557184c8..639e789a 100644 --- a/butane_core/src/fkey.rs +++ b/butane_core/src/fkey.rs @@ -86,6 +86,7 @@ impl ForeignKey { /// Loads the value referred to by this foreign key from the /// database if necessary and returns a reference to it. pub async fn load(&self, conn: &impl crate::ConnectionMethods) -> Result<&T> { + use crate::DataObjectOpAsync; self.val .get_or_try_init(|| async { let pk = self.valpk.get().unwrap(); diff --git a/butane_core/src/lib.rs b/butane_core/src/lib.rs index 6a2a0a8f..f180a8e7 100644 --- a/butane_core/src/lib.rs +++ b/butane_core/src/lib.rs @@ -107,27 +107,40 @@ pub trait DataObject: DataResult + internal::DataObjectInternal + Sy /// Get the primary key fn pk(&self) -> &Self::PKType; +} +/// [`DataObject`] operations that require a live database connection. +#[allow(async_fn_in_trait)] // Implementation is intended to be through procmacro +#[maybe_async_cfg::maybe( + idents( + ConnectionMethods(sync = "ConnectionMethodsSync", async), + save_many_to_many(sync = "save_many_to_many_sync", async = "save_many_to_many_async"), + QueryOp, + ), + sync(), + async() +)] +pub trait DataObjectOp { /// Find this object in the database based on primary key. /// Returns `Error::NoSuchObject` if the primary key does not exist. async fn get(conn: &impl ConnectionMethods, id: impl ToSql) -> Result where - Self: Sized, + Self: DataObject + Sized, Self::PKType: Sync, { Self::try_get(conn, id).await?.ok_or(Error::NoSuchObject) } + /// Find this object in the database based on primary key. /// Returns `None` if the primary key does not exist. async fn try_get(conn: &impl ConnectionMethods, id: impl ToSql) -> Result> where - Self: Sized, + Self: DataObject + Sized, { - // todo make sync and async variants - use crate::query::QueryOpAsync; + use crate::query::QueryOp; Ok(::query() .filter(query::BoolExpr::Eq( - Self::PKCOL, + T::PKCOL, query::Expr::Val(id.borrow().to_sql()), )) .limit(1) @@ -136,19 +149,7 @@ pub trait DataObject: DataResult + internal::DataObjectInternal + Sy .into_iter() .nth(0)) } -} -/// [`DataObject`] operations that require a live database connection. -#[allow(async_fn_in_trait)] // Implementation is intended to be through procmacro -#[maybe_async_cfg::maybe( - idents( - ConnectionMethods(sync = "ConnectionMethodsSync", async), - save_many_to_many(sync = "save_many_to_many_sync", async = "save_many_to_many_async") - ), - sync(), - async() -)] -pub trait DataObjectOp { /// Save the object to the database. async fn save(&mut self, conn: &impl ConnectionMethods) -> Result<()> where diff --git a/butane_core/src/migrations/mod.rs b/butane_core/src/migrations/mod.rs index 875191c1..988dfdfb 100644 --- a/butane_core/src/migrations/mod.rs +++ b/butane_core/src/migrations/mod.rs @@ -8,7 +8,8 @@ use std::path::Path; use fallible_iterator::FallibleIterator; use nonempty::NonEmpty; -use crate::db::{BackendRows, Column, ConnectionMethods}; +use crate::db::sync::Backend; +use crate::db::{BackendRows, Column}; use crate::sqlval::{FromSql, SqlValRef, ToSql}; use crate::{db, query, DataObject, DataResult, Error, PrimaryKeyType, Result, SqlType}; @@ -189,7 +190,7 @@ where /// Returns true if a migration was created, false if `from` and `current` represent identical states. fn create_migration( &mut self, - backends: &NonEmpty>, + backends: &NonEmpty>, name: &str, from: Option<&Self::M>, ) -> Result { @@ -202,7 +203,7 @@ where /// Returns true if a migration was created, false if `from` and `current` represent identical states. fn create_migration_to( &mut self, - backends: &NonEmpty>, + backends: &NonEmpty>, name: &str, from: Option<&Self::M>, to_db: ADB, @@ -353,7 +354,10 @@ impl crate::internal::DataObjectInternal for ButaneMigration { } values } - async fn save_many_to_many_async(&mut self, _conn: &impl ConnectionMethods) -> Result<()> { + async fn save_many_to_many_async( + &mut self, + _conn: &impl crate::db::ConnectionMethods, + ) -> Result<()> { Ok(()) // no-op } fn save_many_to_many_sync( diff --git a/example/src/main.rs b/example/src/main.rs index 635aca3f..f1e4a3db 100644 --- a/example/src/main.rs +++ b/example/src/main.rs @@ -1,6 +1,6 @@ //! Simple example with all code in a single file. use butane::db::{Connection, ConnectionSpec}; -use butane::prelude::*; +use butane::prelude_async::*; use butane::{find, model, query, AutoPk, Error, ForeignKey, Many}; type Result = std::result::Result; diff --git a/examples/getting_started/src/bin/delete_post.rs b/examples/getting_started/src/bin/delete_post.rs index 845c34ef..596f824d 100644 --- a/examples/getting_started/src/bin/delete_post.rs +++ b/examples/getting_started/src/bin/delete_post.rs @@ -9,7 +9,7 @@ fn main() { let target = args().nth(1).expect("Expected a target to match against"); let pattern = format!("%{target}%"); - let conn = establish_connection() + let conn = establish_connection(); let cnt = query!(Post, title.like({ pattern })) .delete(&conn) .expect("error deleting posts"); diff --git a/examples/getting_started/src/bin/publish_post.rs b/examples/getting_started/src/bin/publish_post.rs index 8cb59929..6073d7f4 100644 --- a/examples/getting_started/src/bin/publish_post.rs +++ b/examples/getting_started/src/bin/publish_post.rs @@ -5,16 +5,15 @@ use butane::prelude::*; use getting_started::models::Post; use getting_started::*; -async fn main() { +fn main() { let id = args() .nth(1) .expect("publish_post requires a post id") .parse::() .expect("Invalid ID"); - let conn = establish_connection() + let conn = establish_connection(); - let mut post = Post::get(&conn, id) - .unwrap_or_else(|_| panic!("Unable to find post {id}")); + let mut post = Post::get(&conn, id).unwrap_or_else(|_| panic!("Unable to find post {id}")); // Just a normal Rust assignment, no fancy set methods post.published = true; post.save(&conn).unwrap(); diff --git a/examples/getting_started/src/lib.rs b/examples/getting_started/src/lib.rs index dcacc12c..b8f98d73 100644 --- a/examples/getting_started/src/lib.rs +++ b/examples/getting_started/src/lib.rs @@ -5,7 +5,8 @@ pub mod butane_migrations; pub mod models; -use butane::db::{Connection, ConnectionSpec}; +use butane::db::sync::Connection; +use butane::db::ConnectionSpec; use butane::migrations::{Migration, Migrations}; use butane::prelude::*; use models::{Blog, Post}; diff --git a/examples/getting_started_async/src/bin/delete_post.rs b/examples/getting_started_async/src/bin/delete_post.rs index 0eeefbac..db3d3284 100644 --- a/examples/getting_started_async/src/bin/delete_post.rs +++ b/examples/getting_started_async/src/bin/delete_post.rs @@ -1,6 +1,6 @@ use std::env::args; -use butane::prelude::*; +use butane::prelude_async::*; use butane::query; use getting_started_async::models::Post; use getting_started_async::*; diff --git a/examples/getting_started_async/src/bin/publish_post.rs b/examples/getting_started_async/src/bin/publish_post.rs index cfbe99f8..f69f1ca2 100644 --- a/examples/getting_started_async/src/bin/publish_post.rs +++ b/examples/getting_started_async/src/bin/publish_post.rs @@ -1,7 +1,7 @@ #![allow(clippy::expect_fun_call)] use std::env::args; -use butane::prelude::*; +use butane::prelude_async::*; use getting_started_async::models::Post; use getting_started_async::*; diff --git a/examples/getting_started_async/src/bin/show_posts.rs b/examples/getting_started_async/src/bin/show_posts.rs index ccd67a6c..3f86f09f 100644 --- a/examples/getting_started_async/src/bin/show_posts.rs +++ b/examples/getting_started_async/src/bin/show_posts.rs @@ -1,4 +1,4 @@ -use butane::prelude::*; +use butane::prelude_async::*; use butane::query; use getting_started_async::models::Post; use getting_started_async::*; diff --git a/examples/getting_started_async/src/lib.rs b/examples/getting_started_async/src/lib.rs index 438867bc..6ebbd3ba 100644 --- a/examples/getting_started_async/src/lib.rs +++ b/examples/getting_started_async/src/lib.rs @@ -7,7 +7,7 @@ pub mod models; use butane::db::{Connection, ConnectionSpec}; use butane::migrations; -use butane::prelude::*; +use butane::prelude_async::*; use models::{Blog, Post}; /// Load a [Connection]. diff --git a/examples/newtype/src/lib.rs b/examples/newtype/src/lib.rs index 3bf17bd4..52c37eae 100644 --- a/examples/newtype/src/lib.rs +++ b/examples/newtype/src/lib.rs @@ -7,7 +7,7 @@ pub mod models; use butane::db::{Connection, ConnectionSpec}; use butane::migrations; -use butane::prelude::*; +use butane::prelude_async::*; use models::{Blog, Post}; // todo this example doesn't necessarily need to be async @@ -19,7 +19,7 @@ pub async fn establish_connection() -> Connection { .await .unwrap(); let migrations = butane_migrations::get_migrations().unwrap(); - migrations::apply_unapplied_migrations_async(migrations, connection) + migrations::apply_unapplied_migrations_async(migrations, &mut connection) .await .unwrap(); connection From ceeed301d1818003bb4ca1331b2bf920af3d4a4c Mon Sep 17 00:00:00 2001 From: James Oakley Date: Tue, 23 Jul 2024 21:44:16 -0400 Subject: [PATCH 38/78] CLI sync --- butane_cli/src/main.rs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/butane_cli/src/main.rs b/butane_cli/src/main.rs index 992fa0ef..c723fbdc 100644 --- a/butane_cli/src/main.rs +++ b/butane_cli/src/main.rs @@ -162,9 +162,12 @@ async fn main() { }; match &cli.command { - Commands::Init(args) => { - handle_error(init(&base_dir, &args.backend, &args.connection, args.connect).await) - } + Commands::Init(args) => handle_error(init( + &base_dir, + &args.backend, + &args.connection, + args.connect, + )), Commands::Backend { subcommand } => match subcommand { BackendCommands::Add { name } => handle_error(add_backend(&base_dir, name)), BackendCommands::Remove { name } => handle_error(remove_backend(&base_dir, name)), @@ -173,16 +176,14 @@ async fn main() { Commands::MakeMigration { name } => handle_error(make_migration(&base_dir, Some(name))), Commands::DescribeMigration { name } => handle_error(describe_migration(&base_dir, name)), Commands::Regenerate => handle_error(regenerate_migrations(&base_dir)), - Commands::DetachMigration => handle_error(detach_latest_migration(&base_dir).await), - Commands::Migrate { name } => handle_error(migrate(&base_dir, name.to_owned()).await), - Commands::Rollback { name } => handle_error(rollback(&base_dir, name.to_owned()).await), + Commands::DetachMigration => handle_error(detach_latest_migration(&base_dir)), + Commands::Migrate { name } => handle_error(migrate(&base_dir, name.to_owned())), + Commands::Rollback { name } => handle_error(rollback(&base_dir, name.to_owned())), Commands::Embed => handle_error(embed(&base_dir)), - Commands::List => handle_error(list_migrations(&base_dir).await), - Commands::Collapse { name } => { - handle_error(collapse_migrations(&base_dir, Some(name)).await) - } + Commands::List => handle_error(list_migrations(&base_dir)), + Commands::Collapse { name } => handle_error(collapse_migrations(&base_dir, Some(name))), Commands::Clear { subcommand } => match subcommand { - ClearCommands::Data => handle_error(clear_data(&base_dir).await), + ClearCommands::Data => handle_error(clear_data(&base_dir)), }, Commands::Delete { subcommand } => match subcommand { DeleteCommands::Table { name } => handle_error(delete_table(&base_dir, name)), From b1c45165992fd868558ea486bd2fd6fdeaeb1e77 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Wed, 24 Jul 2024 08:25:10 -0400 Subject: [PATCH 39/78] Single Backend trait --- butane/src/lib.rs | 2 +- butane/tests/custom_enum_derived.rs | 2 +- butane/tests/migration-tests.rs | 22 +++--- butane/tests/nullable.rs | 2 +- butane_cli/src/lib.rs | 19 ++--- butane_core/src/db/adapter.rs | 50 ++++--------- butane_core/src/db/dummy.rs | 22 ++---- butane_core/src/db/mod.rs | 86 ++++++---------------- butane_core/src/db/pg.rs | 8 +- butane_core/src/db/sqlite.rs | 10 ++- butane_core/src/db/sync_adapter.rs | 17 +++-- butane_core/src/lib.rs | 1 - butane_core/src/many.rs | 12 +-- butane_core/src/migrations/mod.rs | 3 +- butane_core/tests/connection.rs | 6 +- butane_test_helper/src/lib.rs | 17 +++-- examples/getting_started/tests/rollback.rs | 2 +- examples/getting_started_async/Cargo.toml | 14 +++- 18 files changed, 121 insertions(+), 174 deletions(-) diff --git a/butane/src/lib.rs b/butane/src/lib.rs index e6bdb7d4..83de2e21 100644 --- a/butane/src/lib.rs +++ b/butane/src/lib.rs @@ -181,7 +181,7 @@ mod prelude_common { pub mod prelude { //! Prelude module to improve ergonomics. Brings certain traits into scope. - //! This module is for sync operation. For asynchronous, see [`prelude_async`]. + //! This module is for sync operation. For asynchronous, see [`super::prelude_async`]. //! //! Its use is recommended, but not required. diff --git a/butane/tests/custom_enum_derived.rs b/butane/tests/custom_enum_derived.rs index 82096eea..7a0db723 100644 --- a/butane/tests/custom_enum_derived.rs +++ b/butane/tests/custom_enum_derived.rs @@ -1,6 +1,6 @@ // Tests deriving FieldType for an enum use butane::db::Connection; -use butane::prelude::*; +use butane::prelude_async::*; use butane::{model, query}; use butane::{FieldType, FromSql, SqlVal, ToSql}; use butane_test_helper::*; diff --git a/butane/tests/migration-tests.rs b/butane/tests/migration-tests.rs index a66372c3..c5ac7f56 100644 --- a/butane/tests/migration-tests.rs +++ b/butane/tests/migration-tests.rs @@ -2,7 +2,7 @@ use butane::migrations::{ adb::DeferredSqlType, adb::TypeIdentifier, adb::TypeKey, MemMigrations, Migration, MigrationMut, Migrations, MigrationsMut, }; -use butane::{db::Connection, prelude::*, SqlType, SqlVal}; +use butane::{db::Connection, prelude_async::*, SqlType, SqlVal}; use butane_core::codegen::{butane_type_with_migrations, model_with_migrations}; #[cfg(feature = "pg")] use butane_test_helper::pg_connection; @@ -175,10 +175,10 @@ fn current_migration_custom_type() { } #[cfg(feature = "sqlite")] -#[tokio::test] -async fn migration_add_field_sqlite() { +#[test] +fn migration_add_field_sqlite() { migration_add_field( - &mut butane_test_helper::sqlite_connection().await, + &mut butane_test_helper::sqlite_connection(), "ALTER TABLE Foo ADD COLUMN baz INTEGER NOT NULL DEFAULT 0;", // The exact details of futzing a DROP COLUMN in sqlite aren't // important (e.g. the temp table naming is certainly not part @@ -201,8 +201,7 @@ async fn migration_add_field_pg() { &mut conn, "ALTER TABLE Foo ADD COLUMN baz BIGINT NOT NULL DEFAULT 0;", "ALTER TABLE Foo DROP COLUMN baz;", - ) - .await; + ); } #[cfg(feature = "sqlite")] @@ -267,10 +266,9 @@ async fn migration_add_and_remove_field_pg() { } #[cfg(feature = "sqlite")] -#[tokio::test] async fn migration_delete_table_sqlite() { migration_delete_table( - &mut sqlite_connection().await, + &mut sqlite_connection(), "DROP TABLE Foo;", "CREATE TABLE Foo (id INTEGER NOT NULL PRIMARY KEY,bar TEXT NOT NULL);", ) @@ -280,7 +278,7 @@ async fn migration_delete_table_sqlite() { #[cfg(feature = "pg")] #[tokio::test] async fn migration_delete_table_pg() { - let (mut conn, _data) = pg_connection().await; + let (mut conn, _data) = pg_connection(); migration_delete_table( &mut conn, "DROP TABLE Foo;", @@ -289,7 +287,7 @@ async fn migration_delete_table_pg() { .await; } -async fn test_migrate( +fn test_migrate( conn: &mut Connection, init_tokens: TokenStream, v2_tokens: TokenStream, @@ -307,7 +305,7 @@ async fn test_migrate( .create_migration(&backends, "v2", ms.latest().as_ref()) .unwrap()); - let mut to_apply = ms.unapplied_migrations(conn).await.unwrap(); + let mut to_apply = ms.unapplied_migrations(conn).unwrap(); assert_eq!(to_apply.len(), 2); for m in &to_apply { m.apply(conn).await.unwrap(); @@ -342,7 +340,7 @@ fn verify_sql( assert_eq!(actual_down_ast, expected_down_ast); } -async fn migration_add_field(conn: &mut Connection, up_sql: &str, down_sql: &str) { +fn migration_add_field(conn: &mut Connection, up_sql: &str, down_sql: &str) { let init = quote! { struct Foo { id: i64, diff --git a/butane/tests/nullable.rs b/butane/tests/nullable.rs index 614888c5..137263f0 100644 --- a/butane/tests/nullable.rs +++ b/butane/tests/nullable.rs @@ -1,5 +1,5 @@ use butane::db::Connection; -use butane::prelude::*; +use butane::prelude_async::*; use butane::{model, query}; use butane_test_helper::*; diff --git a/butane_cli/src/lib.rs b/butane_cli/src/lib.rs index 2d717dc3..81e2baef 100644 --- a/butane_cli/src/lib.rs +++ b/butane_cli/src/lib.rs @@ -13,7 +13,8 @@ use std::{ path::{Path, PathBuf}, }; -use butane::db::sync::{Backend, Connection, ConnectionMethods}; +use butane::db::sync::{Connection, ConnectionMethods}; +use butane::db::Backend; use butane::migrations::adb; use butane::migrations::adb::{diff, AColumn, ARef, Operation, ADB}; use butane::migrations::{ @@ -57,7 +58,7 @@ pub fn default_name() -> String { } pub fn init(base_dir: &PathBuf, name: &str, connstr: &str, connect: bool) -> Result<()> { - if db::get_async_backend(name).is_none() { + if db::get_backend(name).is_none() { eprintln!("Unknown backend {name}"); std::process::exit(1); }; @@ -434,8 +435,8 @@ pub fn add_backend(base_dir: &Path, backend_name: &str) -> Result<()> { } } - let backend = db::get_async_backend(backend_name) - .ok_or(anyhow::anyhow!("Backend {backend_name} not found"))?; + let backend = + db::get_backend(backend_name).ok_or(anyhow::anyhow!("Backend {backend_name} not found"))?; let migrations = get_migrations(base_dir)?; let migration_list = migrations.all_migrations()?; @@ -473,8 +474,8 @@ pub fn remove_backend(base_dir: &Path, backend_name: &str) -> Result<()> { return Err(anyhow::anyhow!("Can not remove the last backend.")); } - let backend = db::get_async_backend(backend_name) - .ok_or(anyhow::anyhow!("Backend {backend_name} not found"))?; + let backend = + db::get_backend(backend_name).ok_or(anyhow::anyhow!("Backend {backend_name} not found"))?; let migrations = get_migrations(base_dir)?; let migration_list = migrations.all_migrations()?; @@ -538,7 +539,7 @@ pub fn load_latest_migration_backends(base_dir: &Path) -> Result Result Result>> { +pub fn load_backends(base_dir: &Path) -> Result>> { // Try to use the same backends as the latest migration. let backends = load_latest_migration_backends(base_dir); if backends.is_ok() { @@ -560,7 +561,7 @@ pub fn load_backends(base_dir: &Path) -> Result Box { - // no sync-to-async translation needed but we still have to - // dispatch to our worker thread because only that thread owns - // the BackendConnection object. // todo clean up unwrap - Box::new(BackendAdapter::new( - self.invoke_blocking(|conn| Ok(conn.backend())).unwrap(), - )) + self.invoke_blocking(|conn| Ok(conn.backend())).unwrap() } fn backend_name(&self) -> &'static str { // todo clean up unwrap @@ -405,36 +400,19 @@ where } } -#[derive(Clone)] -pub(super) struct BackendAdapter +/// Create an async connection using the synchronous `connect` method of `backend`. Use this when authoring +/// a backend which doesn't natively support async. +#[cfg(feature = "sqlite")] // todo expose this publicly for out-of-tree backends +pub async fn connect_async_via_sync(backend: &B, conn_str: &str) -> Result where - T: sync::Backend + Clone, + B: Backend + Clone + 'static, { - inner: T, -} -impl BackendAdapter { - pub(super) fn new(inner: T) -> Self { - BackendAdapter { inner } - } -} - -#[async_trait] -impl Backend for BackendAdapter { - fn name(&self) -> &'static str { - self.inner.name() - } - fn create_migration_sql(&self, current: &adb::ADB, ops: Vec) -> Result { - self.inner.create_migration_sql(current, ops) - } - async fn connect(&self, conn_str: &str) -> Result { - // create a copy of the backend that can be moved into the closure - let sync_backend: T = self.inner.clone(); - let conn_str2 = conn_str.to_string(); - tokio::task::spawn_blocking(move || { - let connmethods_async = - adapter::AsyncAdapter::new(|| sync_backend.connect(&conn_str2))?; - Ok(connmethods_async.into_connection()) - }) - .await? - } + // create a copy of the backend that can be moved into the closure + let backend2 = backend.clone(); + let conn_str2 = conn_str.to_string(); + tokio::task::spawn_blocking(move || { + let connmethods_async = adapter::AsyncAdapter::new(|| backend2.connect(&conn_str2))?; + Ok(connmethods_async.into_connection()) + }) + .await? } diff --git a/butane_core/src/db/dummy.rs b/butane_core/src/db/dummy.rs index 8c9eba72..569b10ae 100644 --- a/butane_core/src/db/dummy.rs +++ b/butane_core/src/db/dummy.rs @@ -4,7 +4,6 @@ use async_trait::async_trait; use super::connmethods::sync::ConnectionMethods as ConnectionMethodsSync; -use super::sync::Backend as BackendSync; use super::sync::BackendConnection as BackendConnectionSync; use super::sync::Connection as ConnectionSync; use super::sync::Transaction as TransactionSync; @@ -21,15 +20,6 @@ use BackendConnection as BackendConnectionAsync; #[derive(Clone, Debug)] struct DummyBackend {} -#[maybe_async_cfg::maybe( - idents( - Backend(sync = "BackendSync", async), - Connection(sync = "ConnectionSync", async) - ), - keep_self, - sync(), - async() -)] #[async_trait] impl Backend for DummyBackend { fn name(&self) -> &'static str { @@ -38,7 +28,10 @@ impl Backend for DummyBackend { fn create_migration_sql(&self, current: &adb::ADB, ops: Vec) -> Result { Err(Error::PoisonedConnection) } - async fn connect(&self, conn_str: &str) -> Result { + fn connect(&self, conn_str: &str) -> Result { + Err(Error::PoisonedConnection) + } + async fn connect_async(&self, conn_str: &str) -> Result { Err(Error::PoisonedConnection) } } @@ -117,12 +110,7 @@ impl ConnectionMethods for DummyConnection { } } -#[maybe_async_cfg::maybe( - idents(Backend(sync = "BackendSync", async), BackendConnection, Transaction), - keep_self, - sync(), - async() -)] +#[maybe_async_cfg::maybe(idents(BackendConnection, Transaction), keep_self, sync(), async())] #[async_trait(?Send)] impl BackendConnection for DummyConnection { async fn transaction(&mut self) -> Result> { diff --git a/butane_core/src/db/mod.rs b/butane_core/src/db/mod.rs index f142092a..282d3cde 100644 --- a/butane_core/src/db/mod.rs +++ b/butane_core/src/db/mod.rs @@ -75,7 +75,6 @@ mod internal { #[maybe_async_cfg::maybe( idents( AsyncRequiresSend, - Backend, ConnectionMethods(sync = "ConnectionMethodsSync", async), Transaction(sync = "TransactionSync", async), ), @@ -96,7 +95,7 @@ mod internal { } #[maybe_async_cfg::maybe( - idents(Backend, BackendConnection, Connection, Transaction), + idents(BackendConnection, Connection, Transaction), keep_self, sync(), async() @@ -256,11 +255,7 @@ mod internal { } } - #[maybe_async_cfg::maybe( - idents(Backend, BackendConnection, Connection, Transaction), - sync(), - async() - )] + #[maybe_async_cfg::maybe(idents(BackendConnection, Connection, Transaction), sync(), async())] #[async_trait(?Send)] impl BackendConnection for Connection { async fn transaction(&mut self) -> Result { @@ -466,26 +461,8 @@ mod internal { self.deref().has_table(table).await } } - - #[maybe_async_cfg::maybe(idents(Connection(sync = "ConnectionSync", async)), sync(), async())] - /// Database backend. A boxed implementation can be returned by name via [get_backend][crate::db::get_backend]. - // todo do we really need two versions of this? Can we give it two connect methods instead? - #[async_trait] - pub trait Backend: Send + Sync + DynClone { - fn name(&self) -> &'static str; - fn create_migration_sql( - &self, - current: &adb::ADB, - ops: Vec, - ) -> Result; - async fn connect(&self, conn_str: &str) -> Result; - } - - dyn_clone::clone_trait_object!(BackendAsync); - dyn_clone::clone_trait_object!(BackendSync); } -pub use internal::BackendAsync as Backend; pub use internal::BackendConnectionAsync as BackendConnection; pub use internal::ConnectionAsync as Connection; pub use internal::TransactionAsync as Transaction; @@ -500,7 +477,6 @@ pub mod sync { pub use super::connmethods::sync::ConnectionMethods; pub use super::internal::BackendConnectionSync as BackendConnection; - pub use super::internal::BackendSync as Backend; pub use super::internal::ConnectionSync as Connection; pub use super::internal::TransactionSync as Transaction; @@ -509,6 +485,17 @@ pub mod sync { pub(crate) use super::internal::BackendTransactionSync as BackendTransaction; } +/// Database backend. A boxed implementation can be returned by name via [get_backend][crate::db::get_backend]. +#[async_trait] +pub trait Backend: Send + Sync + DynClone { + fn name(&self) -> &'static str; + fn create_migration_sql(&self, current: &adb::ADB, ops: Vec) -> Result; + fn connect(&self, conn_str: &str) -> Result; + async fn connect_async(&self, conn_str: &str) -> Result; +} + +dyn_clone::clone_trait_object!(Backend); + /// Connection specification. Contains the name of a database backend /// and the backend-specific connection string. See [`connect`] /// to make a [`Connection`] from a `ConnectionSpec`. @@ -537,14 +524,8 @@ impl ConnectionSpec { let path = conn_complete_if_dir(path.as_ref()); serde_json::from_reader(fs::File::open(path)?).map_err(|e| e.into()) } - pub fn get_sync_backend(&self) -> Result> { - match get_sync_backend(&self.backend_name) { - Some(backend) => Ok(backend), - None => Err(crate::Error::UnknownBackend(self.backend_name.clone())), - } - } - pub fn get_async_backend(&self) -> Result> { - match get_async_backend(&self.backend_name) { + pub fn get_backend(&self) -> Result> { + match get_backend(&self.backend_name) { Some(backend) => Ok(backend), None => Err(crate::Error::UnknownBackend(self.backend_name.clone())), } @@ -562,18 +543,6 @@ fn conn_complete_if_dir(path: &Path) -> Cow { /// Database backend. A boxed implementation can be returned by name via [`get_backend`]. #[async_trait] impl Backend for Box { - fn name(&self) -> &'static str { - self.deref().name() - } - fn create_migration_sql(&self, current: &adb::ADB, ops: Vec) -> Result { - self.deref().create_migration_sql(current, ops) - } - async fn connect(&self, conn_str: &str) -> Result { - self.deref().connect(conn_str).await - } -} - -impl sync::Backend for Box { fn name(&self) -> &'static str { self.deref().name() } @@ -583,27 +552,18 @@ impl sync::Backend for Box { fn connect(&self, conn_str: &str) -> Result { self.deref().connect(conn_str) } -} - -/// Find a backend by name. -pub fn get_async_backend(name: &str) -> Option> { - match name { - #[cfg(feature = "sqlite")] - sqlite::BACKEND_NAME => Some(Box::new(adapter::BackendAdapter::new( - sqlite::SQLiteBackend::new(), - ))), - #[cfg(feature = "pg")] - pg::BACKEND_NAME => Some(Box::new(pg::PgBackend::new())), - _ => None, + async fn connect_async(&self, conn_str: &str) -> Result { + self.deref().connect_async(conn_str).await } } /// Find a backend by name. -pub fn get_sync_backend(name: &str) -> Option> { +pub fn get_backend(name: &str) -> Option> { match name { #[cfg(feature = "sqlite")] sqlite::BACKEND_NAME => Some(Box::new(sqlite::SQLiteBackend::new())), - // todo wrap PG + #[cfg(feature = "pg")] + pg::BACKEND_NAME => Some(Box::new(pg::PgBackend::new())), _ => None, } } @@ -611,7 +571,7 @@ pub fn get_sync_backend(name: &str) -> Option> { /// Connect to a database. For non-boxed connections, see individual /// [`Backend`] implementations. pub fn connect(spec: &ConnectionSpec) -> Result { - get_sync_backend(&spec.backend_name) + get_backend(&spec.backend_name) .ok_or_else(|| Error::UnknownBackend(spec.backend_name.clone()))? .connect(&spec.conn_str) } @@ -619,8 +579,8 @@ pub fn connect(spec: &ConnectionSpec) -> Result { /// Connect to a database. For non-boxed connections, see individual /// [`Backend`] implementations. pub async fn connect_async(spec: &ConnectionSpec) -> Result { - get_async_backend(&spec.backend_name) + get_backend(&spec.backend_name) .ok_or_else(|| Error::UnknownBackend(spec.backend_name.clone()))? - .connect(&spec.conn_str) + .connect_async(&spec.conn_str) .await } diff --git a/butane_core/src/db/pg.rs b/butane_core/src/db/pg.rs index 259fb261..c952fbcb 100644 --- a/butane_core/src/db/pg.rs +++ b/butane_core/src/db/pg.rs @@ -12,7 +12,7 @@ use super::helper; use crate::custom::{SqlTypeCustom, SqlValRefCustom}; use crate::db::{ Backend, BackendConnection, BackendRow, BackendTransaction, Column, Connection, - ConnectionMethods, RawQueryResult, Transaction, + ConnectionMethods, RawQueryResult, SyncAdapter, Transaction, }; use crate::migrations::adb::{AColumn, ARef, ATable, Operation, TypeIdentifier, ADB}; use crate::query::{BoolExpr, Expr}; @@ -53,7 +53,11 @@ impl Backend for PgBackend { .join("\n")) } - async fn connect(&self, path: &str) -> Result { + fn connect(&self, path: &str) -> Result { + SyncAdapter::new(self.clone())?.connect(path) + } + + async fn connect_async(&self, path: &str) -> Result { Ok(Connection { conn: Box::new(self.connect(path).await?), }) diff --git a/butane_core/src/db/sqlite.rs b/butane_core/src/db/sqlite.rs index b7c21881..c2b2badb 100644 --- a/butane_core/src/db/sqlite.rs +++ b/butane_core/src/db/sqlite.rs @@ -1,4 +1,5 @@ //! SQLite database backend +use async_trait::async_trait; use std::borrow::Cow; use std::fmt::{Debug, Write}; use std::ops::Deref; @@ -8,9 +9,9 @@ use std::pin::Pin; use std::sync::Once; use super::sync::{ - Backend, BackendConnection, BackendTransaction, Connection, ConnectionMethods, Transaction, + BackendConnection, BackendTransaction, Connection, ConnectionMethods, Transaction, }; -use super::{helper, BackendRow, Column, RawQueryResult}; +use super::{helper, Backend, BackendRow, Column, RawQueryResult}; use crate::db::connmethods::BackendRows; use crate::migrations::adb::ARef; use crate::migrations::adb::{AColumn, ATable, Operation, TypeIdentifier, ADB}; @@ -60,6 +61,8 @@ impl SQLiteBackend { Ok(connection) } } + +#[async_trait] impl Backend for SQLiteBackend { fn name(&self) -> &'static str { BACKEND_NAME @@ -83,6 +86,9 @@ impl Backend for SQLiteBackend { conn: Box::new(self.connect(path)?), }) } + async fn connect_async(&self, path: &str) -> Result { + super::adapter::connect_async_via_sync(self, path).await + } } /// SQLite database connection. diff --git a/butane_core/src/db/sync_adapter.rs b/butane_core/src/db/sync_adapter.rs index d49a964b..9ac4587f 100644 --- a/butane_core/src/db/sync_adapter.rs +++ b/butane_core/src/db/sync_adapter.rs @@ -1,7 +1,8 @@ -use crate::db::RawQueryResult; +use crate::db::{Backend, RawQueryResult}; use crate::migrations::adb; use crate::query::{BoolExpr, Order}; use crate::{Column, Result, SqlVal, SqlValRef}; +use async_trait::async_trait; use std::future::Future; @@ -132,8 +133,8 @@ where transaction_adapter, ))) } - fn backend(&self) -> Box { - Box::new(SyncAdapter::new(self.inner.backend()).unwrap()) + fn backend(&self) -> Box { + self.inner.backend() } fn backend_name(&self) -> &'static str { self.inner.backend_name() @@ -167,9 +168,10 @@ where } } -impl crate::db::sync::Backend for SyncAdapter +#[async_trait] +impl Backend for SyncAdapter where - T: crate::db::Backend + Clone, + T: Backend + Clone, { fn name(&self) -> &'static str { self.inner.name() @@ -178,9 +180,12 @@ where self.inner.create_migration_sql(current, ops) } fn connect(&self, conn_str: &str) -> Result { - let conn_async = self.block_on(self.inner.connect(conn_str))?; + let conn_async = self.block_on(self.inner.connect_async(conn_str))?; Ok(crate::db::sync::Connection { conn: Box::new(SyncAdapter::new(conn_async.conn)?), }) } + async fn connect_async(&self, conn_str: &str) -> Result { + self.inner.connect_async(conn_str).await + } } diff --git a/butane_core/src/lib.rs b/butane_core/src/lib.rs index f180a8e7..23a5ad28 100644 --- a/butane_core/src/lib.rs +++ b/butane_core/src/lib.rs @@ -2,7 +2,6 @@ #![deny(missing_docs)] #![allow(clippy::iter_nth_zero)] #![allow(clippy::upper_case_acronyms)] //grandfathered, not going to break API to rename -#![deny(missing_docs)] use std::borrow::Borrow; use std::cmp::{Eq, PartialEq}; diff --git a/butane_core/src/many.rs b/butane_core/src/many.rs index 93619a1f..a9be3388 100644 --- a/butane_core/src/many.rs +++ b/butane_core/src/many.rs @@ -134,13 +134,13 @@ where )] /// Loads the values referred to by this many relationship from a /// database query if necessary and returns a reference to them. -async fn load_query_uncached<'a, T: DataObject>( +async fn load_query_uncached<'a, T>( many: &'a Many, conn: &impl ConnectionMethods, query: Query, ) -> Result> where - T: 'a, + T: DataObject + 'a, { use crate::query::QueryOp; let mut vals: Vec = query.load(conn).await?; @@ -158,13 +158,13 @@ where /// Loads the values referred to by this many relationship from a /// database query if necessary and returns a reference to them. -async fn load_query_async<'a, T: DataObject>( +async fn load_query_async<'a, T>( many: &'a Many, conn: &impl ConnectionMethods, query: Query, ) -> Result> where - T: 'a, + T: DataObject + 'a, { many.all_values .get_or_try_init(|| load_query_uncached_async(many, conn, query)) @@ -174,13 +174,13 @@ where /// Loads the values referred to by this many relationship from a /// database query if necessary and returns a reference to them. -fn load_query_sync<'a, T: DataObject>( +fn load_query_sync<'a, T>( many: &'a Many, conn: &impl ConnectionMethodsSync, query: Query, ) -> Result> where - T: 'a, + T: DataObject + 'a, { let rt = tokio::runtime::Builder::new_current_thread() .enable_all() diff --git a/butane_core/src/migrations/mod.rs b/butane_core/src/migrations/mod.rs index 988dfdfb..570a4a9f 100644 --- a/butane_core/src/migrations/mod.rs +++ b/butane_core/src/migrations/mod.rs @@ -8,8 +8,7 @@ use std::path::Path; use fallible_iterator::FallibleIterator; use nonempty::NonEmpty; -use crate::db::sync::Backend; -use crate::db::{BackendRows, Column}; +use crate::db::{Backend, BackendRows, Column}; use crate::sqlval::{FromSql, SqlValRef, ToSql}; use crate::{db, query, DataObject, DataResult, Error, PrimaryKeyType, Result, SqlType}; diff --git a/butane_core/tests/connection.rs b/butane_core/tests/connection.rs index 13ac5dc3..3db7e18f 100644 --- a/butane_core/tests/connection.rs +++ b/butane_core/tests/connection.rs @@ -1,4 +1,4 @@ -use butane_core::db::{connect, BackendConnection, Connection, ConnectionSpec}; +use butane_core::db::{connect_async, BackendConnection, Connection, ConnectionSpec}; use butane_test_helper::*; async fn connection_not_closed(conn: Connection) { @@ -27,7 +27,7 @@ async fn invalid_pg_connection() { assert_eq!(spec.backend_name, "pg".to_string()); assert_eq!(spec.conn_str, "does_not_parse".to_string()); - let result = connect(&spec).await; + let result = connect_async(&spec).await; assert!(matches!(result, Err(butane_core::Error::Postgres(_)))); match result { Err(butane_core::Error::Postgres(e)) => { @@ -47,7 +47,7 @@ async fn unreachable_pg_connection() { "host=does_not_exist user=does_not_exist".to_string() ); - let result = connect(&spec).await; + let result = connect_async(&spec).await; assert!(matches!(result, Err(butane_core::Error::Postgres(_)))); match result { Err(butane_core::Error::Postgres(e)) => { diff --git a/butane_test_helper/src/lib.rs b/butane_test_helper/src/lib.rs index 5a3d25f1..69a559a5 100644 --- a/butane_test_helper/src/lib.rs +++ b/butane_test_helper/src/lib.rs @@ -2,8 +2,9 @@ //! Macros depend on [`butane_core`], `env_logger` and [`log`]. #![deny(missing_docs)] +use butane_core::db::sync::Connection as ConnectionSync; use butane_core::db::{ - connect_async, get_async_backend, pg, sqlite, Backend, Connection, ConnectionSpec, + connect_async, get_backend, pg, sqlite, Backend, Connection, ConnectionSpec, }; use butane_core::migrations::{self, MemMigrations, Migration, MigrationsMut}; use once_cell::sync::Lazy; @@ -18,10 +19,10 @@ use block_id::{Alphabet, BlockId}; use uuid::Uuid; /// Create a postgres [`Connection`]. -pub async fn pg_connection() -> (Connection, PgSetupData) { - let backend = get_async_backend(pg::BACKEND_NAME).unwrap(); +pub async fn pg_connection() -> (ConnectionSync, PgSetupData) { + let backend = get_backend(pg::BACKEND_NAME).unwrap(); let data = pg_setup().await; - (backend.connect(&pg_connstr(&data)).await.unwrap(), data) + (backend.connect(&pg_connstr(&data)).unwrap(), data) } /// Create a postgres [`ConnectionSpec`]. @@ -221,9 +222,9 @@ pub async fn setup_db(backend: Box, conn: &mut Connection, migrate: } /// Create a sqlite [`Connection`]. -pub async fn sqlite_connection() -> Connection { - let backend = get_async_backend(sqlite::BACKEND_NAME).unwrap(); - backend.connect(":memory:").await.unwrap() +pub fn sqlite_connection() -> ConnectionSync { + let backend = get_backend(sqlite::BACKEND_NAME).unwrap(); + backend.connect(":memory:").unwrap() } /// Create a sqlite [`ConnectionSpec`]. @@ -247,7 +248,7 @@ macro_rules! maketest { let backend = butane_core::db::get_backend(&stringify!($backend)).expect("Could not find backend"); let $dataname = butane_test_helper::[<$backend _setup>]().await; log::info!("connecting to {}..", &$connstr); - let mut conn = backend.connect(&$connstr).await.expect("Could not connect backend"); + let mut conn = backend.connect_async(&$connstr).await.expect("Could not connect backend"); butane_test_helper::setup_db(backend, &mut conn, $migrate).await; log::info!("running test on {}..", &$connstr); $fname(conn).await; diff --git a/examples/getting_started/tests/rollback.rs b/examples/getting_started/tests/rollback.rs index 2da3fd85..a18ba658 100644 --- a/examples/getting_started/tests/rollback.rs +++ b/examples/getting_started/tests/rollback.rs @@ -38,7 +38,7 @@ async fn migrate_and_rollback(mut connection: Connection) { // Migrate forward. let base_dir = std::path::PathBuf::from(".butane"); let migrations = butane_cli::get_migrations(&base_dir).unwrap(); - let to_apply = migrations.unapplied_migrations(&connection).await.unwrap(); + let to_apply = migrations.unapplied_migrations(&connection).unwrap(); for migration in &to_apply { if connection.backend_name() == "pg" && migration.name() == "20240115_023841384_dbconstraints" diff --git a/examples/getting_started_async/Cargo.toml b/examples/getting_started_async/Cargo.toml index 2179cdfc..8fe7e801 100644 --- a/examples/getting_started_async/Cargo.toml +++ b/examples/getting_started_async/Cargo.toml @@ -7,15 +7,23 @@ edition.workspace = true publish = false [[bin]] -name = "show_posts" +name = "show_posts_async" +path = "src/bin/show_posts.rs" doc = false [[bin]] -name = "write_post" +name = "write_post_async" +path = "src/bin/write_post.rs" doc = false [[bin]] -name = "delete_post" +name = "delete_post_async" +path = "src/bin/delete_post.rs" +doc = false + +[[bin]] +name = "publish_post_async" +path = "src/bin/publish_post.rs" doc = false [lib] From 9fa72df9b380b83ab6362c480cb7a0680522c58a Mon Sep 17 00:00:00 2001 From: James Oakley Date: Thu, 25 Jul 2024 22:16:18 -0400 Subject: [PATCH 40/78] Tests almost all passing --- Cargo.lock | 1 + async_checklist.md | 5 +- butane/tests/common/blog.rs | 1 + butane/tests/custom_pg.rs | 2 +- butane/tests/json.rs | 2 +- butane/tests/migration-tests.rs | 150 +++++++----------- butane/tests/query.rs | 2 +- butane/tests/unmigrate.rs | 9 +- butane/tests/uuid.rs | 2 +- butane_core/src/db/mod.rs | 5 +- butane_core/src/db/pg.rs | 11 +- butane_core/src/db/sync_adapter.rs | 48 ++++-- butane_core/src/migrations/mod.rs | 40 ++--- butane_test_helper/Cargo.toml | 2 + butane_test_helper/src/lib.rs | 36 ++++- examples/getting_started/tests/unmigrate.rs | 6 +- .../getting_started_async/tests/rollback.rs | 36 +---- examples/newtype/tests/unmigrate.rs | 4 +- 18 files changed, 182 insertions(+), 180 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index de128c18..f73275d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -372,6 +372,7 @@ dependencies = [ "butane_core", "libc", "log", + "maybe-async-cfg", "nonempty", "once_cell", "rand", diff --git a/async_checklist.md b/async_checklist.md index 6499aacd..23789992 100644 --- a/async_checklist.md +++ b/async_checklist.md @@ -1,12 +1,11 @@ -* [ ] Fully support sync too. Using async should not be required * [ ] Clean up pattern for sync/async variants. Inconsistent between suffix and module * [ ] Tests should run against sync and async * [ ] Establish soundness for unsafe sections of AsyncAdapter * [ ] Consider publishing `AsyncAdapter` into its own crate * [ ] Ensure Postgres works in sync -* [ ] Ensure sqlite works in async (might already be done) * [ ] Re-enable R2D2 for sync and find async alternative (deadpool) * [ ] Fix `#[async_trait(?Send)]` to set up Send bound again as it's required for e.g. `tokio::spawn` * [ ] Separate sync and async examples * [ ] Should async_adapter be under a separate feature? Do we need it for migrations? - +* [x] Ensure sqlite works in async +* [x] Fully support sync too. Using async should not be required diff --git a/butane/tests/common/blog.rs b/butane/tests/common/blog.rs index 97f3bfc5..75eeb3d1 100644 --- a/butane/tests/common/blog.rs +++ b/butane/tests/common/blog.rs @@ -69,6 +69,7 @@ impl Post { #[cfg(feature = "datetime")] #[dataresult(Post)] +#[allow(unused)] pub struct PostMetadata { pub id: i64, pub title: String, diff --git a/butane/tests/custom_pg.rs b/butane/tests/custom_pg.rs index 2a16a214..05d3b95f 100644 --- a/butane/tests/custom_pg.rs +++ b/butane/tests/custom_pg.rs @@ -2,7 +2,7 @@ #[cfg(feature = "pg")] mod custom_pg { use butane::custom::{SqlTypeCustom, SqlValRefCustom}; - use butane::prelude::*; + use butane::prelude_async::*; use butane::{butane_type, db::Connection, model}; use butane::{AutoPk, FieldType, FromSql, SqlType, SqlVal, SqlValRef, ToSql}; use butane_test_helper::{maketest, maketest_pg}; diff --git a/butane/tests/json.rs b/butane/tests/json.rs index d468ba9a..b820bc67 100644 --- a/butane/tests/json.rs +++ b/butane/tests/json.rs @@ -3,7 +3,7 @@ use std::collections::{BTreeMap, HashMap}; use butane::model; -use butane::prelude::*; +use butane::prelude_async::*; use butane::{db::Connection, FieldType}; use butane_test_helper::*; use serde::{Deserialize, Serialize}; diff --git a/butane/tests/migration-tests.rs b/butane/tests/migration-tests.rs index a44156ce..bca734ab 100644 --- a/butane/tests/migration-tests.rs +++ b/butane/tests/migration-tests.rs @@ -1,10 +1,8 @@ -use butane::db::{BackendConnection, Connection}; +use butane::db::sync::{BackendConnection, Connection}; use butane::migrations::{MemMigrations, Migration, MigrationMut, Migrations, MigrationsMut}; -use butane::{prelude_async::*, SqlType, SqlVal}; +use butane::{SqlType, SqlVal}; use butane_core::codegen::{butane_type_with_migrations, model_with_migrations}; use butane_core::migrations::adb::{DeferredSqlType, TypeIdentifier, TypeKey}; -use butane_core::migrations::{MemMigrations, Migration, MigrationMut, Migrations, MigrationsMut}; -use butane_core::{SqlType, SqlVal}; #[cfg(feature = "pg")] use butane_test_helper::pg_connection; #[cfg(feature = "sqlite")] @@ -191,13 +189,12 @@ fn migration_add_field_sqlite() { INSERT INTO Foo__butane_tmp SELECT id, bar FROM Foo;DROP TABLE Foo; ALTER TABLE Foo__butane_tmp RENAME TO Foo;", ) - .await; } #[cfg(feature = "pg")] -#[tokio::test] -async fn migration_add_field_pg() { - let (mut conn, _data) = pg_connection().await; +#[test] +fn migration_add_field_pg() { + let (mut conn, _data) = pg_connection(); migration_add_field( &mut conn, "ALTER TABLE Foo ADD COLUMN baz BIGINT NOT NULL DEFAULT 0;", @@ -206,35 +203,34 @@ async fn migration_add_field_pg() { } #[cfg(feature = "sqlite")] -#[tokio::test] -async fn migration_add_field_with_default_sqlite() { +#[test] +fn migration_add_field_with_default_sqlite() { migration_add_field_with_default( - &mut sqlite_connection().await, + &mut sqlite_connection(), "ALTER TABLE Foo ADD COLUMN baz INTEGER NOT NULL DEFAULT 42;", // See comments on migration_add_field_sqlite r#"CREATE TABLE Foo__butane_tmp (id INTEGER NOT NULL PRIMARY KEY,bar TEXT NOT NULL); INSERT INTO Foo__butane_tmp SELECT id, bar FROM Foo; DROP TABLE Foo;ALTER TABLE Foo__butane_tmp RENAME TO Foo;"#, ) - .await; } #[cfg(feature = "pg")] -#[tokio::test] -async fn migration_add_field_with_default_pg() { - let (mut conn, _data) = pg_connection().await; +#[test] +fn migration_add_field_with_default_pg() { + let (mut conn, _data) = pg_connection(); migration_add_field_with_default( &mut conn, "ALTER TABLE Foo ADD COLUMN baz BIGINT NOT NULL DEFAULT 42;", "ALTER TABLE Foo DROP COLUMN baz;", ) - .await; } #[cfg(feature = "pg")] -#[tokio::test] -async fn migration_modify_field_pg() { - let (mut conn, _data) = pg_connection().await; +#[test] +fn migration_modify_field_pg() { + env_logger::try_init().ok(); + let (mut conn, _data) = pg_connection(); // Not verifying rename right now because we don't detect it // https://github.com/Electron100/butane/issues/89 @@ -242,49 +238,44 @@ async fn migration_modify_field_pg() { &mut conn, "ALTER TABLE Foo ALTER COLUMN bar SET DATA TYPE BIGINT;", "ALTER TABLE Foo ALTER COLUMN bar SET DATA TYPE INTEGER;", - ) - .await; + ); migration_modify_field_nullability_change( &mut conn, "ALTER TABLE Foo ALTER COLUMN bar DROP NOT NULL;", "ALTER TABLE Foo ALTER COLUMN bar SET NOT NULL;", - ) - .await; + ); migration_modify_field_pkey_change( &mut conn, "ALTER TABLE Foo DROP CONSTRAINT IF EXISTS Foo_pkey;\nALTER TABLE Foo ADD PRIMARY KEY (baz);", "ALTER TABLE Foo DROP CONSTRAINT IF EXISTS Foo_pkey;\nALTER TABLE Foo ADD PRIMARY KEY (bar);", - ).await; + ); migration_modify_field_uniqueness_change( &mut conn, "ALTER TABLE Foo ADD UNIQUE (bar);", "ALTER TABLE Foo DROP CONSTRAINT Foo_bar_key;", - ) - .await; + ); migration_modify_field_default_added( &mut conn, "ALTER TABLE Foo ALTER COLUMN bar SET DEFAULT 42;", "ALTER TABLE Foo ALTER COLUMN bar DROP DEFAULT;", - ) - .await; + ); migration_modify_field_different_default( &mut conn, "ALTER TABLE Foo ALTER COLUMN bar SET DEFAULT 42;", "ALTER TABLE Foo ALTER COLUMN bar SET DEFAULT 41;", - ) - .await; + ); } #[cfg(feature = "sqlite")] -#[tokio::test] -async fn migration_add_and_remove_field_sqlite() { +#[test] +fn migration_add_and_remove_field_sqlite() { migration_add_and_remove_field( - &mut sqlite_connection().await, + &mut sqlite_connection(), // The exact details of futzing a DROP COLUMN in sqlite aren't // important (e.g. the temp table naming is certainly not part // of the API contract), but the goal here is to ensure we're @@ -300,41 +291,38 @@ async fn migration_add_and_remove_field_sqlite() { INSERT INTO Foo__butane_tmp SELECT id, bar FROM Foo;DROP TABLE Foo; ALTER TABLE Foo__butane_tmp RENAME TO Foo;"#, ) - .await; } #[cfg(feature = "pg")] -#[tokio::test] -async fn migration_add_and_remove_field_pg() { - let (mut conn, _data) = pg_connection().await; +#[test] +fn migration_add_and_remove_field_pg() { + let (mut conn, _data) = pg_connection(); migration_add_and_remove_field( &mut conn, "ALTER TABLE Foo ADD COLUMN baz BIGINT NOT NULL DEFAULT 0;ALTER TABLE Foo DROP COLUMN bar;", "ALTER TABLE Foo ADD COLUMN bar TEXT NOT NULL DEFAULT '';ALTER TABLE Foo DROP COLUMN baz;", - ) - .await; + ); } #[cfg(feature = "sqlite")] -async fn migration_delete_table_sqlite() { +#[test] +fn migration_delete_table_sqlite() { migration_delete_table( &mut sqlite_connection(), "DROP TABLE Foo;", "CREATE TABLE Foo (id INTEGER NOT NULL PRIMARY KEY,bar TEXT NOT NULL);", ) - .await; } #[cfg(feature = "pg")] -#[tokio::test] -async fn migration_delete_table_pg() { +#[test] +fn migration_delete_table_pg() { let (mut conn, _data) = pg_connection(); migration_delete_table( &mut conn, "DROP TABLE Foo;", "CREATE TABLE Foo (id BIGINT NOT NULL PRIMARY KEY,bar TEXT NOT NULL);", - ) - .await; + ); } fn test_migrate( @@ -355,20 +343,20 @@ fn test_migrate( .create_migration(&backends, "v2", ms.latest().as_ref()) .unwrap()); - let mut to_apply = ms.unapplied_migrations(conn).unwrap(); + let to_apply = ms.unapplied_migrations(conn).unwrap(); assert_eq!(to_apply.len(), 2); - ms.migrate(conn).await.unwrap(); + ms.migrate(conn).unwrap(); - let to_apply = ms.unapplied_migrations(conn).await.unwrap(); + let to_apply = ms.unapplied_migrations(conn).unwrap(); assert_eq!(to_apply.len(), 0); verify_sql(conn, &ms, expected_up_sql, expected_down_sql); // Now downgrade, just to make sure we can - ms.unmigrate(conn).await.unwrap(); + ms.unmigrate(conn).unwrap(); - let to_apply = ms.unapplied_migrations(conn).await.unwrap(); + let to_apply = ms.unapplied_migrations(conn).unwrap(); assert_eq!(to_apply.len(), 2); } @@ -408,10 +396,10 @@ fn migration_add_field(conn: &mut Connection, up_sql: &str, down_sql: &str) { baz: u32, } }; - test_migrate(conn, init, v2, up_sql, down_sql).await; + test_migrate(conn, init, v2, up_sql, down_sql); } -async fn migration_add_field_with_default(conn: &mut Connection, up_sql: &str, down_sql: &str) { +fn migration_add_field_with_default(conn: &mut Connection, up_sql: &str, down_sql: &str) { let init = quote! { struct Foo { id: i64, @@ -427,10 +415,10 @@ async fn migration_add_field_with_default(conn: &mut Connection, up_sql: &str, d baz: u32, } }; - test_migrate(conn, init, v2, up_sql, down_sql).await; + test_migrate(conn, init, v2, up_sql, down_sql); } -async fn migration_modify_field_type_change(conn: &mut Connection, up_sql: &str, down_sql: &str) { +fn migration_modify_field_type_change(conn: &mut Connection, up_sql: &str, down_sql: &str) { let init = quote! { struct Foo { id: i64, @@ -444,14 +432,10 @@ async fn migration_modify_field_type_change(conn: &mut Connection, up_sql: &str, bar: i64, } }; - test_migrate(conn, init, v2, up_sql, down_sql).await; + test_migrate(conn, init, v2, up_sql, down_sql); } -async fn migration_modify_field_nullability_change( - conn: &mut Connection, - up_sql: &str, - down_sql: &str, -) { +fn migration_modify_field_nullability_change(conn: &mut Connection, up_sql: &str, down_sql: &str) { let init = quote! { struct Foo { id: i64, @@ -465,14 +449,10 @@ async fn migration_modify_field_nullability_change( bar: Option, } }; - test_migrate(conn, init, v2, up_sql, down_sql).await; + test_migrate(conn, init, v2, up_sql, down_sql); } -async fn migration_modify_field_uniqueness_change( - conn: &mut Connection, - up_sql: &str, - down_sql: &str, -) { +fn migration_modify_field_uniqueness_change(conn: &mut Connection, up_sql: &str, down_sql: &str) { let init = quote! { struct Foo { id: i64, @@ -487,10 +467,10 @@ async fn migration_modify_field_uniqueness_change( bar: i32, } }; - test_migrate(conn, init, v2, up_sql, down_sql).await; + test_migrate(conn, init, v2, up_sql, down_sql); } -async fn migration_modify_field_pkey_change(conn: &mut Connection, up_sql: &str, down_sql: &str) { +fn migration_modify_field_pkey_change(conn: &mut Connection, up_sql: &str, down_sql: &str) { let init = quote! { struct Foo { #[pk] @@ -506,10 +486,10 @@ async fn migration_modify_field_pkey_change(conn: &mut Connection, up_sql: &str, baz: i32 } }; - test_migrate(conn, init, v2, up_sql, down_sql).await; + test_migrate(conn, init, v2, up_sql, down_sql); } -async fn migration_modify_field_default_added(conn: &mut Connection, up_sql: &str, down_sql: &str) { +fn migration_modify_field_default_added(conn: &mut Connection, up_sql: &str, down_sql: &str) { let init = quote! { struct Foo { id: i64, @@ -524,14 +504,10 @@ async fn migration_modify_field_default_added(conn: &mut Connection, up_sql: &st bar: String, } }; - test_migrate(conn, init, v2, up_sql, down_sql).await; + test_migrate(conn, init, v2, up_sql, down_sql); } -async fn migration_modify_field_different_default( - conn: &mut Connection, - up_sql: &str, - down_sql: &str, -) { +fn migration_modify_field_different_default(conn: &mut Connection, up_sql: &str, down_sql: &str) { let init = quote! { struct Foo { id: i64, @@ -547,10 +523,10 @@ async fn migration_modify_field_different_default( bar: String, } }; - test_migrate(conn, init, v2, up_sql, down_sql).await; + test_migrate(conn, init, v2, up_sql, down_sql); } -async fn migration_add_and_remove_field(conn: &mut Connection, up_sql: &str, down_sql: &str) { +fn migration_add_and_remove_field(conn: &mut Connection, up_sql: &str, down_sql: &str) { let init = quote! { struct Foo { id: i64, @@ -564,14 +540,10 @@ async fn migration_add_and_remove_field(conn: &mut Connection, up_sql: &str, dow baz: u32, } }; - test_migrate(conn, init, v2, up_sql, down_sql).await; + test_migrate(conn, init, v2, up_sql, down_sql); } -async fn migration_delete_table( - conn: &mut Connection, - expected_up_sql: &str, - expected_down_sql: &str, -) { +fn migration_delete_table(conn: &mut Connection, expected_up_sql: &str, expected_down_sql: &str) { let init_tokens = quote! { struct Foo { id: i64, @@ -590,19 +562,19 @@ async fn migration_delete_table( .create_migration(&backends, "v2", ms.latest().as_ref()) .unwrap()); - let to_apply = ms.unapplied_migrations(conn).await.unwrap(); + let to_apply = ms.unapplied_migrations(conn).unwrap(); assert_eq!(to_apply.len(), 2); - ms.migrate(conn).await.unwrap(); + ms.migrate(conn).unwrap(); - let to_apply = ms.unapplied_migrations(conn).await.unwrap(); + let to_apply = ms.unapplied_migrations(conn).unwrap(); assert_eq!(to_apply.len(), 0); verify_sql(conn, &ms, expected_up_sql, expected_down_sql); // Now downgrade, just to make sure we can - ms.unmigrate(conn).await.unwrap(); + ms.unmigrate(conn).unwrap(); - let to_apply = ms.unapplied_migrations(conn).await.unwrap(); + let to_apply = ms.unapplied_migrations(conn).unwrap(); assert_eq!(to_apply.len(), 2); } diff --git a/butane/tests/query.rs b/butane/tests/query.rs index 9f2f1841..ade9c2d6 100644 --- a/butane/tests/query.rs +++ b/butane/tests/query.rs @@ -1,5 +1,5 @@ use butane::db::Connection; -use butane::prelude::*; +use butane::prelude_async::*; use butane::query::BoolExpr; use butane::{colname, filter, find, query, Many}; use butane_test_helper::*; diff --git a/butane/tests/unmigrate.rs b/butane/tests/unmigrate.rs index a655311b..2f2cfa0e 100644 --- a/butane/tests/unmigrate.rs +++ b/butane/tests/unmigrate.rs @@ -9,15 +9,16 @@ async fn unmigrate(mut connection: Connection) { let mem_migrations = create_current_migrations(&connection); connection - .with_sync(|conn| { - let migrations = mem_migrations.unapplied_migrations(&connection).unwrap(); + .with_sync(move |conn| { + let migrations = mem_migrations.unapplied_migrations(conn).unwrap(); assert_eq!(migrations.len(), 0); let migration = mem_migrations.latest().unwrap(); - migration.downgrade(&mut connection).unwrap(); + migration.downgrade(conn).unwrap(); - let migrations = mem_migrations.unapplied_migrations(&connection).unwrap(); + let migrations = mem_migrations.unapplied_migrations(conn).unwrap(); assert_eq!(migrations.len(), 1); + Ok(()) }) .await .unwrap(); diff --git a/butane/tests/uuid.rs b/butane/tests/uuid.rs index a4121308..6214fe76 100644 --- a/butane/tests/uuid.rs +++ b/butane/tests/uuid.rs @@ -1,6 +1,6 @@ use butane::db::Connection; use butane::model; -use butane::prelude::*; +use butane::prelude_async::*; use butane_test_helper::*; use uuid_for_test::Uuid; diff --git a/butane_core/src/db/mod.rs b/butane_core/src/db/mod.rs index 453f9bca..bcc9b429 100644 --- a/butane_core/src/db/mod.rs +++ b/butane_core/src/db/mod.rs @@ -572,9 +572,10 @@ pub fn get_backend(name: &str) -> Option> { /// Connect to a database. For non-boxed connections, see individual /// [`Backend`] implementations. pub fn connect(spec: &ConnectionSpec) -> Result { - get_backend(&spec.backend_name) + let conn = get_backend(&spec.backend_name) .ok_or_else(|| Error::UnknownBackend(spec.backend_name.clone()))? - .connect(&spec.conn_str) + .connect(&spec.conn_str)?; + Ok(conn) } /// Connect to a database. For non-boxed connections, see individual diff --git a/butane_core/src/db/pg.rs b/butane_core/src/db/pg.rs index fb99622e..40d8c9d1 100644 --- a/butane_core/src/db/pg.rs +++ b/butane_core/src/db/pg.rs @@ -32,11 +32,6 @@ impl PgBackend { PgBackend {} } } -impl PgBackend { - async fn connect(&self, params: &str) -> Result { - PgConnection::open(params).await - } -} #[async_trait] impl Backend for PgBackend { @@ -55,12 +50,14 @@ impl Backend for PgBackend { } fn connect(&self, path: &str) -> Result { - SyncAdapter::new(self.clone())?.connect(path) + debug!("connecting via sync adapter"); + let conn = SyncAdapter::new(self.clone())?.connect(path)?; + Ok(conn) } async fn connect_async(&self, path: &str) -> Result { Ok(Connection { - conn: Box::new(self.connect(path).await?), + conn: Box::new(PgConnection::open(path).await?), }) } } diff --git a/butane_core/src/db/sync_adapter.rs b/butane_core/src/db/sync_adapter.rs index 9ac4587f..ee5c6578 100644 --- a/butane_core/src/db/sync_adapter.rs +++ b/butane_core/src/db/sync_adapter.rs @@ -1,44 +1,57 @@ use crate::db::{Backend, RawQueryResult}; use crate::migrations::adb; use crate::query::{BoolExpr, Order}; -use crate::{Column, Result, SqlVal, SqlValRef}; +use crate::{debug, Column, Result, SqlVal, SqlValRef}; use async_trait::async_trait; use std::future::Future; +use std::sync::Arc; #[derive(Debug)] pub struct SyncAdapter { runtime_handle: tokio::runtime::Handle, - _runtime: Option, + _runtime: Option>, inner: T, } impl SyncAdapter { - // TODO needs an inner new that preserves the handle pub fn new(inner: T) -> Result { // TODO needs to check that the existing runtime isn't a current_thread // if it is, handle.block_on can't drive IO. // We can create a new runtime in that case, but not on the same thread. match tokio::runtime::Handle::try_current() { - Ok(handle) => Ok(Self { - runtime_handle: handle, - _runtime: None, - inner, - }), + Ok(handle) => { + debug!("Using existing tokio runtime"); + Ok(Self { + runtime_handle: handle, + _runtime: None, + inner, + }) + } Err(_) => { + debug!("Creating new tokio runtime"); let runtime = tokio::runtime::Builder::new_multi_thread() .worker_threads(1) .enable_all() .build()?; Ok(Self { runtime_handle: runtime.handle().clone(), - _runtime: Some(runtime), + _runtime: Some(Arc::new(runtime)), inner, }) } } } + /// Creates a new SyncAdapater for a different type, using the same runtime + fn chain(&self, inner: S) -> SyncAdapter { + SyncAdapter { + runtime_handle: self.runtime_handle.clone(), + _runtime: self._runtime.as_ref().map(|r| r.clone()), + inner, + } + } + fn block_on(&self, future: F) -> F::Output { self.runtime_handle.block_on(future) } @@ -126,9 +139,17 @@ where T: crate::db::BackendConnection, { fn transaction(&mut self) -> Result> { + // We can't use chain because of the lifetimes and mutable borrows below, + // so set up these runtime clones now. + let runtime_handle = self.runtime_handle.clone(); + let runtime = self._runtime.as_ref().map(|r| r.clone()); let transaction: crate::db::Transaction = self.runtime_handle.block_on(self.inner.transaction())?; - let transaction_adapter = SyncAdapter::new(transaction.trans)?; + let transaction_adapter = SyncAdapter { + runtime_handle, + _runtime: runtime, + inner: transaction.trans, + }; Ok(crate::db::sync::Transaction::new(Box::new( transaction_adapter, ))) @@ -181,9 +202,10 @@ where } fn connect(&self, conn_str: &str) -> Result { let conn_async = self.block_on(self.inner.connect_async(conn_str))?; - Ok(crate::db::sync::Connection { - conn: Box::new(SyncAdapter::new(conn_async.conn)?), - }) + let conn = crate::db::sync::Connection { + conn: Box::new(self.chain(conn_async.conn)), + }; + Ok(conn) } async fn connect_async(&self, conn_str: &str) -> Result { self.inner.connect_async(conn_str).await diff --git a/butane_core/src/migrations/mod.rs b/butane_core/src/migrations/mod.rs index 16566bbf..89e540d8 100644 --- a/butane_core/src/migrations/mod.rs +++ b/butane_core/src/migrations/mod.rs @@ -132,7 +132,12 @@ pub trait Migrations: Clone { where Self: Send + 'static, { - apply_unapplied_migrations_async(self.clone(), conn).await + let m2 = self.clone(); + conn.with_sync(move |conn| { + m2.migrate(conn)?; + Ok(()) + }) + .await } /// Remove all applied migrations. @@ -152,30 +157,19 @@ pub trait Migrations: Clone { } Ok(()) } -} -pub fn apply_unapplied_migrations( - migrations: &impl Migrations, - conn: &mut impl crate::db::sync::BackendConnection, -) -> Result<()> { - let to_apply = migrations.unapplied_migrations(conn)?; - for migration in to_apply { - crate::info!("Applying migration {}", migration.name()); - migration.apply(conn)?; + /// Remove all applied migrations. + async fn unmigrate_async(&self, conn: &mut crate::db::Connection) -> Result<()> + where + Self: Send + 'static, + { + let m2 = self.clone(); + conn.with_sync(move |conn| { + m2.unmigrate(conn)?; + Ok(()) + }) + .await } - Ok(()) -} - -async fn apply_unapplied_migrations_async( - migrations: M, - conn: &mut crate::db::Connection, -) -> Result<()> { - conn.with_sync(|conn| { - let m2 = migrations; // temp variable to force pass-by-value into the closure to satisfy Send - apply_unapplied_migrations(&m2, conn)?; - Ok(()) - }) - .await } /// Extension of [`Migrations`] to modify the series of migrations. diff --git a/butane_test_helper/Cargo.toml b/butane_test_helper/Cargo.toml index ee474be8..9256a888 100644 --- a/butane_test_helper/Cargo.toml +++ b/butane_test_helper/Cargo.toml @@ -16,6 +16,7 @@ block-id = "0.2" butane_core = { features = ["pg", "sqlite"], workspace = true } libc = "0.2" log.workspace = true +maybe-async-cfg.workspace = true nonempty.workspace = true once_cell = { workspace = true } tokio-postgres = { features = ["with-geo-types-0_7"], workspace = true } @@ -23,5 +24,6 @@ rand.workspace = true tempfile.workspace = true uuid = { features = ["v4"], workspace = true } + [package.metadata.release] release = false diff --git a/butane_test_helper/src/lib.rs b/butane_test_helper/src/lib.rs index 0ed30afd..59893ef9 100644 --- a/butane_test_helper/src/lib.rs +++ b/butane_test_helper/src/lib.rs @@ -2,9 +2,11 @@ //! Macros depend on [`butane_core`], `env_logger` and [`log`]. #![deny(missing_docs)] +use butane_core::db::sync::BackendConnection as BackendConnectionSync; use butane_core::db::sync::Connection as ConnectionSync; use butane_core::db::{ - connect_async, get_backend, pg, sqlite, Backend, BackendConnection, Connection, ConnectionSpec, + connect, connect_async, get_backend, pg, sqlite, Backend, BackendConnection, Connection, + ConnectionSpec, }; use butane_core::migrations::{self, MemMigrations, Migration, Migrations, MigrationsMut}; use once_cell::sync::Lazy; @@ -19,9 +21,9 @@ use block_id::{Alphabet, BlockId}; use uuid::Uuid; /// Create a postgres [`Connection`]. -pub async fn pg_connection() -> (ConnectionSync, PgSetupData) { +pub fn pg_connection() -> (ConnectionSync, PgSetupData) { let backend = get_backend(pg::BACKEND_NAME).unwrap(); - let data = pg_setup().await; + let data = pg_setup_sync(); (backend.connect(&pg_connstr(&data)).unwrap(), data) } @@ -147,6 +149,33 @@ extern "C" fn proc_teardown() { static TMP_SERVER: Lazy>> = Lazy::new(|| Mutex::new(Some(create_tmp_server()))); +/// Create a running empty postgres database named `butane_test_`. +pub fn pg_setup_sync() -> PgSetupData { + log::trace!("starting pg_setup"); + // By default we set up a temporary, local postgres server just + // for this test. This can be overridden by the environment + // variable BUTANE_PG_CONNSTR + let connstr = match std::env::var("BUTANE_PG_CONNSTR") { + Ok(connstr) => connstr, + Err(_) => { + let server_mguard = &TMP_SERVER.deref().lock().unwrap(); + let server: &PgServerState = server_mguard.as_ref().unwrap(); + let host = server.sockdir.path().to_str().unwrap(); + format!("host={host} user=postgres") + } + }; + let new_dbname = format!("butane_test_{}", Uuid::new_v4().simple()); + log::info!("new db is `{}`", &new_dbname); + + let mut conn = connect(&ConnectionSpec::new("pg", &connstr)).unwrap(); + log::debug!("closed is {}", BackendConnectionSync::is_closed(&conn)); + conn.execute(format!("CREATE DATABASE {new_dbname};")) + .unwrap(); + + let connstr = format!("{connstr} dbname={new_dbname}"); + PgSetupData { connstr } +} + /// Create a running empty postgres database named `butane_test_`. pub async fn pg_setup() -> PgSetupData { log::trace!("starting pg_setup"); @@ -168,6 +197,7 @@ pub async fn pg_setup() -> PgSetupData { let mut conn = connect_async(&ConnectionSpec::new("pg", &connstr)) .await .unwrap(); + log::debug!("[async]closed is {}", BackendConnection::is_closed(&conn)); conn.execute(format!("CREATE DATABASE {new_dbname};")) .await .unwrap(); diff --git a/examples/getting_started/tests/unmigrate.rs b/examples/getting_started/tests/unmigrate.rs index 6056ffba..35b06f16 100644 --- a/examples/getting_started/tests/unmigrate.rs +++ b/examples/getting_started/tests/unmigrate.rs @@ -1,8 +1,11 @@ +// todo re-enable this when we have a sync version of the test machinery available + +/* use butane::db::{BackendConnection, Connection}; use butane::migrations::Migrations; use butane::DataObjectOpAsync; use butane_test_helper::*; -use tokio; + use getting_started::models::{Blog, Post, Tag}; @@ -48,3 +51,4 @@ async fn migrate_and_unmigrate(mut connection: Connection) { migrations.unmigrate(&mut connection).await.unwrap(); } testall_no_migrate!(migrate_and_unmigrate); +*/ diff --git a/examples/getting_started_async/tests/rollback.rs b/examples/getting_started_async/tests/rollback.rs index 2da3fd85..bea1190e 100644 --- a/examples/getting_started_async/tests/rollback.rs +++ b/examples/getting_started_async/tests/rollback.rs @@ -1,5 +1,5 @@ use butane::db::{BackendConnection, Connection}; -use butane::migrations::{Migration, Migrations}; +use butane::migrations::Migrations; use butane::DataObjectOpAsync; use butane_test_helper::*; @@ -34,38 +34,16 @@ async fn insert_data(connection: &Connection) { post.save(connection).await.unwrap(); } -async fn migrate_and_rollback(mut connection: Connection) { +async fn migrate_and_unmigrate(mut connection: Connection) { // Migrate forward. let base_dir = std::path::PathBuf::from(".butane"); let migrations = butane_cli::get_migrations(&base_dir).unwrap(); - let to_apply = migrations.unapplied_migrations(&connection).await.unwrap(); - for migration in &to_apply { - if connection.backend_name() == "pg" - && migration.name() == "20240115_023841384_dbconstraints" - { - // migration 20240115_023841384_dbconstraints failed: Postgres error db error: - // ERROR: cannot drop table tag because other objects depend on it - // DETAIL: constraint post_tags_many__butane_tmp_has_fkey1 on table post_tags_many depends on table tag - let err = migration.apply(&mut connection).await.unwrap_err(); - eprintln!("Migration {} failed: {err:?}", migration.name()); - return; - } - migration - .apply(&mut connection) - .await - .unwrap_or_else(|err| panic!("migration {} failed: {err}", migration.name())); - eprintln!("Applied {}", migration.name()); - } + + migrations.migrate(&mut connection).await.unwrap(); insert_data(&connection).await; - // Rollback migrations. - for migration in to_apply.iter().rev() { - migration - .downgrade(&mut connection) - .await - .unwrap_or_else(|err| panic!("rollback of {} failed: {err}", migration.name())); - eprintln!("Rolled back {}", migration.name()); - } + // Undo migrations. + migrations.unmigrate(&mut connection).await.unwrap(); } -testall_no_migrate!(migrate_and_rollback); +testall_no_migrate!(migrate_and_unmigrate); diff --git a/examples/newtype/tests/unmigrate.rs b/examples/newtype/tests/unmigrate.rs index c4074e38..798f7691 100644 --- a/examples/newtype/tests/unmigrate.rs +++ b/examples/newtype/tests/unmigrate.rs @@ -32,11 +32,11 @@ async fn migrate_and_unmigrate(mut connection: Connection) { let base_dir = std::path::PathBuf::from(".butane"); let migrations = butane_cli::get_migrations(&base_dir).unwrap(); - migrations.migrate(&mut connection).await.unwrap(); + migrations.migrate_async(&mut connection).await.unwrap(); insert_data(&connection).await; // Undo migrations. - migrations.unmigrate(&mut connection).await.unwrap(); + migrations.unmigrate_async(&mut connection).await.unwrap(); } testall_no_migrate!(migrate_and_unmigrate); From c5810834a86c1eed9983ffb913bd69236e9871f9 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Thu, 25 Jul 2024 22:17:56 -0400 Subject: [PATCH 41/78] Last test passing (I think) --- example/build.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/build.rs b/example/build.rs index d1af9f62..4b0430ec 100644 --- a/example/build.rs +++ b/example/build.rs @@ -11,7 +11,7 @@ fn main() { std::fs::remove_dir_all(dir).unwrap(); } let db = "db.sqlite"; - if std::path::Path::new(&db).is_dir() { + if std::path::Path::new(&db).is_file() { std::fs::remove_file(db).unwrap(); } } From 1d157b3078bd4ff06b856693de894246b9c229d7 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Fri, 26 Jul 2024 08:16:15 -0400 Subject: [PATCH 42/78] Fix doctest --- butane/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/butane/src/lib.rs b/butane/src/lib.rs index f3233fe9..25b424ea 100644 --- a/butane/src/lib.rs +++ b/butane/src/lib.rs @@ -71,7 +71,7 @@ pub mod db { /// rank: i32, /// nationality: String /// } -/// # tokio_test::block_opn(async { +/// # tokio_test::block_on(async { /// let e: BoolExpr = filter!(Contestant, nationality == "US" && rank < 42); /// let first_place = 1; /// let e2 = filter!(Contestant, rank == { first_place }); @@ -154,7 +154,7 @@ macro_rules! colname { /// } /// /// # tokio_test::block_on(async { -/// let conn = butane::db::connect(&ConnectionSpec::new("sqlite", "foo.db")).await.unwrap(); +/// let conn = butane::db::connect_async(&ConnectionSpec::new("sqlite", "foo.db")).await.unwrap(); /// let alice: Result = find!(Contestant, name == "Alice", &conn); /// # }) ///``` From cbad41a4b2851416408225884f0d211459098b56 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Fri, 26 Jul 2024 21:50:15 -0400 Subject: [PATCH 43/78] Fix typo and fake test --- butane/tests/fake.rs | 2 +- butane_core/src/db/sync_adapter.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/butane/tests/fake.rs b/butane/tests/fake.rs index 8040599e..be4f91e5 100644 --- a/butane/tests/fake.rs +++ b/butane/tests/fake.rs @@ -1,5 +1,5 @@ use butane::db::Connection; -use butane::prelude::*; +use butane::prelude_async::*; use butane::{find, ForeignKey}; use butane_test_helper::*; use fake::{Fake, Faker}; diff --git a/butane_core/src/db/sync_adapter.rs b/butane_core/src/db/sync_adapter.rs index ee5c6578..e0637989 100644 --- a/butane_core/src/db/sync_adapter.rs +++ b/butane_core/src/db/sync_adapter.rs @@ -43,7 +43,7 @@ impl SyncAdapter { } } - /// Creates a new SyncAdapater for a different type, using the same runtime + /// Creates a new SyncAdapter for a different type, using the same runtime fn chain(&self, inner: S) -> SyncAdapter { SyncAdapter { runtime_handle: self.runtime_handle.clone(), From f3bd17742399e88347a47d56bf190b1fc10e5449 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Sat, 27 Jul 2024 14:38:02 -0400 Subject: [PATCH 44/78] Re-enable r2d2 feature --- Cargo.lock | 1 + async_checklist.md | 3 +- butane/Cargo.toml | 3 +- butane/tests/r2d2.rs | 12 ++++-- butane/tests/unmigrate.rs | 4 +- butane_core/Cargo.toml | 3 +- butane_core/src/db/mod.rs | 7 +--- butane_core/src/db/r2.rs | 69 ++++++++++++++++++++++++++++++++--- butane_test_helper/src/lib.rs | 17 ++++++--- 9 files changed, 91 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f73275d3..d93c6bac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -350,6 +350,7 @@ dependencies = [ "postgres-native-tls", "proc-macro2 1.0.79", "quote 1.0.35", + "r2d2", "rand", "regex", "rusqlite", diff --git a/async_checklist.md b/async_checklist.md index 23789992..7045157f 100644 --- a/async_checklist.md +++ b/async_checklist.md @@ -3,7 +3,8 @@ * [ ] Establish soundness for unsafe sections of AsyncAdapter * [ ] Consider publishing `AsyncAdapter` into its own crate * [ ] Ensure Postgres works in sync -* [ ] Re-enable R2D2 for sync and find async alternative (deadpool) +* [x] Re-enable R2D2 for sync +* [x] Integrate deadpool or bb8 for async connection poll * [ ] Fix `#[async_trait(?Send)]` to set up Send bound again as it's required for e.g. `tokio::spawn` * [ ] Separate sync and async examples * [ ] Should async_adapter be under a separate feature? Do we need it for migrations? diff --git a/butane/Cargo.toml b/butane/Cargo.toml index 9c12adf7..249e4e58 100644 --- a/butane/Cargo.toml +++ b/butane/Cargo.toml @@ -22,8 +22,7 @@ pg = ["butane_core/pg"] datetime = ["butane_codegen/datetime", "butane_core/datetime"] debug = ["butane_core/debug"] log = ["butane_core/log"] -# todo re-enable r2d2 -#r2d2 = ["butane_core/r2d2"] +r2d2 = ["butane_core/r2d2"] tls = ["butane_core/tls"] uuid = ["butane_codegen/uuid", "butane_core/uuid"] diff --git a/butane/tests/r2d2.rs b/butane/tests/r2d2.rs index 42f7226c..b2b91ca3 100644 --- a/butane/tests/r2d2.rs +++ b/butane/tests/r2d2.rs @@ -1,5 +1,5 @@ #[cfg(any(feature = "pg", feature = "sqlite"))] -use butane::db; +use butane::db::r2::ConnectionManager; #[cfg(feature = "pg")] use butane_test_helper::pg_connspec; #[cfg(any(feature = "pg", feature = "sqlite"))] @@ -12,7 +12,7 @@ use r2d2_for_test as r2d2; #[cfg(feature = "sqlite")] #[test] fn r2d2_sqlite() { - let manager = db::ConnectionManager::new(sqlite_connspec()); + let manager = ConnectionManager::new(sqlite_connspec()); let pool = r2d2::Pool::builder().max_size(3).build(manager).unwrap(); { @@ -30,8 +30,12 @@ fn r2d2_sqlite() { #[cfg(feature = "pg")] #[test] fn r2d2_pq() { - let (connspec, _data) = pg_connspec(); - let manager = db::ConnectionManager::new(connspec); + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let (connspec, _data) = rt.block_on(pg_connspec()); + let manager = ConnectionManager::new(connspec); let pool = r2d2::Pool::builder().max_size(3).build(manager).unwrap(); { diff --git a/butane/tests/unmigrate.rs b/butane/tests/unmigrate.rs index 2f2cfa0e..e9d4556a 100644 --- a/butane/tests/unmigrate.rs +++ b/butane/tests/unmigrate.rs @@ -1,12 +1,12 @@ //! Test the "current" migration created by the butane_test_helper due to //! all of the other tests in the butane/tests directory. #![cfg(test)] -use butane::db::Connection; +use butane::db::{BackendConnection, Connection}; use butane::migrations::{Migration, Migrations}; use butane_test_helper::*; async fn unmigrate(mut connection: Connection) { - let mem_migrations = create_current_migrations(&connection); + let mem_migrations = create_current_migrations(connection.backend()); connection .with_sync(move |conn| { diff --git a/butane_core/Cargo.toml b/butane_core/Cargo.toml index 3a996103..0dedac73 100644 --- a/butane_core/Cargo.toml +++ b/butane_core/Cargo.toml @@ -47,8 +47,7 @@ tokio-postgres = { optional = true, workspace = true } postgres-native-tls = { version = "0.5", optional = true } proc-macro2 = { workspace = true } quote = { workspace = true } -# todo re-enable r2d2 -#r2d2 = { optional = true, workspace = true } +r2d2 = { optional = true, workspace = true } rand = { optional = true, workspace = true } regex = { version = "1.5", features = ["std"] } rusqlite = { workspace = true, optional = true } diff --git a/butane_core/src/db/mod.rs b/butane_core/src/db/mod.rs index bcc9b429..50b1432b 100644 --- a/butane_core/src/db/mod.rs +++ b/butane_core/src/db/mod.rs @@ -48,11 +48,8 @@ pub mod pg; #[cfg(feature = "sqlite")] pub mod sqlite; -// TODO re-enable -//#[cfg(feature = "r2d2")] -//pub mod r2; -//#[cfg(feature = "r2d2")] -//pub use r2::ConnectionManager; +#[cfg(feature = "r2d2")] +pub mod r2; // Macros are always exported at the root of the crate use crate::connection_method_wrapper; diff --git a/butane_core/src/db/r2.rs b/butane_core/src/db/r2.rs index 281d75f0..3e9bc486 100644 --- a/butane_core/src/db/r2.rs +++ b/butane_core/src/db/r2.rs @@ -2,11 +2,11 @@ pub use r2d2::ManageConnection; -use crate::connection_method_wrapper; -use crate::db::{ - BackendConnection, Column, Connection, ConnectionMethods, ConnectionSpec, RawQueryResult, -}; -use crate::{query::BoolExpr, Result, SqlVal, SqlValRef}; +use crate::db::sync::{BackendConnection, Connection, ConnectionMethods}; +use crate::db::{Column, ConnectionSpec, RawQueryResult}; +use crate::{query::BoolExpr, query::Order, Result, SqlVal, SqlValRef}; + +use std::ops::Deref; /// R2D2 support for Butane. Implements [`r2d2::ManageConnection`]. #[derive(Clone, Debug)] @@ -36,4 +36,61 @@ impl ManageConnection for ConnectionManager { } } -connection_method_wrapper!(r2d2::PooledConnection); +impl ConnectionMethods for r2d2::PooledConnection { + fn execute(&self, sql: &str) -> Result<()> { + self.deref().execute(sql) + } + fn query<'c>( + &'c self, + table: &str, + columns: &[Column], + expr: Option, + limit: Option, + offset: Option, + sort: Option<&[Order]>, + ) -> Result> { + self.deref() + .query(table, columns, expr, limit, offset, sort) + } + fn insert_returning_pk( + &self, + table: &str, + columns: &[Column], + pkcol: &Column, + values: &[SqlValRef<'_>], + ) -> Result { + self.deref() + .insert_returning_pk(table, columns, pkcol, values) + } + /// Like `insert_returning_pk` but with no return value + fn insert_only(&self, table: &str, columns: &[Column], values: &[SqlValRef<'_>]) -> Result<()> { + self.deref().insert_only(table, columns, values) + } + /// Insert unless there's a conflict on the primary key column, in which case update + fn insert_or_replace( + &self, + table: &str, + columns: &[Column], + pkcol: &Column, + values: &[SqlValRef<'_>], + ) -> Result<()> { + self.deref() + .insert_or_replace(table, columns, pkcol, values) + } + fn update( + &self, + table: &str, + pkcol: Column, + pk: SqlValRef<'_>, + columns: &[Column], + values: &[SqlValRef<'_>], + ) -> Result<()> { + self.deref().update(table, pkcol, pk, columns, values) + } + fn delete_where(&self, table: &str, expr: BoolExpr) -> Result { + self.deref().delete_where(table, expr) + } + fn has_table(&self, table: &str) -> Result { + self.deref().has_table(table) + } +} diff --git a/butane_test_helper/src/lib.rs b/butane_test_helper/src/lib.rs index 59893ef9..d6c57bd9 100644 --- a/butane_test_helper/src/lib.rs +++ b/butane_test_helper/src/lib.rs @@ -217,9 +217,7 @@ pub fn pg_connstr(data: &PgSetupData) -> String { } /// Create a [`MemMigrations`]` for the "current" migration. -pub fn create_current_migrations(connection: &Connection) -> MemMigrations { - let backend = connection.backend(); - +pub fn create_current_migrations(backend: Box) -> MemMigrations { let mut root = std::env::current_dir().unwrap(); root.push(".butane/migrations"); let mut disk_migrations = migrations::from_root(&root); @@ -248,12 +246,19 @@ pub fn create_current_migrations(connection: &Connection) -> MemMigrations { } /// Populate the database schema. -pub async fn setup_db(conn: &mut Connection) { - let mem_migrations = create_current_migrations(conn); +pub async fn setup_db_async(conn: &mut Connection) { + let mem_migrations = create_current_migrations(conn.backend()); log::info!("created current migration"); mem_migrations.migrate_async(conn).await.unwrap(); } +/// Populate the database schema. +pub fn setup_db(conn: &mut ConnectionSync) { + let mem_migrations = create_current_migrations(conn.backend()); + log::info!("created current migration"); + mem_migrations.migrate(conn).unwrap(); +} + /// Create a sqlite [`Connection`]. pub fn sqlite_connection() -> ConnectionSync { let backend = get_backend(sqlite::BACKEND_NAME).unwrap(); @@ -283,7 +288,7 @@ macro_rules! maketest { log::info!("connecting to {}..", &$connstr); let mut conn = backend.connect_async(&$connstr).await.expect("Could not connect backend"); if $migrate { - butane_test_helper::setup_db(&mut conn).await; + butane_test_helper::setup_db_async(&mut conn).await; } log::info!("running test on {}..", &$connstr); $fname(conn).await; From ffe490f92b4f2e357e57ad84313087efe40da6be Mon Sep 17 00:00:00 2001 From: James Oakley Date: Wed, 28 Aug 2024 20:51:30 -0400 Subject: [PATCH 45/78] Changed approach to naming. Sync first, async gets a suffix --- butane/src/lib.rs | 4 +- butane/tests/basic.rs | 40 +- butane/tests/common/blog.rs | 6 +- butane/tests/custom_enum_derived.rs | 6 +- butane/tests/custom_pg.rs | 4 +- butane/tests/custom_type.rs | 6 +- butane/tests/fake.rs | 4 +- butane/tests/json.rs | 16 +- butane/tests/many.rs | 16 +- butane/tests/migration-tests.rs | 2 +- butane/tests/nullable.rs | 8 +- butane/tests/query.rs | 36 +- butane/tests/unmigrate.rs | 4 +- butane/tests/uuid.rs | 4 +- butane_cli/src/lib.rs | 2 +- butane_core/src/codegen/dbobj.rs | 4 +- butane_core/src/db/adapter.rs | 33 +- butane_core/src/db/connmethods.rs | 120 ++-- butane_core/src/db/dummy.rs | 27 +- butane_core/src/db/macros.rs | 8 +- butane_core/src/db/mod.rs | 791 ++++++++++++------------ butane_core/src/db/pg.rs | 12 +- butane_core/src/db/r2.rs | 2 +- butane_core/src/db/sqlite.rs | 9 +- butane_core/src/db/sync_adapter.rs | 40 +- butane_core/src/fkey.rs | 2 +- butane_core/src/lib.rs | 12 +- butane_core/src/many.rs | 13 +- butane_core/src/migrations/migration.rs | 10 +- butane_core/src/migrations/mod.rs | 31 +- butane_core/src/query/mod.rs | 17 +- butane_core/tests/connection.rs | 6 +- butane_core/tests/migration.rs | 8 +- butane_core/tests/transactions.rs | 8 +- butane_test_helper/src/lib.rs | 21 +- example/src/main.rs | 4 +- examples/getting_started/src/lib.rs | 2 +- examples/newtype/src/lib.rs | 10 +- examples/newtype/tests/unmigrate.rs | 6 +- 39 files changed, 664 insertions(+), 690 deletions(-) diff --git a/butane/src/lib.rs b/butane/src/lib.rs index 25b424ea..4ba3c5b2 100644 --- a/butane/src/lib.rs +++ b/butane/src/lib.rs @@ -187,7 +187,7 @@ pub mod prelude { pub use super::prelude_common::*; - pub use butane_core::db::sync::BackendConnection; + pub use butane_core::db::BackendConnection; pub use butane_core::many::ManyOpSync; pub use butane_core::query::QueryOpSync; pub use butane_core::DataObjectOpSync; @@ -199,7 +199,7 @@ pub mod prelude_async { //! Its use is recommended, but not required. pub use super::prelude_common::*; - pub use butane_core::db::BackendConnection; + pub use butane_core::db::BackendConnectionAsync; pub use butane_core::many::ManyOpAsync; pub use butane_core::query::QueryOpAsync; pub use butane_core::DataObjectOpAsync; diff --git a/butane/tests/basic.rs b/butane/tests/basic.rs index c103ebc5..2c4f6a9c 100644 --- a/butane/tests/basic.rs +++ b/butane/tests/basic.rs @@ -1,6 +1,6 @@ #![allow(clippy::disallowed_names)] -use butane::db::Connection; +use butane::db::ConnectionAsync; use butane::{butane_type, find, model, query, AutoPk, ForeignKey}; use butane::{colname, prelude_async::*}; use butane_test_helper::*; @@ -109,7 +109,7 @@ struct TimeHolder { pub when: chrono::DateTime, } -async fn basic_crud(conn: Connection) { +async fn basic_crud(conn: ConnectionAsync) { //create let mut foo = Foo::new(1); foo.bam = 0.1; @@ -143,7 +143,7 @@ async fn basic_crud(conn: Connection) { } testall!(basic_crud); -async fn basic_find(conn: Connection) { +async fn basic_find(conn: ConnectionAsync) { //create let mut foo1 = Foo::new(1); foo1.bar = 42; @@ -160,7 +160,7 @@ async fn basic_find(conn: Connection) { } testall!(basic_find); -async fn basic_query(conn: Connection) { +async fn basic_query(conn: ConnectionAsync) { //create let mut foo1 = Foo::new(1); foo1.bar = 42; @@ -182,7 +182,7 @@ async fn basic_query(conn: Connection) { } testall!(basic_query); -async fn basic_query_delete(conn: Connection) { +async fn basic_query_delete(conn: ConnectionAsync) { //create let mut foo1 = Foo::new(1); foo1.bar = 42; @@ -210,7 +210,7 @@ async fn basic_query_delete(conn: Connection) { } testall!(basic_query_delete); -async fn string_pk(conn: Connection) { +async fn string_pk(conn: ConnectionAsync) { let mut foo = Foo::new(1); foo.save(&conn).await.unwrap(); let mut bar = Bar::new("tarzan", foo); @@ -221,7 +221,7 @@ async fn string_pk(conn: Connection) { } testall!(string_pk); -async fn foreign_key(conn: Connection) { +async fn foreign_key(conn: ConnectionAsync) { let mut foo = Foo::new(1); foo.save(&conn).await.unwrap(); let mut bar = Bar::new("tarzan", foo.clone()); @@ -236,7 +236,7 @@ async fn foreign_key(conn: Connection) { } testall!(foreign_key); -async fn auto_pk(conn: Connection) { +async fn auto_pk(conn: ConnectionAsync) { let mut baz1 = Baz::new("baz1"); baz1.save(&conn).await.unwrap(); let mut baz2 = Baz::new("baz2"); @@ -248,7 +248,7 @@ async fn auto_pk(conn: Connection) { } testall!(auto_pk); -async fn only_pk(conn: Connection) { +async fn only_pk(conn: ConnectionAsync) { let mut obj = HasOnlyPk::new(1); obj.save(&conn).await.unwrap(); assert_eq!(obj.id, 1); @@ -260,7 +260,7 @@ async fn only_pk(conn: Connection) { } testall!(only_pk); -async fn only_auto_pk(conn: Connection) { +async fn only_auto_pk(conn: ConnectionAsync) { let mut obj = HasOnlyAutoPk::default(); obj.save(&conn).await.unwrap(); let pk = obj.id; @@ -272,7 +272,7 @@ async fn only_auto_pk(conn: Connection) { } testall!(only_auto_pk); -async fn basic_committed_transaction(mut conn: Connection) { +async fn basic_committed_transaction(mut conn: ConnectionAsync) { let tr = conn.transaction().await.unwrap(); // Create an object with a transaction and commit it @@ -287,7 +287,7 @@ async fn basic_committed_transaction(mut conn: Connection) { } testall!(basic_committed_transaction); -async fn basic_dropped_transaction(mut conn: Connection) { +async fn basic_dropped_transaction(mut conn: ConnectionAsync) { // Create an object with a transaction but never commit it { let tr = conn.transaction().await.unwrap(); @@ -305,7 +305,7 @@ async fn basic_dropped_transaction(mut conn: Connection) { } testall!(basic_dropped_transaction); -async fn basic_rollback_transaction(mut conn: Connection) { +async fn basic_rollback_transaction(mut conn: ConnectionAsync) { let tr = conn.transaction().await.unwrap(); // Create an object with a transaction but then roll back the transaction @@ -323,7 +323,7 @@ async fn basic_rollback_transaction(mut conn: Connection) { } testall!(basic_rollback_transaction); -async fn basic_unique_field_error_on_non_unique(conn: Connection) { +async fn basic_unique_field_error_on_non_unique(conn: ConnectionAsync) { let mut foo1 = Foo::new(1); foo1.bar = 42; foo1.save(&conn).await.unwrap(); @@ -347,7 +347,7 @@ async fn basic_unique_field_error_on_non_unique(conn: Connection) { } testall!(basic_unique_field_error_on_non_unique); -async fn fkey_same_type(conn: Connection) { +async fn fkey_same_type(conn: ConnectionAsync) { let mut o1 = SelfReferential::new(1); let mut o2 = SelfReferential::new(2); o2.save(&conn).await.unwrap(); @@ -362,7 +362,7 @@ async fn fkey_same_type(conn: Connection) { } testall!(fkey_same_type); -async fn cant_save_unsaved_fkey(conn: Connection) { +async fn cant_save_unsaved_fkey(conn: ConnectionAsync) { let foo = Foo::new(1); let mut bar = Bar::new("tarzan", foo); assert!(bar.save(&conn).await.is_err()); @@ -370,7 +370,7 @@ async fn cant_save_unsaved_fkey(conn: Connection) { testall!(cant_save_unsaved_fkey); #[cfg(feature = "datetime")] -async fn basic_time(conn: Connection) { +async fn basic_time(conn: ConnectionAsync) { let now = Utc::now(); let mut time = TimeHolder { id: 1, @@ -388,7 +388,7 @@ async fn basic_time(conn: Connection) { #[cfg(feature = "datetime")] testall!(basic_time); -async fn basic_load_first(conn: Connection) { +async fn basic_load_first(conn: ConnectionAsync) { //create let mut foo1 = Foo::new(1); foo1.bar = 42; @@ -409,7 +409,7 @@ async fn basic_load_first(conn: Connection) { } testall!(basic_load_first); -async fn basic_load_first_ordered(conn: Connection) { +async fn basic_load_first_ordered(conn: ConnectionAsync) { //create let mut foo1 = Foo::new(1); foo1.bar = 42; @@ -440,7 +440,7 @@ async fn basic_load_first_ordered(conn: Connection) { } testall!(basic_load_first_ordered); -async fn save_upserts_by_default(conn: Connection) { +async fn save_upserts_by_default(conn: ConnectionAsync) { let mut foo = Foo::new(1); foo.bar = 42; foo.save(&conn).await.unwrap(); diff --git a/butane/tests/common/blog.rs b/butane/tests/common/blog.rs index 75eeb3d1..0f0497ef 100644 --- a/butane/tests/common/blog.rs +++ b/butane/tests/common/blog.rs @@ -1,6 +1,6 @@ //! Helpers for several tests. use butane::{dataresult, model, DataObject, DataObjectOpAsync}; -use butane::{db::Connection, ForeignKey, Many}; +use butane::{db::ConnectionAsync, ForeignKey, Many}; #[cfg(feature = "datetime")] use chrono::{naive::NaiveDateTime, offset::Utc}; #[cfg(feature = "fake")] @@ -100,7 +100,7 @@ impl Tag { } } -pub async fn create_tag(conn: &Connection, name: &str) -> Tag { +pub async fn create_tag(conn: &ConnectionAsync, name: &str) -> Tag { let mut tag = Tag::new(name); tag.save(conn).await.unwrap(); tag @@ -110,7 +110,7 @@ pub async fn create_tag(conn: &Connection, name: &str) -> Tag { /// 1. "Cats" /// 2. "Mountains" #[allow(dead_code)] // only used by some test files -pub async fn setup_blog(conn: &Connection) { +pub async fn setup_blog(conn: &ConnectionAsync) { let mut cats_blog = Blog::new(1, "Cats"); cats_blog.save(conn).await.unwrap(); let mut mountains_blog = Blog::new(2, "Mountains"); diff --git a/butane/tests/custom_enum_derived.rs b/butane/tests/custom_enum_derived.rs index 7a0db723..0c1bfe4e 100644 --- a/butane/tests/custom_enum_derived.rs +++ b/butane/tests/custom_enum_derived.rs @@ -1,5 +1,5 @@ // Tests deriving FieldType for an enum -use butane::db::Connection; +use butane::db::ConnectionAsync; use butane::prelude_async::*; use butane::{model, query}; use butane::{FieldType, FromSql, SqlVal, ToSql}; @@ -24,7 +24,7 @@ impl HasCustomField2 { } } -async fn roundtrip_custom_type(conn: Connection) { +async fn roundtrip_custom_type(conn: ConnectionAsync) { //create let mut obj = HasCustomField2::new(1, Whatsit::Foo); obj.save(&conn).await.unwrap(); @@ -35,7 +35,7 @@ async fn roundtrip_custom_type(conn: Connection) { } testall!(roundtrip_custom_type); -async fn query_custom_type(conn: Connection) { +async fn query_custom_type(conn: ConnectionAsync) { //create let mut obj_foo = HasCustomField2::new(1, Whatsit::Foo); obj_foo.save(&conn).await.unwrap(); diff --git a/butane/tests/custom_pg.rs b/butane/tests/custom_pg.rs index 05d3b95f..b0c7b3ed 100644 --- a/butane/tests/custom_pg.rs +++ b/butane/tests/custom_pg.rs @@ -3,7 +3,7 @@ mod custom_pg { use butane::custom::{SqlTypeCustom, SqlValRefCustom}; use butane::prelude_async::*; - use butane::{butane_type, db::Connection, model}; + use butane::{butane_type, db::ConnectionAsync, model}; use butane::{AutoPk, FieldType, FromSql, SqlType, SqlVal, SqlValRef, ToSql}; use butane_test_helper::{maketest, maketest_pg}; use geo_types; @@ -58,7 +58,7 @@ mod custom_pg { pt_to: Point, } - async fn roundtrip_custom(conn: Connection) { + async fn roundtrip_custom(conn: ConnectionAsync) { let mut trip = Trip { id: AutoPk::uninitialized(), pt_from: Point::new(0.0, 0.0), diff --git a/butane/tests/custom_type.rs b/butane/tests/custom_type.rs index ee444171..0af175e6 100644 --- a/butane/tests/custom_type.rs +++ b/butane/tests/custom_type.rs @@ -1,4 +1,4 @@ -use butane::db::Connection; +use butane::db::ConnectionAsync; use butane::prelude_async::*; use butane::{butane_type, model, query}; use butane::{FieldType, FromSql, SqlType, SqlVal, SqlValRef, ToSql}; @@ -60,7 +60,7 @@ impl HasCustomField { } } -async fn roundtrip_custom_type(conn: Connection) { +async fn roundtrip_custom_type(conn: ConnectionAsync) { //create let mut obj = HasCustomField::new(1, Frobnozzle::Foo); obj.save(&conn).await.unwrap(); @@ -71,7 +71,7 @@ async fn roundtrip_custom_type(conn: Connection) { } testall!(roundtrip_custom_type); -async fn query_custom_type(conn: Connection) { +async fn query_custom_type(conn: ConnectionAsync) { //create let mut obj_foo = HasCustomField::new(1, Frobnozzle::Foo); obj_foo.save(&conn).await.unwrap(); diff --git a/butane/tests/fake.rs b/butane/tests/fake.rs index be4f91e5..d5395326 100644 --- a/butane/tests/fake.rs +++ b/butane/tests/fake.rs @@ -1,4 +1,4 @@ -use butane::db::Connection; +use butane::db::ConnectionAsync; use butane::prelude_async::*; use butane::{find, ForeignKey}; use butane_test_helper::*; @@ -7,7 +7,7 @@ use fake::{Fake, Faker}; mod common; use common::blog::{Blog, Post, Tag}; -async fn fake_blog_post(conn: Connection) { +async fn fake_blog_post(conn: ConnectionAsync) { let mut fake_blog: Blog = Faker.fake(); fake_blog.save(&conn).await.unwrap(); diff --git a/butane/tests/json.rs b/butane/tests/json.rs index b820bc67..19d2c11c 100644 --- a/butane/tests/json.rs +++ b/butane/tests/json.rs @@ -4,7 +4,7 @@ use std::collections::{BTreeMap, HashMap}; use butane::model; use butane::prelude_async::*; -use butane::{db::Connection, FieldType}; +use butane::{db::ConnectionAsync, FieldType}; use butane_test_helper::*; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -26,7 +26,7 @@ impl FooJJ { } } -async fn json_null(conn: Connection) { +async fn json_null(conn: ConnectionAsync) { // create let id = 4; let mut foo = FooJJ::new(id); @@ -44,7 +44,7 @@ async fn json_null(conn: Connection) { } testall!(json_null); -async fn basic_json(conn: Connection) { +async fn basic_json(conn: ConnectionAsync) { // create let id = 4; let mut foo = FooJJ::new(id); @@ -89,7 +89,7 @@ impl FooHH { } } } -async fn basic_hashmap(conn: Connection) { +async fn basic_hashmap(conn: ConnectionAsync) { // create let id = 4; let mut foo = FooHH::new(id); @@ -127,7 +127,7 @@ impl FooFullPrefixHashMap { } } } -async fn basic_hashmap_full_prefix(conn: Connection) { +async fn basic_hashmap_full_prefix(conn: ConnectionAsync) { // create let id = 4; let mut foo = FooFullPrefixHashMap::new(id); @@ -165,7 +165,7 @@ impl FooBTreeMap { } } } -async fn basic_btreemap(conn: Connection) { +async fn basic_btreemap(conn: ConnectionAsync) { // create let id = 4; let mut foo = FooBTreeMap::new(id); @@ -209,7 +209,7 @@ impl FooHHO { } } } -async fn hashmap_with_object_values(conn: Connection) { +async fn hashmap_with_object_values(conn: ConnectionAsync) { // create let id = 4; let mut foo = FooHHO::new(id); @@ -255,7 +255,7 @@ impl OuterFoo { } } -async fn inline_json(conn: Connection) { +async fn inline_json(conn: ConnectionAsync) { // create let id = 4; let mut foo = OuterFoo::new(id, InlineFoo::new(4, 8)); diff --git a/butane/tests/many.rs b/butane/tests/many.rs index 15d819cc..172b3193 100644 --- a/butane/tests/many.rs +++ b/butane/tests/many.rs @@ -1,4 +1,4 @@ -use butane::db::Connection; +use butane::db::ConnectionAsync; use butane::prelude_async::*; use butane::{model, query::OrderDirection, AutoPk, Many}; use butane_test_helper::testall; @@ -47,7 +47,7 @@ struct AutoItem { val: String, } -async fn load_sorted_from_many(conn: Connection) { +async fn load_sorted_from_many(conn: ConnectionAsync) { let mut cats_blog = Blog::new(1, "Cats"); cats_blog.save(&conn).await.unwrap(); let mut post = Post::new( @@ -87,7 +87,7 @@ async fn load_sorted_from_many(conn: Connection) { } testall!(load_sorted_from_many); -async fn remove_one_from_many(conn: Connection) { +async fn remove_one_from_many(conn: ConnectionAsync) { let mut cats_blog = Blog::new(1, "Cats"); cats_blog.save(&conn).await.unwrap(); let mut post = Post::new( @@ -114,7 +114,7 @@ async fn remove_one_from_many(conn: Connection) { } testall!(remove_one_from_many); -async fn remove_multiple_from_many(conn: Connection) { +async fn remove_multiple_from_many(conn: ConnectionAsync) { let mut cats_blog = Blog::new(1, "Cats"); cats_blog.save(&conn).await.unwrap(); let mut post = Post::new( @@ -144,7 +144,7 @@ async fn remove_multiple_from_many(conn: Connection) { } testall!(remove_multiple_from_many); -async fn delete_all_from_many(conn: Connection) { +async fn delete_all_from_many(conn: ConnectionAsync) { let mut cats_blog = Blog::new(1, "Cats"); cats_blog.save(&conn).await.unwrap(); let mut post = Post::new( @@ -171,7 +171,7 @@ async fn delete_all_from_many(conn: Connection) { } testall!(delete_all_from_many); -async fn can_add_to_many_before_save(conn: Connection) { +async fn can_add_to_many_before_save(conn: ConnectionAsync) { // Verify that for an object with an auto-pk, we can add items to a Many field before we actually // save the original object (and thus get the actual pk); let mut obj = AutoPkWithMany::new(); @@ -185,7 +185,7 @@ async fn can_add_to_many_before_save(conn: Connection) { } testall!(can_add_to_many_before_save); -async fn cant_add_unsaved_to_many(_conn: Connection) { +async fn cant_add_unsaved_to_many(_conn: ConnectionAsync) { let unsaved_item = AutoItem { id: AutoPk::uninitialized(), val: "shiny".to_string(), @@ -199,7 +199,7 @@ async fn cant_add_unsaved_to_many(_conn: Connection) { } testall!(cant_add_unsaved_to_many); -async fn can_add_to_many_with_custom_table_name(conn: Connection) { +async fn can_add_to_many_with_custom_table_name(conn: ConnectionAsync) { let mut obj = RenamedAutoPkWithMany::new(); obj.tags.add(&create_tag(&conn, "blue").await).unwrap(); obj.tags.add(&create_tag(&conn, "red").await).unwrap(); diff --git a/butane/tests/migration-tests.rs b/butane/tests/migration-tests.rs index bca734ab..56f8731a 100644 --- a/butane/tests/migration-tests.rs +++ b/butane/tests/migration-tests.rs @@ -1,4 +1,4 @@ -use butane::db::sync::{BackendConnection, Connection}; +use butane::db::{BackendConnection, Connection}; use butane::migrations::{MemMigrations, Migration, MigrationMut, Migrations, MigrationsMut}; use butane::{SqlType, SqlVal}; use butane_core::codegen::{butane_type_with_migrations, model_with_migrations}; diff --git a/butane/tests/nullable.rs b/butane/tests/nullable.rs index 137263f0..9f791cef 100644 --- a/butane/tests/nullable.rs +++ b/butane/tests/nullable.rs @@ -1,4 +1,4 @@ -use butane::db::Connection; +use butane::db::ConnectionAsync; use butane::prelude_async::*; use butane::{model, query}; use butane_test_helper::*; @@ -15,7 +15,7 @@ impl WithNullable { } } -async fn basic_optional(conn: Connection) { +async fn basic_optional(conn: ConnectionAsync) { let mut with_none = WithNullable::new(1); with_none.save(&conn).await.unwrap(); @@ -31,7 +31,7 @@ async fn basic_optional(conn: Connection) { } testall!(basic_optional); -async fn query_optional_with_some(conn: Connection) { +async fn query_optional_with_some(conn: ConnectionAsync) { let mut obj = WithNullable::new(1); obj.save(&conn).await.unwrap(); @@ -55,7 +55,7 @@ async fn query_optional_with_some(conn: Connection) { } testall!(query_optional_with_some); -async fn query_optional_with_none(conn: Connection) { +async fn query_optional_with_none(conn: ConnectionAsync) { let mut obj = WithNullable::new(1); obj.save(&conn).await.unwrap(); diff --git a/butane/tests/query.rs b/butane/tests/query.rs index ade9c2d6..6534e78e 100644 --- a/butane/tests/query.rs +++ b/butane/tests/query.rs @@ -1,4 +1,4 @@ -use butane::db::Connection; +use butane::db::ConnectionAsync; use butane::prelude_async::*; use butane::query::BoolExpr; use butane::{colname, filter, find, query, Many}; @@ -10,7 +10,7 @@ mod common; use common::blog; use common::blog::{Blog, Post, PostMetadata, Tag}; -async fn equality(conn: Connection) { +async fn equality(conn: ConnectionAsync) { blog::setup_blog(&conn).await; let mut posts = query!(Post, published == true).load(&conn).await.unwrap(); assert_eq!(posts.len(), 3); @@ -21,7 +21,7 @@ async fn equality(conn: Connection) { } testall!(equality); -async fn equality_separate_dataresult(conn: Connection) { +async fn equality_separate_dataresult(conn: ConnectionAsync) { blog::setup_blog(&conn).await; let mut posts = query!(PostMetadata, published == true) .load(&conn) @@ -35,7 +35,7 @@ async fn equality_separate_dataresult(conn: Connection) { } testall!(equality_separate_dataresult); -async fn ordered(conn: Connection) { +async fn ordered(conn: ConnectionAsync) { blog::setup_blog(&conn).await; let posts = query!(Post, published == true) .order_asc(colname!(Post, title)) @@ -49,7 +49,7 @@ async fn ordered(conn: Connection) { } testall!(ordered); -async fn comparison(conn: Connection) { +async fn comparison(conn: ConnectionAsync) { blog::setup_blog(&conn).await; let mut posts = query!(Post, likes < 5).load(&conn).await.unwrap(); assert_eq!(posts.len(), 2); @@ -59,7 +59,7 @@ async fn comparison(conn: Connection) { } testall!(comparison); -async fn like(conn: Connection) { +async fn like(conn: ConnectionAsync) { blog::setup_blog(&conn).await; let mut posts = query!(Post, title.like("M%")).load(&conn).await.unwrap(); assert_eq!(posts.len(), 2); @@ -69,7 +69,7 @@ async fn like(conn: Connection) { } testall!(like); -async fn combination(conn: Connection) { +async fn combination(conn: ConnectionAsync) { blog::setup_blog(&conn).await; let posts = query!(Post, published == true && likes < 5) .load(&conn) @@ -80,7 +80,7 @@ async fn combination(conn: Connection) { } testall!(combination); -async fn combination_allof(conn: Connection) { +async fn combination_allof(conn: ConnectionAsync) { blog::setup_blog(&conn).await; let posts = Post::query() .filter(BoolExpr::AllOf(vec![ @@ -96,7 +96,7 @@ async fn combination_allof(conn: Connection) { } testall!(combination_allof); -async fn not_found(conn: Connection) { +async fn not_found(conn: ConnectionAsync) { blog::setup_blog(&conn).await; let posts = query!(Post, published == false && likes > 5) .load(&conn) @@ -106,7 +106,7 @@ async fn not_found(conn: Connection) { } testall!(not_found); -async fn rustval(conn: Connection) { +async fn rustval(conn: ConnectionAsync) { blog::setup_blog(&conn).await; // We don't need to escape into rust for this, but we can let post = find!(Post, title == { "The Tiger" }, &conn).unwrap(); @@ -119,7 +119,7 @@ async fn rustval(conn: Connection) { } testall!(rustval); -async fn fkey_match(conn: Connection) { +async fn fkey_match(conn: ConnectionAsync) { blog::setup_blog(&conn).await; let blog: Blog = find!(Blog, name == "Cats", &conn).unwrap(); let mut posts = query!(Post, blog == { &blog }).load(&conn).await.unwrap(); @@ -141,7 +141,7 @@ async fn fkey_match(conn: Connection) { } testall!(fkey_match); -async fn many_load(conn: Connection) { +async fn many_load(conn: ConnectionAsync) { blog::setup_blog(&conn).await; let post: Post = find!(Post, title == "The Tiger", &conn).unwrap(); let tags = post.tags.load(&conn).await.unwrap(); @@ -152,7 +152,7 @@ async fn many_load(conn: Connection) { } testall!(many_load); -async fn many_serialize(conn: Connection) { +async fn many_serialize(conn: ConnectionAsync) { blog::setup_blog(&conn).await; let post: Post = find!(Post, title == "The Tiger", &conn).unwrap(); let tags_json: String = serde_json::to_string(&post.tags).unwrap(); @@ -165,7 +165,7 @@ async fn many_serialize(conn: Connection) { } testall!(many_serialize); -async fn many_objects_with_tag(conn: Connection) { +async fn many_objects_with_tag(conn: ConnectionAsync) { blog::setup_blog(&conn).await; let mut posts = query!(Post, tags.contains("danger")) .load(&conn) @@ -178,7 +178,7 @@ async fn many_objects_with_tag(conn: Connection) { } testall!(many_objects_with_tag); -async fn many_objects_with_tag_explicit(conn: Connection) { +async fn many_objects_with_tag_explicit(conn: ConnectionAsync) { blog::setup_blog(&conn).await; let mut posts = query!(Post, tags.contains(tag == "danger")) .load(&conn) @@ -192,7 +192,7 @@ async fn many_objects_with_tag_explicit(conn: Connection) { testall!(many_objects_with_tag_explicit); #[cfg(feature = "datetime")] -async fn by_timestamp(conn: Connection) { +async fn by_timestamp(conn: ConnectionAsync) { blog::setup_blog(&conn).await; let mut post = find!(Post, title == "Sir Charles", &conn).unwrap(); // Pretend this post was published in 1970 @@ -234,7 +234,7 @@ async fn by_timestamp(conn: Connection) { #[cfg(feature = "datetime")] testall!(by_timestamp); -async fn limit(conn: Connection) { +async fn limit(conn: ConnectionAsync) { blog::setup_blog(&conn).await; let posts = Post::query() .order_asc(colname!(Post, title)) @@ -248,7 +248,7 @@ async fn limit(conn: Connection) { } testall!(limit); -async fn offset(conn: Connection) { +async fn offset(conn: ConnectionAsync) { blog::setup_blog(&conn).await; // Now get the more posts after the two we got in the limit test above let posts = Post::query() diff --git a/butane/tests/unmigrate.rs b/butane/tests/unmigrate.rs index e9d4556a..234ed899 100644 --- a/butane/tests/unmigrate.rs +++ b/butane/tests/unmigrate.rs @@ -1,11 +1,11 @@ //! Test the "current" migration created by the butane_test_helper due to //! all of the other tests in the butane/tests directory. #![cfg(test)] -use butane::db::{BackendConnection, Connection}; +use butane::db::{BackendConnectionAsync, ConnectionAsync}; use butane::migrations::{Migration, Migrations}; use butane_test_helper::*; -async fn unmigrate(mut connection: Connection) { +async fn unmigrate(mut connection: ConnectionAsync) { let mem_migrations = create_current_migrations(connection.backend()); connection diff --git a/butane/tests/uuid.rs b/butane/tests/uuid.rs index 6214fe76..d55c1d9b 100644 --- a/butane/tests/uuid.rs +++ b/butane/tests/uuid.rs @@ -1,4 +1,4 @@ -use butane::db::Connection; +use butane::db::ConnectionAsync; use butane::model; use butane::prelude_async::*; use butane_test_helper::*; @@ -16,7 +16,7 @@ impl FooUU { } } -async fn basic_uuid(conn: Connection) { +async fn basic_uuid(conn: ConnectionAsync) { //create let id = Uuid::new_v4(); #[allow(clippy::disallowed_names)] diff --git a/butane_cli/src/lib.rs b/butane_cli/src/lib.rs index 371feef4..f621d1a6 100644 --- a/butane_cli/src/lib.rs +++ b/butane_cli/src/lib.rs @@ -13,8 +13,8 @@ use std::{ path::{Path, PathBuf}, }; -use butane::db::sync::{Connection, ConnectionMethods}; use butane::db::Backend; +use butane::db::{Connection, ConnectionMethods}; use butane::migrations::adb; use butane::migrations::adb::{diff, AColumn, ARef, Operation, ADB}; use butane::migrations::{ diff --git a/butane_core/src/codegen/dbobj.rs b/butane_core/src/codegen/dbobj.rs index 40dcf100..ad808fa1 100644 --- a/butane_core/src/codegen/dbobj.rs +++ b/butane_core/src/codegen/dbobj.rs @@ -83,11 +83,11 @@ pub fn impl_dbobject(ast_struct: &ItemStruct, config: &Config) -> TokenStream2 { fn pk_mut(&mut self) -> &mut impl butane::PrimaryKeyType { &mut self.#pkident } - async fn save_many_to_many_async(&mut self, #conn_arg_name: &impl butane::db::ConnectionMethods) -> butane::Result<()> { + async fn save_many_to_many_async(&mut self, #conn_arg_name: &impl butane::db::ConnectionMethodsAsync) -> butane::Result<()> { #many_save_async Ok(()) } - fn save_many_to_many_sync(&mut self, #conn_arg_name: &impl butane::db::sync::ConnectionMethods) -> butane::Result<()> { + fn save_many_to_many_sync(&mut self, #conn_arg_name: &impl butane::db::ConnectionMethods) -> butane::Result<()> { #many_save_sync Ok(()) } diff --git a/butane_core/src/db/adapter.rs b/butane_core/src/db/adapter.rs index 5e80a34c..3199fb61 100644 --- a/butane_core/src/db/adapter.rs +++ b/butane_core/src/db/adapter.rs @@ -260,9 +260,9 @@ impl Drop for AsyncAdapter { } #[async_trait(?Send)] -impl ConnectionMethods for AsyncAdapter +impl ConnectionMethodsAsync for AsyncAdapter where - T: sync::ConnectionMethods + ?Sized, + T: ConnectionMethods + ?Sized, { async fn execute(&self, sql: &str) -> Result<()> { self.invoke(|conn| conn.execute(sql)).await @@ -339,21 +339,20 @@ where } #[async_trait(?Send)] -impl BackendConnection for AsyncAdapter +impl BackendConnectionAsync for AsyncAdapter where - T: sync::BackendConnection, + T: BackendConnection, { - async fn transaction<'c>(&'c mut self) -> Result> { - let transaction_ptr: SyncSendPtrMut = self + async fn transaction<'c>(&'c mut self) -> Result> { + let transaction_ptr: SyncSendPtrMut = self .invoke_mut(|conn| { - let transaction: sync::Transaction = conn.transaction()?; - let transaction_ptr: *mut dyn sync::BackendTransaction = - Box::into_raw(transaction.trans); + let transaction: Transaction = conn.transaction()?; + let transaction_ptr: *mut dyn BackendTransaction = Box::into_raw(transaction.trans); Ok(SyncSendPtrMut::new(transaction_ptr)) }) .await?; let transaction_adapter = self.new_internal(transaction_ptr); - Ok(Transaction::new(Box::new(transaction_adapter))) + Ok(TransactionAsync::new(Box::new(transaction_adapter))) } fn backend(&self) -> Box { @@ -375,19 +374,19 @@ where impl AsyncAdapter where - T: sync::BackendConnection + 'static, + T: BackendConnection + 'static, { - pub fn into_connection(self) -> Connection { - Connection { + pub fn into_connection(self) -> ConnectionAsync { + ConnectionAsync { conn: Box::new(self), } } } #[async_trait(?Send)] -impl BackendTransaction<'c> for AsyncAdapter +impl BackendTransactionAsync<'c> for AsyncAdapter where - T: sync::BackendTransaction<'c> + ?Sized, + T: BackendTransaction<'c> + ?Sized, { async fn commit(&mut self) -> Result<()> { self.invoke_mut(|conn| conn.commit()).await @@ -395,7 +394,7 @@ where async fn rollback(&mut self) -> Result<()> { self.invoke_mut(|conn| conn.rollback()).await } - fn connection_methods(&self) -> &dyn ConnectionMethods { + fn connection_methods(&self) -> &dyn ConnectionMethodsAsync { self } } @@ -403,7 +402,7 @@ where /// Create an async connection using the synchronous `connect` method of `backend`. Use this when authoring /// a backend which doesn't natively support async. #[cfg(feature = "sqlite")] // todo expose this publicly for out-of-tree backends -pub async fn connect_async_via_sync(backend: &B, conn_str: &str) -> Result +pub async fn connect_async_via_sync(backend: &B, conn_str: &str) -> Result where B: Backend + Clone + 'static, { diff --git a/butane_core/src/db/connmethods.rs b/butane_core/src/db/connmethods.rs index e7aef2e3..4b748bc8 100644 --- a/butane_core/src/db/connmethods.rs +++ b/butane_core/src/db/connmethods.rs @@ -7,71 +7,61 @@ use std::ops::{Deref, DerefMut}; use crate::query::{BoolExpr, Expr, Order}; use crate::{Result, SqlType, SqlVal, SqlValRef}; -mod internal { - use super::*; - - /// Methods available on a database connection. Most users do not need - /// to call these methods directly and will instead use methods on - /// [DataObject][crate::DataObject] or the `query!` macro. This trait is - /// implemented by both database connections and transactions. - #[maybe_async_cfg::maybe(sync(), async())] - #[async_trait(?Send)] - pub trait ConnectionMethods { - async fn execute(&self, sql: &str) -> Result<()>; - async fn query<'c>( - &'c self, - table: &str, - columns: &[Column], - expr: Option, - limit: Option, - offset: Option, - sort: Option<&[Order]>, - ) -> Result>; - async fn insert_returning_pk( - &self, - table: &str, - columns: &[Column], - pkcol: &Column, - values: &[SqlValRef<'_>], - ) -> Result; - /// Like `insert_returning_pk` but with no return value - async fn insert_only( - &self, - table: &str, - columns: &[Column], - values: &[SqlValRef<'_>], - ) -> Result<()>; - /// Insert unless there's a conflict on the primary key column, in which case update - async fn insert_or_replace( - &self, - table: &str, - columns: &[Column], - pkcol: &Column, - values: &[SqlValRef<'_>], - ) -> Result<()>; - async fn update( - &self, - table: &str, - pkcol: Column, - pk: SqlValRef<'_>, - columns: &[Column], - values: &[SqlValRef<'_>], - ) -> Result<()>; - async fn delete(&self, table: &str, pkcol: &'static str, pk: SqlVal) -> Result<()> { - self.delete_where(table, BoolExpr::Eq(pkcol, Expr::Val(pk))) - .await?; - Ok(()) - } - async fn delete_where(&self, table: &str, expr: BoolExpr) -> Result; - /// Tests if a table exists in the database. - async fn has_table(&self, table: &str) -> Result; - } -} - -pub use internal::ConnectionMethodsAsync as ConnectionMethods; - -pub mod sync { - pub use super::internal::ConnectionMethodsSync as ConnectionMethods; +/// Methods available on a database connection. Most users do not need +/// to call these methods directly and will instead use methods on +/// [DataObject][crate::DataObject] or the `query!` macro. This trait is +/// implemented by both database connections and transactions. +#[maybe_async_cfg::maybe(sync(keep_self), async(self = "ConnectionMethodsAsync"))] +#[async_trait(?Send)] +pub trait ConnectionMethods { + async fn execute(&self, sql: &str) -> Result<()>; + async fn query<'c>( + &'c self, + table: &str, + columns: &[Column], + expr: Option, + limit: Option, + offset: Option, + sort: Option<&[Order]>, + ) -> Result>; + async fn insert_returning_pk( + &self, + table: &str, + columns: &[Column], + pkcol: &Column, + values: &[SqlValRef<'_>], + ) -> Result; + /// Like `insert_returning_pk` but with no return value + async fn insert_only( + &self, + table: &str, + columns: &[Column], + values: &[SqlValRef<'_>], + ) -> Result<()>; + /// Insert unless there's a conflict on the primary key column, in which case update + async fn insert_or_replace( + &self, + table: &str, + columns: &[Column], + pkcol: &Column, + values: &[SqlValRef<'_>], + ) -> Result<()>; + async fn update( + &self, + table: &str, + pkcol: Column, + pk: SqlValRef<'_>, + columns: &[Column], + values: &[SqlValRef<'_>], + ) -> Result<()>; + async fn delete(&self, table: &str, pkcol: &'static str, pk: SqlVal) -> Result<()> { + self.delete_where(table, BoolExpr::Eq(pkcol, Expr::Val(pk))) + .await?; + Ok(()) + } + async fn delete_where(&self, table: &str, expr: BoolExpr) -> Result; + /// Tests if a table exists in the database. + async fn has_table(&self, table: &str) -> Result; } /// Represents a database column. Most users do not need to use this diff --git a/butane_core/src/db/dummy.rs b/butane_core/src/db/dummy.rs index 569b10ae..00a5ac26 100644 --- a/butane_core/src/db/dummy.rs +++ b/butane_core/src/db/dummy.rs @@ -3,19 +3,10 @@ use async_trait::async_trait; -use super::connmethods::sync::ConnectionMethods as ConnectionMethodsSync; -use super::sync::BackendConnection as BackendConnectionSync; -use super::sync::Connection as ConnectionSync; -use super::sync::Transaction as TransactionSync; -use super::Transaction as TransactionAsync; -use super::{ - connmethods::Column, Backend, BackendConnection, Connection, ConnectionMethods, RawQueryResult, - Transaction, -}; +use super::*; use crate::migrations::adb; use crate::query::{BoolExpr, Order}; use crate::{Error, Result, SqlVal, SqlValRef}; -use BackendConnection as BackendConnectionAsync; #[derive(Clone, Debug)] struct DummyBackend {} @@ -28,10 +19,10 @@ impl Backend for DummyBackend { fn create_migration_sql(&self, current: &adb::ADB, ops: Vec) -> Result { Err(Error::PoisonedConnection) } - fn connect(&self, conn_str: &str) -> Result { + fn connect(&self, conn_str: &str) -> Result { Err(Error::PoisonedConnection) } - async fn connect_async(&self, conn_str: &str) -> Result { + async fn connect_async(&self, conn_str: &str) -> Result { Err(Error::PoisonedConnection) } } @@ -45,7 +36,7 @@ impl DummyConnection { } #[maybe_async_cfg::maybe( - idents(ConnectionMethods(sync = "ConnectionMethodsSync", async)), + idents(ConnectionMethods(sync = "ConnectionMethods", async = "ConnectionMethodsAsync")), keep_self, sync(), async() @@ -110,7 +101,15 @@ impl ConnectionMethods for DummyConnection { } } -#[maybe_async_cfg::maybe(idents(BackendConnection, Transaction), keep_self, sync(), async())] +#[maybe_async_cfg::maybe( + idents( + BackendConnection(sync = "BackendConnection"), + Transaction(sync = "Transaction") + ), + keep_self, + sync(), + async() +)] #[async_trait(?Send)] impl BackendConnection for DummyConnection { async fn transaction(&mut self) -> Result> { diff --git a/butane_core/src/db/macros.rs b/butane_core/src/db/macros.rs index dc1a6516..2d7bbbc6 100644 --- a/butane_core/src/db/macros.rs +++ b/butane_core/src/db/macros.rs @@ -2,8 +2,12 @@ macro_rules! connection_method_wrapper { ($ty:path) => { #[maybe_async_cfg::maybe( - idents(ConnectionMethods(sync = "ConnectionMethodsSync", async)), - sync(), + idents( + Connection(sync = "Connection"), + ConnectionMethods(sync = "ConnectionMethods"), + Transaction(sync = "Transaction") + ), + sync(keep_self), async() )] #[async_trait::async_trait(?Send)] diff --git a/butane_core/src/db/mod.rs b/butane_core/src/db/mod.rs index 50b1432b..b82ce44c 100644 --- a/butane_core/src/db/mod.rs +++ b/butane_core/src/db/mod.rs @@ -38,7 +38,8 @@ pub use sync_adapter::SyncAdapter; mod connmethods; pub use connmethods::{ - BackendRow, BackendRows, Column, ConnectionMethods, MapDeref, QueryResult, RawQueryResult, + BackendRow, BackendRows, Column, ConnectionMethods, ConnectionMethodsAsync, MapDeref, + QueryResult, RawQueryResult, }; mod helper; mod macros; @@ -55,9 +56,6 @@ pub mod r2; use crate::connection_method_wrapper; mod internal { - use super::*; - use connmethods::sync::ConnectionMethods as ConnectionMethodsSync; - #[maybe_async_cfg::maybe(sync())] pub trait AsyncRequiresSend {} #[maybe_async_cfg::maybe(idents(AsyncRequiresSend), sync())] @@ -67,420 +65,413 @@ mod internal { pub trait AsyncRequiresSend: Send {} #[maybe_async_cfg::maybe(idents(AsyncRequiresSend), async())] impl AsyncRequiresSend for T {} +} - /// Database connection. - #[maybe_async_cfg::maybe( - idents( - AsyncRequiresSend, - ConnectionMethods(sync = "ConnectionMethodsSync", async), - Transaction(sync = "TransactionSync", async), - ), - sync(), - async() - )] - #[async_trait(?Send)] - pub trait BackendConnection: ConnectionMethods + Debug + Send { - /// Begin a database transaction. The transaction object must be - /// used in place of this connection until it is committed or aborted. - async fn transaction(&mut self) -> Result>; - /// Retrieve the backend for this connection - fn backend(&self) -> Box; - /// Retrieve the backend name for this connection. - fn backend_name(&self) -> &'static str; - /// Tests if the connection has been closed. Backends which do not - /// support this check should return false. - fn is_closed(&self) -> bool; - } - - #[maybe_async_cfg::maybe( - idents(BackendConnection, Connection, Transaction), - keep_self, - sync(), - async() - )] - #[async_trait(?Send)] - impl BackendConnection for Box { - async fn transaction(&mut self) -> Result { - self.deref_mut().transaction().await - } - fn backend(&self) -> Box { - self.deref().backend() - } - fn backend_name(&self) -> &'static str { - self.deref().backend_name() - } - fn is_closed(&self) -> bool { - self.deref().is_closed() - } - } +/// Database connection. +#[maybe_async_cfg::maybe( + idents( + AsyncRequiresSend, + ConnectionMethods(sync = "ConnectionMethods", async = "ConnectionMethodsAsync"), + Transaction(sync = "Transaction", async = "TransactionAsync"), + ), + sync(self = "BackendConnection"), + async(self = "BackendConnectionAsync") +)] +#[async_trait(?Send)] +pub trait BackendConnection: ConnectionMethods + Debug + Send { + /// Begin a database transaction. The transaction object must be + /// used in place of this connection until it is committed or aborted. + async fn transaction(&mut self) -> Result>; + /// Retrieve the backend for this connection + fn backend(&self) -> Box; + /// Retrieve the backend name for this connection. + fn backend_name(&self) -> &'static str; + /// Tests if the connection has been closed. Backends which do not + /// support this check should return false. + fn is_closed(&self) -> bool; +} - #[maybe_async_cfg::maybe( - idents( - BackendConnection, - ConnectionMethods(sync = "ConnectionMethodsSync", async) - ), - keep_self, - sync(), - async() - )] - #[async_trait(?Send)] - impl ConnectionMethods for Box { - async fn execute(&self, sql: &str) -> Result<()> { - self.deref().execute(sql).await - } - async fn query<'c>( - &'c self, - table: &str, - columns: &[Column], - expr: Option, - limit: Option, - offset: Option, - sort: Option<&[Order]>, - ) -> Result> { - self.deref() - .query(table, columns, expr, limit, offset, sort) - .await - } - async fn insert_returning_pk( - &self, - table: &str, - columns: &[Column], - pkcol: &Column, - values: &[SqlValRef<'_>], - ) -> Result { - self.deref() - .insert_returning_pk(table, columns, pkcol, values) - .await - } - async fn insert_only( - &self, - table: &str, - columns: &[Column], - values: &[SqlValRef<'_>], - ) -> Result<()> { - self.deref().insert_only(table, columns, values).await - } - async fn insert_or_replace( - &self, - table: &str, - columns: &[Column], - pkcol: &Column, - values: &[SqlValRef<'_>], - ) -> Result<()> { - self.deref() - .insert_or_replace(table, columns, pkcol, values) - .await - } - async fn update( - &self, - table: &str, - pkcol: Column, - pk: SqlValRef<'_>, - columns: &[Column], - values: &[SqlValRef<'_>], - ) -> Result<()> { - self.deref().update(table, pkcol, pk, columns, values).await - } - async fn delete_where(&self, table: &str, expr: BoolExpr) -> Result { - self.deref().delete_where(table, expr).await - } - async fn has_table(&self, table: &str) -> Result { - self.deref().has_table(table).await - } +#[maybe_async_cfg::maybe( + idents( + BackendConnection(sync = "BackendConnection", async = "BackendConnectionAsync"), + Connection(sync = "Connection", async = "ConnectionAsync"), + Transaction(sync = "Transaction", async = "TransactionAsync") + ), + keep_self, + sync(), + async() +)] +#[async_trait(?Send)] +impl BackendConnection for Box { + async fn transaction(&mut self) -> Result { + self.deref_mut().transaction().await + } + fn backend(&self) -> Box { + self.deref().backend() + } + fn backend_name(&self) -> &'static str { + self.deref().backend_name() } + fn is_closed(&self) -> bool { + self.deref().is_closed() + } +} - /// Database connection. May be a connection to any type of database - /// as it is a boxed abstraction over a specific connection. - #[maybe_async_cfg::maybe(idents(BackendConnection), sync(), async())] - #[derive(Debug)] - pub struct Connection { - pub(super) conn: Box, +#[maybe_async_cfg::maybe( + idents( + BackendConnection(sync = "BackendConnection"), + ConnectionMethods(sync = "ConnectionMethods") + ), + keep_self, + sync(), + async() +)] +#[async_trait(?Send)] +impl ConnectionMethods for Box { + async fn execute(&self, sql: &str) -> Result<()> { + self.deref().execute(sql).await + } + async fn query<'c>( + &'c self, + table: &str, + columns: &[Column], + expr: Option, + limit: Option, + offset: Option, + sort: Option<&[Order]>, + ) -> Result> { + self.deref() + .query(table, columns, expr, limit, offset, sort) + .await + } + async fn insert_returning_pk( + &self, + table: &str, + columns: &[Column], + pkcol: &Column, + values: &[SqlValRef<'_>], + ) -> Result { + self.deref() + .insert_returning_pk(table, columns, pkcol, values) + .await + } + async fn insert_only( + &self, + table: &str, + columns: &[Column], + values: &[SqlValRef<'_>], + ) -> Result<()> { + self.deref().insert_only(table, columns, values).await } + async fn insert_or_replace( + &self, + table: &str, + columns: &[Column], + pkcol: &Column, + values: &[SqlValRef<'_>], + ) -> Result<()> { + self.deref() + .insert_or_replace(table, columns, pkcol, values) + .await + } + async fn update( + &self, + table: &str, + pkcol: Column, + pk: SqlValRef<'_>, + columns: &[Column], + values: &[SqlValRef<'_>], + ) -> Result<()> { + self.deref().update(table, pkcol, pk, columns, values).await + } + async fn delete_where(&self, table: &str, expr: BoolExpr) -> Result { + self.deref().delete_where(table, expr).await + } + async fn has_table(&self, table: &str) -> Result { + self.deref().has_table(table).await + } +} - #[maybe_async_cfg::maybe(idents(BackendConnection), sync(), async())] - impl Connection { - pub fn new(conn: Box) -> Self { - Self { conn } - } - pub async fn execute(&mut self, sql: impl AsRef) -> Result<()> { - self.conn.execute(sql.as_ref()).await - } - // For use with connection_method_wrapper macro - #[allow(clippy::unnecessary_wraps)] - fn wrapped_connection_methods(&self) -> Result<&dyn BackendConnection> { - Ok(self.conn.as_ref()) - } +/// Database connection. May be a connection to any type of database +/// as it is a boxed abstraction over a specific connection. +#[maybe_async_cfg::maybe( + idents(BackendConnection(sync = "BackendConnection")), + sync(keep_self), + async(self = "ConnectionAsync") +)] +#[derive(Debug)] +pub struct Connection { + pub(super) conn: Box, +} - #[maybe_async_cfg::only_if(key = "async")] - pub fn into_sync(self) -> Result { - Ok(SyncAdapter::new(self)?.into_connection()) - } +#[maybe_async_cfg::maybe( + idents(BackendConnection(sync = "BackendConnection")), + sync(keep_self), + async() +)] +impl Connection { + pub fn new(conn: Box) -> Self { + Self { conn } + } + pub async fn execute(&mut self, sql: impl AsRef) -> Result<()> { + self.conn.execute(sql.as_ref()).await + } + // For use with connection_method_wrapper macro + #[allow(clippy::unnecessary_wraps)] + fn wrapped_connection_methods(&self) -> Result<&dyn BackendConnection> { + Ok(self.conn.as_ref()) + } - #[maybe_async_cfg::only_if(key = "sync")] - pub fn into_async(self) -> Result { - Ok(adapter::AsyncAdapter::new(|| Ok(self))?.into_connection()) - } + #[maybe_async_cfg::only_if(key = "sync")] + pub fn into_async(self) -> Result { + Ok(adapter::AsyncAdapter::new(|| Ok(self))?.into_connection()) + } - /// Runs the provided function with a synchronous wrapper around this - /// asynchronous connection. - /// Because this relies on some (safe) memory gymnastics, - /// there is a small but nonzero risk that if tokio fails at - /// the wrong place the the connection will be poisoned -- all subsequent calls - /// to all methods will fail. - #[maybe_async_cfg::only_if(key = "async")] - pub async fn with_sync(&mut self, f: F) -> Result - where - F: FnOnce(&mut SyncAdapter) -> Result + Send + 'static, - T: Send + 'static, - { - let mut conn2 = Connection::new(Box::new(DummyConnection::new())); - std::mem::swap(&mut conn2, self); - let ret: Result<(Result, Connection)> = tokio::task::spawn_blocking(|| { - let mut sync_conn = SyncAdapter::new(conn2)?; - let f_ret = f(&mut sync_conn); - let async_conn = sync_conn.into_inner(); - Ok((f_ret, async_conn)) - }) - .await?; - match ret { - Ok((inner_ret, mut conn)) => { - std::mem::swap(&mut conn, self); - inner_ret - } - // Self is poisoned - Err(e) => Err(e), + /// Runs the provided function with a synchronous wrapper around this + /// asynchronous connection. + /// Because this relies on some (safe) memory gymnastics, + /// there is a small but nonzero risk that if tokio fails at + /// the wrong place the the connection will be poisoned -- all subsequent calls + /// to all methods will fail. + #[maybe_async_cfg::only_if(key = "async")] + pub async fn with_sync(&mut self, f: F) -> Result + where + F: FnOnce(&mut SyncAdapter) -> Result + Send + 'static, + T: Send + 'static, + { + let mut conn2 = Connection::new(Box::new(DummyConnection::new())); + std::mem::swap(&mut conn2, self); + let ret: Result<(Result, Connection)> = tokio::task::spawn_blocking(|| { + let mut sync_conn = SyncAdapter::new(conn2)?; + let f_ret = f(&mut sync_conn); + let async_conn = sync_conn.into_inner(); + Ok((f_ret, async_conn)) + }) + .await?; + match ret { + Ok((inner_ret, mut conn)) => { + std::mem::swap(&mut conn, self); + inner_ret } + // Self is poisoned + Err(e) => Err(e), } } +} - #[maybe_async_cfg::maybe(idents(BackendConnection, Connection, Transaction), sync(), async())] - #[async_trait(?Send)] - impl BackendConnection for Connection { - async fn transaction(&mut self) -> Result { - self.conn.transaction().await - } - fn backend(&self) -> Box { - self.conn.backend() - } - fn backend_name(&self) -> &'static str { - self.conn.backend_name() - } - fn is_closed(&self) -> bool { - self.conn.is_closed() - } - } - connection_method_wrapper!(Connection); - - #[maybe_async_cfg::maybe( - idents( - ConnectionMethods(sync = "ConnectionMethodsSync", async), - AsyncRequiresSend - ), - sync(), - async() - )] - #[async_trait(?Send)] - pub trait BackendTransaction<'c>: ConnectionMethods + AsyncRequiresSend + Debug { - /// Commit the transaction Unfortunately because we use this as a - /// trait object, we can't consume self. It should be understood - /// that no methods should be called after commit. This trait is - /// not public, and that behavior is enforced by Transaction - async fn commit(&mut self) -> Result<()>; - /// Roll back the transaction. Same comment about consuming self as above. - async fn rollback(&mut self) -> Result<()>; - - // Workaround for https://github.com/rust-lang/rfcs/issues/2765 - fn connection_methods(&self) -> &dyn ConnectionMethods; - } - - /// Database transaction. - /// - /// Begin a transaction using the `BackendConnection` - /// [`transaction`][crate::db::BackendConnection::transaction] method. - #[maybe_async_cfg::maybe( - idents(BackendTransaction( - sync = "BackendTransactionSync", - async = "BackendTransactionAsync" - )), - sync(), - async() - )] - #[derive(Debug)] - pub struct Transaction<'c> { - pub(super) trans: Box + 'c>, - } - - #[maybe_async_cfg::maybe( - idents( - BackendTransaction(sync = "BackendTransactionSync", async = "BackendTransactionAsync"), - ConnectionMethods(sync = "ConnectionMethodsSync", async) - ), - sync(), - async() - )] - impl<'c> Transaction<'c> { - // unused may occur if no backends are selected - #[allow(unused)] - pub(super) fn new(trans: Box + 'c>) -> Self { - Transaction { trans } - } - /// Commit the transaction - pub async fn commit(mut self) -> Result<()> { - self.trans.commit().await - } - /// Roll back the transaction. Equivalent to dropping it. - pub async fn rollback(mut self) -> Result<()> { - self.trans.deref_mut().rollback().await - } - // For use with connection_method_wrapper macro - #[allow(clippy::unnecessary_wraps)] - fn wrapped_connection_methods(&self) -> Result<&dyn ConnectionMethods> { - let a: &dyn BackendTransaction<'c> = self.trans.as_ref(); - Ok(a.connection_methods()) - } +impl ConnectionAsync { + pub fn into_sync(self) -> Result { + Ok(SyncAdapter::new(self)?.into_connection()) } +} - connection_method_wrapper!(Transaction<'_>); - - #[maybe_async_cfg::maybe( - idents( - BackendTransaction, - ConnectionMethods(sync = "ConnectionMethodsSync", async) - ), - sync(), - async() - )] - #[async_trait(?Send)] - impl<'c> BackendTransaction<'c> for Transaction<'c> { - async fn commit(&mut self) -> Result<()> { - self.trans.commit().await - } - async fn rollback(&mut self) -> Result<()> { - self.trans.deref_mut().rollback().await - } - fn connection_methods(&self) -> &dyn ConnectionMethods { - self - } +#[maybe_async_cfg::maybe( + idents( + BackendConnection(sync = "BackendConnection"), + Connection(sync = "Connection"), + Transaction(sync = "Transaction") + ), + sync(keep_self), + async() +)] +#[async_trait(?Send)] +impl BackendConnection for Connection { + async fn transaction(&mut self) -> Result { + self.conn.transaction().await } - - #[maybe_async_cfg::maybe( - idents( - BackendTransaction, - ConnectionMethods(sync = "ConnectionMethodsSync", async) - ), - keep_self, - sync(), - async() - )] - #[async_trait(?Send)] - impl<'c> BackendTransaction<'c> for Box + 'c> { - async fn commit(&mut self) -> Result<()> { - self.deref_mut().commit().await - } - async fn rollback(&mut self) -> Result<()> { - self.deref_mut().rollback().await - } - fn connection_methods(&self) -> &dyn ConnectionMethods { - self - } + fn backend(&self) -> Box { + self.conn.backend() } - - #[maybe_async_cfg::maybe( - idents( - BackendTransaction, - ConnectionMethods(sync = "ConnectionMethodsSync", async) - ), - keep_self, - sync(), - async() - )] - #[async_trait(?Send)] - impl<'bt> ConnectionMethods for Box + 'bt> { - async fn execute(&self, sql: &str) -> Result<()> { - self.deref().execute(sql).await - } - async fn query<'c>( - &'c self, - table: &str, - columns: &[Column], - expr: Option, - limit: Option, - offset: Option, - sort: Option<&[Order]>, - ) -> Result> { - self.deref() - .query(table, columns, expr, limit, offset, sort) - .await - } - async fn insert_returning_pk( - &self, - table: &str, - columns: &[Column], - pkcol: &Column, - values: &[SqlValRef<'_>], - ) -> Result { - self.deref() - .insert_returning_pk(table, columns, pkcol, values) - .await - } - async fn insert_only( - &self, - table: &str, - columns: &[Column], - values: &[SqlValRef<'_>], - ) -> Result<()> { - self.deref().insert_only(table, columns, values).await - } - async fn insert_or_replace( - &self, - table: &str, - columns: &[Column], - pkcol: &Column, - values: &[SqlValRef<'_>], - ) -> Result<()> { - self.deref() - .insert_or_replace(table, columns, pkcol, values) - .await - } - async fn update( - &self, - table: &str, - pkcol: Column, - pk: SqlValRef<'_>, - columns: &[Column], - values: &[SqlValRef<'_>], - ) -> Result<()> { - self.deref().update(table, pkcol, pk, columns, values).await - } - async fn delete_where(&self, table: &str, expr: BoolExpr) -> Result { - self.deref().delete_where(table, expr).await - } - async fn has_table(&self, table: &str) -> Result { - self.deref().has_table(table).await - } + fn backend_name(&self) -> &'static str { + self.conn.backend_name() + } + fn is_closed(&self) -> bool { + self.conn.is_closed() } } +connection_method_wrapper!(Connection); + +#[maybe_async_cfg::maybe( + idents(ConnectionMethods(sync = "ConnectionMethods"), AsyncRequiresSend), + sync(keep_self), + async() +)] +#[async_trait(?Send)] +pub trait BackendTransaction<'c>: ConnectionMethods + internal::AsyncRequiresSend + Debug { + /// Commit the transaction Unfortunately because we use this as a + /// trait object, we can't consume self. It should be understood + /// that no methods should be called after commit. This trait is + /// not public, and that behavior is enforced by Transaction + async fn commit(&mut self) -> Result<()>; + /// Roll back the transaction. Same comment about consuming self as above. + async fn rollback(&mut self) -> Result<()>; + + // Workaround for https://github.com/rust-lang/rfcs/issues/2765 + fn connection_methods(&self) -> &dyn ConnectionMethods; +} -pub use internal::BackendConnectionAsync as BackendConnection; -pub use internal::ConnectionAsync as Connection; -pub use internal::TransactionAsync as Transaction; +/// Database transaction. +/// +/// Begin a transaction using the `BackendConnection` +/// [`transaction`][crate::db::BackendConnection::transaction] method. +#[maybe_async_cfg::maybe( + idents(BackendTransaction(sync = "BackendTransaction")), + sync(self = "Transaction"), + async() +)] +#[derive(Debug)] +pub struct Transaction<'c> { + pub(super) trans: Box + 'c>, +} -// unused may occur dependending on backends being compiled -// todo include by feature instead -#[allow(unused)] -pub(crate) use internal::BackendTransactionAsync as BackendTransaction; +#[maybe_async_cfg::maybe( + idents( + BackendTransaction(sync = "BackendTransaction"), + ConnectionMethods(sync = "ConnectionMethods") + ), + sync(keep_self), + async() +)] +impl<'c> Transaction<'c> { + // unused may occur if no backends are selected + #[allow(unused)] + pub(super) fn new(trans: Box + 'c>) -> Self { + Transaction { trans } + } + /// Commit the transaction + pub async fn commit(mut self) -> Result<()> { + self.trans.commit().await + } + /// Roll back the transaction. Equivalent to dropping it. + pub async fn rollback(mut self) -> Result<()> { + self.trans.deref_mut().rollback().await + } + // For use with connection_method_wrapper macro + #[allow(clippy::unnecessary_wraps)] + fn wrapped_connection_methods(&self) -> Result<&dyn ConnectionMethods> { + let a: &dyn BackendTransaction<'c> = self.trans.as_ref(); + Ok(a.connection_methods()) + } +} -pub mod sync { - //! Synchronous (non-async versions of traits) +connection_method_wrapper!(Transaction<'_>); + +#[maybe_async_cfg::maybe( + idents( + BackendTransaction(sync = "BackendTransaction", async = "BackendTransactionAsync"), + ConnectionMethods(sync = "ConnectionMethods", async = "ConnectionMethodsAsync") + ), + sync(keep_self), + async() +)] +#[async_trait(?Send)] +impl<'c> BackendTransaction<'c> for Transaction<'c> { + async fn commit(&mut self) -> Result<()> { + self.trans.commit().await + } + async fn rollback(&mut self) -> Result<()> { + self.trans.deref_mut().rollback().await + } + fn connection_methods(&self) -> &dyn ConnectionMethods { + self + } +} - pub use super::connmethods::sync::ConnectionMethods; - pub use super::internal::BackendConnectionSync as BackendConnection; - pub use super::internal::ConnectionSync as Connection; - pub use super::internal::TransactionSync as Transaction; +#[maybe_async_cfg::maybe( + idents( + BackendTransaction(sync = "BackendTransaction"), + ConnectionMethods(sync = "ConnectionMethods") + ), + keep_self, + sync(), + async() +)] +#[async_trait(?Send)] +impl<'c> BackendTransaction<'c> for Box + 'c> { + async fn commit(&mut self) -> Result<()> { + self.deref_mut().commit().await + } + async fn rollback(&mut self) -> Result<()> { + self.deref_mut().rollback().await + } + fn connection_methods(&self) -> &dyn ConnectionMethods { + self + } +} - // unused may occur dependending on backends being compiled - #[allow(unused)] - pub(crate) use super::internal::BackendTransactionSync as BackendTransaction; +#[maybe_async_cfg::maybe( + idents( + BackendTransaction(sync = "BackendTransaction"), + ConnectionMethods(sync = "ConnectionMethods") + ), + keep_self, + sync(), + async() +)] +#[async_trait(?Send)] +impl<'bt> ConnectionMethods for Box + 'bt> { + async fn execute(&self, sql: &str) -> Result<()> { + self.deref().execute(sql).await + } + async fn query<'c>( + &'c self, + table: &str, + columns: &[Column], + expr: Option, + limit: Option, + offset: Option, + sort: Option<&[Order]>, + ) -> Result> { + self.deref() + .query(table, columns, expr, limit, offset, sort) + .await + } + async fn insert_returning_pk( + &self, + table: &str, + columns: &[Column], + pkcol: &Column, + values: &[SqlValRef<'_>], + ) -> Result { + self.deref() + .insert_returning_pk(table, columns, pkcol, values) + .await + } + async fn insert_only( + &self, + table: &str, + columns: &[Column], + values: &[SqlValRef<'_>], + ) -> Result<()> { + self.deref().insert_only(table, columns, values).await + } + async fn insert_or_replace( + &self, + table: &str, + columns: &[Column], + pkcol: &Column, + values: &[SqlValRef<'_>], + ) -> Result<()> { + self.deref() + .insert_or_replace(table, columns, pkcol, values) + .await + } + async fn update( + &self, + table: &str, + pkcol: Column, + pk: SqlValRef<'_>, + columns: &[Column], + values: &[SqlValRef<'_>], + ) -> Result<()> { + self.deref().update(table, pkcol, pk, columns, values).await + } + async fn delete_where(&self, table: &str, expr: BoolExpr) -> Result { + self.deref().delete_where(table, expr).await + } + async fn has_table(&self, table: &str) -> Result { + self.deref().has_table(table).await + } } /// Database backend. A boxed implementation can be returned by name via [get_backend][crate::db::get_backend]. @@ -488,8 +479,8 @@ pub mod sync { pub trait Backend: Send + Sync + DynClone { fn name(&self) -> &'static str; fn create_migration_sql(&self, current: &adb::ADB, ops: Vec) -> Result; - fn connect(&self, conn_str: &str) -> Result; - async fn connect_async(&self, conn_str: &str) -> Result; + fn connect(&self, conn_str: &str) -> Result; + async fn connect_async(&self, conn_str: &str) -> Result; } dyn_clone::clone_trait_object!(Backend); @@ -547,10 +538,10 @@ impl Backend for Box { fn create_migration_sql(&self, current: &adb::ADB, ops: Vec) -> Result { self.deref().create_migration_sql(current, ops) } - fn connect(&self, conn_str: &str) -> Result { + fn connect(&self, conn_str: &str) -> Result { self.deref().connect(conn_str) } - async fn connect_async(&self, conn_str: &str) -> Result { + async fn connect_async(&self, conn_str: &str) -> Result { self.deref().connect_async(conn_str).await } } @@ -568,7 +559,7 @@ pub fn get_backend(name: &str) -> Option> { /// Connect to a database. For non-boxed connections, see individual /// [`Backend`] implementations. -pub fn connect(spec: &ConnectionSpec) -> Result { +pub fn connect(spec: &ConnectionSpec) -> Result { let conn = get_backend(&spec.backend_name) .ok_or_else(|| Error::UnknownBackend(spec.backend_name.clone()))? .connect(&spec.conn_str)?; @@ -577,7 +568,7 @@ pub fn connect(spec: &ConnectionSpec) -> Result { /// Connect to a database. For non-boxed connections, see individual /// [`Backend`] implementations. -pub async fn connect_async(spec: &ConnectionSpec) -> Result { +pub async fn connect_async(spec: &ConnectionSpec) -> Result { get_backend(&spec.backend_name) .ok_or_else(|| Error::UnknownBackend(spec.backend_name.clone()))? .connect_async(&spec.conn_str) diff --git a/butane_core/src/db/pg.rs b/butane_core/src/db/pg.rs index 40d8c9d1..ec77be85 100644 --- a/butane_core/src/db/pg.rs +++ b/butane_core/src/db/pg.rs @@ -11,8 +11,10 @@ use super::connmethods::VecRows; use super::helper; use crate::custom::{SqlTypeCustom, SqlValRefCustom}; use crate::db::{ - Backend, BackendConnection, BackendRow, BackendTransaction, Column, Connection, - ConnectionMethods, RawQueryResult, SyncAdapter, Transaction, + Backend, BackendConnectionAsync as BackendConnection, BackendRow, + BackendTransactionAsync as BackendTransaction, Column, Connection, ConnectionAsync, + ConnectionMethodsAsync as ConnectionMethods, RawQueryResult, SyncAdapter, + TransactionAsync as Transaction, }; use crate::migrations::adb::{AColumn, ARef, ATable, Operation, TypeIdentifier, ADB}; use crate::query::{BoolExpr, Expr}; @@ -49,14 +51,14 @@ impl Backend for PgBackend { Ok(lines.join("\n")) } - fn connect(&self, path: &str) -> Result { + fn connect(&self, path: &str) -> Result { debug!("connecting via sync adapter"); let conn = SyncAdapter::new(self.clone())?.connect(path)?; Ok(conn) } - async fn connect_async(&self, path: &str) -> Result { - Ok(Connection { + async fn connect_async(&self, path: &str) -> Result { + Ok(ConnectionAsync { conn: Box::new(PgConnection::open(path).await?), }) } diff --git a/butane_core/src/db/r2.rs b/butane_core/src/db/r2.rs index 3e9bc486..617355de 100644 --- a/butane_core/src/db/r2.rs +++ b/butane_core/src/db/r2.rs @@ -2,7 +2,7 @@ pub use r2d2::ManageConnection; -use crate::db::sync::{BackendConnection, Connection, ConnectionMethods}; +use crate::db::{BackendConnection, Connection, ConnectionMethods}; use crate::db::{Column, ConnectionSpec, RawQueryResult}; use crate::{query::BoolExpr, query::Order, Result, SqlVal, SqlValRef}; diff --git a/butane_core/src/db/sqlite.rs b/butane_core/src/db/sqlite.rs index 5ef65248..b66fbd7d 100644 --- a/butane_core/src/db/sqlite.rs +++ b/butane_core/src/db/sqlite.rs @@ -8,10 +8,11 @@ use std::pin::Pin; #[cfg(feature = "log")] use std::sync::Once; -use super::sync::{ - BackendConnection, BackendTransaction, Connection, ConnectionMethods, Transaction, -}; use super::{helper, Backend, BackendRow, Column, RawQueryResult}; +use super::{ + BackendConnection, BackendTransaction, Connection, ConnectionAsync, ConnectionMethods, + Transaction, +}; use crate::db::connmethods::BackendRows; use crate::migrations::adb::ARef; use crate::migrations::adb::{AColumn, ATable, Operation, TypeIdentifier, ADB}; @@ -87,7 +88,7 @@ impl Backend for SQLiteBackend { conn: Box::new(self.connect(path)?), }) } - async fn connect_async(&self, path: &str) -> Result { + async fn connect_async(&self, path: &str) -> Result { super::adapter::connect_async_via_sync(self, path).await } } diff --git a/butane_core/src/db/sync_adapter.rs b/butane_core/src/db/sync_adapter.rs index e0637989..464b4ac2 100644 --- a/butane_core/src/db/sync_adapter.rs +++ b/butane_core/src/db/sync_adapter.rs @@ -1,4 +1,8 @@ -use crate::db::{Backend, RawQueryResult}; +use crate::db::{ + Backend, BackendConnection, BackendConnectionAsync, BackendTransaction, + BackendTransactionAsync, Connection, ConnectionAsync, ConnectionMethods, RawQueryResult, + Transaction, TransactionAsync, +}; use crate::migrations::adb; use crate::query::{BoolExpr, Order}; use crate::{debug, Column, Result, SqlVal, SqlValRef}; @@ -74,9 +78,9 @@ where } } -impl crate::db::sync::ConnectionMethods for SyncAdapter +impl crate::db::ConnectionMethods for SyncAdapter where - T: crate::db::ConnectionMethods, + T: crate::db::ConnectionMethodsAsync, { fn execute(&self, sql: &str) -> Result<()> { self.block_on(self.inner.execute(sql)) @@ -134,25 +138,23 @@ where } } -impl crate::db::sync::BackendConnection for SyncAdapter +impl BackendConnection for SyncAdapter where - T: crate::db::BackendConnection, + T: BackendConnectionAsync, { - fn transaction(&mut self) -> Result> { + fn transaction(&mut self) -> Result> { // We can't use chain because of the lifetimes and mutable borrows below, // so set up these runtime clones now. let runtime_handle = self.runtime_handle.clone(); let runtime = self._runtime.as_ref().map(|r| r.clone()); - let transaction: crate::db::Transaction = + let transaction: TransactionAsync = self.runtime_handle.block_on(self.inner.transaction())?; let transaction_adapter = SyncAdapter { runtime_handle, _runtime: runtime, inner: transaction.trans, }; - Ok(crate::db::sync::Transaction::new(Box::new( - transaction_adapter, - ))) + Ok(Transaction::new(Box::new(transaction_adapter))) } fn backend(&self) -> Box { self.inner.backend() @@ -167,16 +169,16 @@ where impl SyncAdapter where - T: crate::db::BackendConnection + 'static, + T: BackendConnectionAsync + 'static, { - pub fn into_connection(self) -> crate::db::sync::Connection { - crate::db::sync::Connection::new(Box::new(self)) + pub fn into_connection(self) -> Connection { + Connection::new(Box::new(self)) } } -impl<'c, T> crate::db::sync::BackendTransaction<'c> for SyncAdapter +impl<'c, T> BackendTransaction<'c> for SyncAdapter where - T: crate::db::BackendTransaction<'c>, + T: BackendTransactionAsync<'c>, { fn commit(&mut self) -> Result<()> { self.runtime_handle.block_on(self.inner.commit()) @@ -184,7 +186,7 @@ where fn rollback(&mut self) -> Result<()> { self.runtime_handle.block_on(self.inner.rollback()) } - fn connection_methods(&self) -> &dyn crate::db::sync::ConnectionMethods { + fn connection_methods(&self) -> &dyn ConnectionMethods { self } } @@ -200,14 +202,14 @@ where fn create_migration_sql(&self, current: &adb::ADB, ops: Vec) -> Result { self.inner.create_migration_sql(current, ops) } - fn connect(&self, conn_str: &str) -> Result { + fn connect(&self, conn_str: &str) -> Result { let conn_async = self.block_on(self.inner.connect_async(conn_str))?; - let conn = crate::db::sync::Connection { + let conn = Connection { conn: Box::new(self.chain(conn_async.conn)), }; Ok(conn) } - async fn connect_async(&self, conn_str: &str) -> Result { + async fn connect_async(&self, conn_str: &str) -> Result { self.inner.connect_async(conn_str).await } } diff --git a/butane_core/src/fkey.rs b/butane_core/src/fkey.rs index 4ec1c6d0..18e9b10a 100644 --- a/butane_core/src/fkey.rs +++ b/butane_core/src/fkey.rs @@ -86,7 +86,7 @@ impl ForeignKey { impl ForeignKey { /// Loads the value referred to by this foreign key from the /// database if necessary and returns a reference to it. - pub async fn load(&self, conn: &impl crate::ConnectionMethods) -> Result<&T> { + pub async fn load(&self, conn: &impl crate::ConnectionMethodsAsync) -> Result<&T> { use crate::DataObjectOpAsync; self.val .get_or_try_init(|| async { diff --git a/butane_core/src/lib.rs b/butane_core/src/lib.rs index f291f959..4eb3aca4 100644 --- a/butane_core/src/lib.rs +++ b/butane_core/src/lib.rs @@ -25,8 +25,7 @@ pub mod uuid; pub use autopk::AutoPk; use custom::SqlTypeCustom; -use db::sync::ConnectionMethods as ConnectionMethodsSync; -use db::{BackendRow, Column, ConnectionMethods}; +use db::{BackendRow, Column, ConnectionMethods, ConnectionMethodsAsync}; pub use query::Query; pub use sqlval::{AsPrimaryKey, FieldType, FromSql, PrimaryKeyType, SqlVal, SqlValRef, ToSql}; @@ -74,11 +73,14 @@ pub mod internal { /// Saves many-to-many relationships pointed to by fields on this model. /// Performed automatically by `save`. You do not need to call this directly. - async fn save_many_to_many_async(&mut self, conn: &impl ConnectionMethods) -> Result<()>; + async fn save_many_to_many_async( + &mut self, + conn: &impl ConnectionMethodsAsync, + ) -> Result<()>; /// Saves many-to-many relationships pointed to by fields on this model. /// Performed automatically by `save`. You do not need to call this directly. - fn save_many_to_many_sync(&mut self, conn: &impl ConnectionMethodsSync) -> Result<()>; + fn save_many_to_many_sync(&mut self, conn: &impl ConnectionMethods) -> Result<()>; /// Returns the Sql values of all columns except not any auto columns. /// Used internally. You are unlikely to need to call this directly. @@ -112,7 +114,7 @@ pub trait DataObject: DataResult + internal::DataObjectInternal + Sy #[allow(async_fn_in_trait)] // Implementation is intended to be through procmacro #[maybe_async_cfg::maybe( idents( - ConnectionMethods(sync = "ConnectionMethodsSync", async), + ConnectionMethods(sync = "ConnectionMethods"), save_many_to_many(sync = "save_many_to_many_sync", async = "save_many_to_many_async"), QueryOp, ), diff --git a/butane_core/src/many.rs b/butane_core/src/many.rs index a9be3388..184b13c4 100644 --- a/butane_core/src/many.rs +++ b/butane_core/src/many.rs @@ -1,7 +1,6 @@ //! Implementation of many-to-many relationships between models. #![deny(missing_docs)] -use crate::db::sync::ConnectionMethods as ConnectionMethodsSync; -use crate::db::{Column, ConnectionMethods}; +use crate::db::{Column, ConnectionMethods, ConnectionMethodsAsync}; use crate::query::{BoolExpr, Expr, OrderDirection, Query}; use crate::{sqlval::PrimaryKeyType, DataObject, Error, FieldType, Result, SqlType, SqlVal, ToSql}; use serde::{Deserialize, Serialize}; @@ -128,7 +127,7 @@ where } #[maybe_async_cfg::maybe( - idents(ConnectionMethods(sync = "ConnectionMethodsSync", async), QueryOp), + idents(ConnectionMethods(sync, async = "ConnectionMethodsAsync"), QueryOp), sync(), async() )] @@ -160,7 +159,7 @@ where /// database query if necessary and returns a reference to them. async fn load_query_async<'a, T>( many: &'a Many, - conn: &impl ConnectionMethods, + conn: &impl ConnectionMethodsAsync, query: Query, ) -> Result> where @@ -176,7 +175,7 @@ where /// database query if necessary and returns a reference to them. fn load_query_sync<'a, T>( many: &'a Many, - conn: &impl ConnectionMethodsSync, + conn: &impl ConnectionMethods, query: Query, ) -> Result> where @@ -194,7 +193,7 @@ where /// [`Many`] operations which require a `Connection` #[allow(async_fn_in_trait)] // Not intended to be implemented outside Butane #[maybe_async_cfg::maybe( - idents(ConnectionMethods(sync = "ConnectionMethodsSync", async),), + idents(ConnectionMethods(sync = "ConnectionMethods"),), sync(), async() )] @@ -227,7 +226,7 @@ pub trait ManyOp { #[maybe_async_cfg::maybe( idents( - ConnectionMethods(sync = "ConnectionMethodsSync", async), + ConnectionMethods(sync = "ConnectionMethods"), ManyOpInternal, ManyOp, load_query(sync = "load_query_sync", async = "load_query_async"), diff --git a/butane_core/src/migrations/migration.rs b/butane_core/src/migrations/migration.rs index a78d26a5..dcab12b2 100644 --- a/butane_core/src/migrations/migration.rs +++ b/butane_core/src/migrations/migration.rs @@ -3,9 +3,9 @@ use std::fmt::Debug; use super::adb::{ATable, DeferredSqlType, TypeKey, ADB}; use super::ButaneMigration; -use crate::db::sync::ConnectionMethods; +use crate::db::{BackendConnection, ConnectionMethods}; use crate::query::{BoolExpr, Expr}; -use crate::{db, sqlval::ToSql, DataObject, DataResult, Error, Result}; +use crate::{sqlval::ToSql, DataObject, DataResult, Error, Result}; /// Type representing a database migration. A migration describes how /// to bring the database from state A to state B. In general, the @@ -38,7 +38,7 @@ pub trait Migration: Debug + PartialEq { /// Apply the migration to a database connection. The connection /// must be for the same type of database as this and the database /// must be in the state of the migration prior to this one - fn apply(&self, conn: &mut impl db::sync::BackendConnection) -> Result<()> { + fn apply(&self, conn: &mut impl BackendConnection) -> Result<()> { let backend_name = conn.backend_name(); let tx = conn.transaction()?; let sql = self @@ -53,7 +53,7 @@ pub trait Migration: Debug + PartialEq { /// work. Use carefully -- the caller must ensure that the /// database schema already matches that expected by this /// migration. - fn mark_applied(&self, conn: &impl db::sync::ConnectionMethods) -> Result<()> { + fn mark_applied(&self, conn: &impl ConnectionMethods) -> Result<()> { conn.insert_only( ButaneMigration::TABLE, ButaneMigration::COLUMNS, @@ -65,7 +65,7 @@ pub trait Migration: Debug + PartialEq { /// connection. The connection must be for the same type of /// database as this and this must be the latest migration applied /// to the database. - fn downgrade(&self, conn: &mut impl db::sync::BackendConnection) -> Result<()> { + fn downgrade(&self, conn: &mut impl BackendConnection) -> Result<()> { let backend_name = conn.backend_name(); let tx = conn.transaction()?; let sql = self diff --git a/butane_core/src/migrations/mod.rs b/butane_core/src/migrations/mod.rs index 89e540d8..c6311716 100644 --- a/butane_core/src/migrations/mod.rs +++ b/butane_core/src/migrations/mod.rs @@ -8,7 +8,10 @@ use std::path::Path; use fallible_iterator::FallibleIterator; use nonempty::NonEmpty; -use crate::db::{sync::BackendConnection, Backend, BackendRows, Column}; +use crate::db::{ + Backend, BackendConnection, BackendRows, Column, ConnectionAsync, ConnectionMethods, + ConnectionMethodsAsync, +}; use crate::sqlval::{FromSql, SqlValRef, ToSql}; use crate::{db, query, DataObject, DataResult, Error, PrimaryKeyType, Result, SqlType}; @@ -72,10 +75,7 @@ pub trait Migrations: Clone { } /// Get migrations which have not yet been applied to the database - fn unapplied_migrations( - &self, - conn: &impl crate::db::sync::ConnectionMethods, - ) -> Result> { + fn unapplied_migrations(&self, conn: &impl ConnectionMethods) -> Result> { match self.last_applied_migration(conn)? { None => self.all_migrations(), Some(m) => self.migrations_since(&m), @@ -84,10 +84,7 @@ pub trait Migrations: Clone { /// Get the last migration that has been applied to the database or None /// if no migrations have been applied - fn last_applied_migration( - &self, - conn: &impl crate::db::sync::ConnectionMethods, - ) -> Result> { + fn last_applied_migration(&self, conn: &impl ConnectionMethods) -> Result> { if !conn.has_table(ButaneMigration::TABLE)? { return Ok(None); } @@ -128,7 +125,7 @@ pub trait Migrations: Clone { } /// Migrate connection forward. - async fn migrate_async(&self, conn: &mut crate::db::Connection) -> Result<()> + async fn migrate_async(&self, conn: &mut ConnectionAsync) -> Result<()> where Self: Send + 'static, { @@ -159,7 +156,7 @@ pub trait Migrations: Clone { } /// Remove all applied migrations. - async fn unmigrate_async(&self, conn: &mut crate::db::Connection) -> Result<()> + async fn unmigrate_async(&self, conn: &mut ConnectionAsync) -> Result<()> where Self: Send + 'static, { @@ -196,7 +193,7 @@ where /// any storage backing it) and deleting the record of their /// existence/application from the database. The database schema /// is not modified, nor is any other data removed. Use carefully. - fn clear_migrations(&mut self, conn: &impl crate::db::sync::ConnectionMethods) -> Result<()> { + fn clear_migrations(&mut self, conn: &impl ConnectionMethods) -> Result<()> { self.delete_migrations()?; conn.delete_where(ButaneMigration::TABLE, query::BoolExpr::True)?; Ok(()) @@ -384,16 +381,10 @@ impl crate::internal::DataObjectInternal for ButaneMigration { } values } - async fn save_many_to_many_async( - &mut self, - _conn: &impl crate::db::ConnectionMethods, - ) -> Result<()> { + async fn save_many_to_many_async(&mut self, _conn: &impl ConnectionMethodsAsync) -> Result<()> { Ok(()) // no-op } - fn save_many_to_many_sync( - &mut self, - _conn: &impl crate::db::sync::ConnectionMethods, - ) -> Result<()> { + fn save_many_to_many_sync(&mut self, _conn: &impl ConnectionMethods) -> Result<()> { Ok(()) // no-op } } diff --git a/butane_core/src/query/mod.rs b/butane_core/src/query/mod.rs index 28b2404f..1c230ad1 100644 --- a/butane_core/src/query/mod.rs +++ b/butane_core/src/query/mod.rs @@ -9,8 +9,7 @@ use std::marker::PhantomData; use fallible_iterator::FallibleIterator; -use crate::db::sync::ConnectionMethods as ConnectionMethodsSync; -use crate::db::{BackendRows, ConnectionMethods, QueryResult}; +use crate::db::{BackendRows, ConnectionMethods, ConnectionMethodsAsync, QueryResult}; use crate::{DataResult, Result, SqlVal}; mod fieldexpr; @@ -191,7 +190,7 @@ mod private { /// Internal QueryOp helpers #[allow(async_fn_in_trait)] // Not truly a public trait #[maybe_async_cfg::maybe( - idents(ConnectionMethods(sync = "ConnectionMethodsSync", async)), + idents(ConnectionMethods(sync = "ConnectionMethods")), sync(), async() )] @@ -203,10 +202,7 @@ mod private { ) -> Result>; } #[maybe_async_cfg::maybe( - idents( - ConnectionMethods(sync = "ConnectionMethodsSync", async), - QueryOpInternal - ), + idents(ConnectionMethods(sync = "ConnectionMethods"), QueryOpInternal), keep_self, sync(), async() @@ -240,10 +236,7 @@ use private::QueryOpInternalSync; /// [`Query`] operations which require a `Connection` #[allow(async_fn_in_trait)] // Not intended to be implemented outside Butane #[maybe_async_cfg::maybe( - idents( - ConnectionMethods(sync = "ConnectionMethodsSync", async), - QueryOpInternal - ), + idents(ConnectionMethods(sync = "ConnectionMethods"), QueryOpInternal), sync(), async() )] @@ -260,7 +253,7 @@ pub trait QueryOp: QueryOpInternal { #[maybe_async_cfg::maybe( idents( - ConnectionMethods(sync = "ConnectionMethodsSync", async), + ConnectionMethods(sync = "ConnectionMethods"), QueryOp, QueryOpInternal ), diff --git a/butane_core/tests/connection.rs b/butane_core/tests/connection.rs index 3db7e18f..d724bbde 100644 --- a/butane_core/tests/connection.rs +++ b/butane_core/tests/connection.rs @@ -1,7 +1,7 @@ -use butane_core::db::{connect_async, BackendConnection, Connection, ConnectionSpec}; +use butane_core::db::{connect_async, BackendConnectionAsync, ConnectionAsync, ConnectionSpec}; use butane_test_helper::*; -async fn connection_not_closed(conn: Connection) { +async fn connection_not_closed(conn: ConnectionAsync) { assert!(!conn.is_closed()); } testall_no_migrate!(connection_not_closed); @@ -61,7 +61,7 @@ async fn unreachable_pg_connection() { } } -async fn debug_connection(conn: Connection) { +async fn debug_connection(conn: ConnectionAsync) { let backend_name = conn.backend_name(); let debug_str = format!("{:?}", conn); diff --git a/butane_core/tests/migration.rs b/butane_core/tests/migration.rs index 7a1d7299..37365643 100644 --- a/butane_core/tests/migration.rs +++ b/butane_core/tests/migration.rs @@ -1,4 +1,4 @@ -use butane_core::db::{BackendConnection, Connection, ConnectionMethods}; +use butane_core::db::{BackendConnectionAsync, ConnectionAsync, ConnectionMethodsAsync}; use butane_core::migrations::adb::*; use butane_core::SqlType; use butane_test_helper::*; @@ -211,7 +211,7 @@ fn add_table_fkey() { /// This is the same as test "add_table_fkey", except that it /// runs the DDL on a database, and then deletes the column. -async fn add_table_fkey_delete_column(conn: Connection) { +async fn add_table_fkey_delete_column(conn: ConnectionAsync) { let known_int_type = DeferredSqlType::KnownId(TypeIdentifier::Ty(SqlType::Int)); let old = ADB::default(); @@ -298,7 +298,7 @@ testall_no_migrate!(add_table_fkey_delete_column); /// This is the same as test "add_table_fkey", except that it /// intentionally links a column on table a to table b, and /// it runs the DDL on a database. -async fn add_table_fkey_back_reference(conn: Connection) { +async fn add_table_fkey_back_reference(conn: ConnectionAsync) { let known_int_type = DeferredSqlType::KnownId(TypeIdentifier::Ty(SqlType::Int)); let old = ADB::default(); @@ -388,7 +388,7 @@ testall_no_migrate!(add_table_fkey_back_reference); /// This is the same as test "add_table_fkey", except that it /// creates a table with multiple fkey constraints. -async fn add_table_fkey_multiple(conn: Connection) { +async fn add_table_fkey_multiple(conn: ConnectionAsync) { let known_int_type = DeferredSqlType::KnownId(TypeIdentifier::Ty(SqlType::Int)); let old = ADB::default(); diff --git a/butane_core/tests/transactions.rs b/butane_core/tests/transactions.rs index b05ecbd1..670afcb3 100644 --- a/butane_core/tests/transactions.rs +++ b/butane_core/tests/transactions.rs @@ -1,7 +1,7 @@ -use butane_core::db::{BackendConnection, Connection}; +use butane_core::db::{BackendConnectionAsync, ConnectionAsync}; use butane_test_helper::*; -async fn commit_empty_transaction(mut conn: Connection) { +async fn commit_empty_transaction(mut conn: ConnectionAsync) { assert!(!conn.is_closed()); let tr = conn.transaction().await.unwrap(); @@ -12,7 +12,7 @@ async fn commit_empty_transaction(mut conn: Connection) { } testall_no_migrate!(commit_empty_transaction); -async fn rollback_empty_transaction(mut conn: Connection) { +async fn rollback_empty_transaction(mut conn: ConnectionAsync) { let tr = conn.transaction().await.unwrap(); assert!(tr.rollback().await.is_ok()); @@ -21,7 +21,7 @@ async fn rollback_empty_transaction(mut conn: Connection) { } testall_no_migrate!(rollback_empty_transaction); -async fn debug_transaction_before_consuming(mut conn: Connection) { +async fn debug_transaction_before_consuming(mut conn: ConnectionAsync) { let backend_name = conn.backend_name(); let tr = conn.transaction().await.unwrap(); diff --git a/butane_test_helper/src/lib.rs b/butane_test_helper/src/lib.rs index d6c57bd9..ac280e33 100644 --- a/butane_test_helper/src/lib.rs +++ b/butane_test_helper/src/lib.rs @@ -2,11 +2,9 @@ //! Macros depend on [`butane_core`], `env_logger` and [`log`]. #![deny(missing_docs)] -use butane_core::db::sync::BackendConnection as BackendConnectionSync; -use butane_core::db::sync::Connection as ConnectionSync; use butane_core::db::{ - connect, connect_async, get_backend, pg, sqlite, Backend, BackendConnection, Connection, - ConnectionSpec, + connect, connect_async, get_backend, pg, sqlite, Backend, BackendConnection, + BackendConnectionAsync, Connection, ConnectionAsync, ConnectionSpec, }; use butane_core::migrations::{self, MemMigrations, Migration, Migrations, MigrationsMut}; use once_cell::sync::Lazy; @@ -21,7 +19,7 @@ use block_id::{Alphabet, BlockId}; use uuid::Uuid; /// Create a postgres [`Connection`]. -pub fn pg_connection() -> (ConnectionSync, PgSetupData) { +pub fn pg_connection() -> (Connection, PgSetupData) { let backend = get_backend(pg::BACKEND_NAME).unwrap(); let data = pg_setup_sync(); (backend.connect(&pg_connstr(&data)).unwrap(), data) @@ -168,7 +166,7 @@ pub fn pg_setup_sync() -> PgSetupData { log::info!("new db is `{}`", &new_dbname); let mut conn = connect(&ConnectionSpec::new("pg", &connstr)).unwrap(); - log::debug!("closed is {}", BackendConnectionSync::is_closed(&conn)); + log::debug!("closed is {}", BackendConnection::is_closed(&conn)); conn.execute(format!("CREATE DATABASE {new_dbname};")) .unwrap(); @@ -197,7 +195,10 @@ pub async fn pg_setup() -> PgSetupData { let mut conn = connect_async(&ConnectionSpec::new("pg", &connstr)) .await .unwrap(); - log::debug!("[async]closed is {}", BackendConnection::is_closed(&conn)); + log::debug!( + "[async]closed is {}", + BackendConnectionAsync::is_closed(&conn) + ); conn.execute(format!("CREATE DATABASE {new_dbname};")) .await .unwrap(); @@ -246,21 +247,21 @@ pub fn create_current_migrations(backend: Box) -> MemMigrations { } /// Populate the database schema. -pub async fn setup_db_async(conn: &mut Connection) { +pub async fn setup_db_async(conn: &mut ConnectionAsync) { let mem_migrations = create_current_migrations(conn.backend()); log::info!("created current migration"); mem_migrations.migrate_async(conn).await.unwrap(); } /// Populate the database schema. -pub fn setup_db(conn: &mut ConnectionSync) { +pub fn setup_db(conn: &mut Connection) { let mem_migrations = create_current_migrations(conn.backend()); log::info!("created current migration"); mem_migrations.migrate(conn).unwrap(); } /// Create a sqlite [`Connection`]. -pub fn sqlite_connection() -> ConnectionSync { +pub fn sqlite_connection() -> Connection { let backend = get_backend(sqlite::BACKEND_NAME).unwrap(); backend.connect(":memory:").unwrap() } diff --git a/example/src/main.rs b/example/src/main.rs index f1e4a3db..e9a89db1 100644 --- a/example/src/main.rs +++ b/example/src/main.rs @@ -1,5 +1,5 @@ //! Simple example with all code in a single file. -use butane::db::{Connection, ConnectionSpec}; +use butane::db::{ConnectionAsync, ConnectionSpec}; use butane::prelude_async::*; use butane::{find, model, query, AutoPk, Error, ForeignKey, Many}; @@ -90,7 +90,7 @@ async fn query() -> Result<()> { Ok(()) } -async fn establish_connection() -> Result { +async fn establish_connection() -> Result { let mut cwd = std::env::current_dir()?; cwd.push(".butane"); let spec = ConnectionSpec::load(cwd)?; diff --git a/examples/getting_started/src/lib.rs b/examples/getting_started/src/lib.rs index 251476d7..119c733c 100644 --- a/examples/getting_started/src/lib.rs +++ b/examples/getting_started/src/lib.rs @@ -5,7 +5,7 @@ pub mod butane_migrations; pub mod models; -use butane::db::sync::Connection; +use butane::db::Connection; use butane::db::ConnectionSpec; use butane::migrations::Migrations; use butane::prelude::*; diff --git a/examples/newtype/src/lib.rs b/examples/newtype/src/lib.rs index 0914c5a6..709a3ccf 100644 --- a/examples/newtype/src/lib.rs +++ b/examples/newtype/src/lib.rs @@ -5,7 +5,7 @@ pub mod butane_migrations; pub mod models; -use butane::db::{Connection, ConnectionSpec}; +use butane::db::{ConnectionAsync, ConnectionSpec}; use butane::migrations::Migrations; use butane::prelude_async::*; use models::{Blog, Post}; @@ -13,7 +13,7 @@ use models::{Blog, Post}; // todo this example doesn't necessarily need to be async /// Load a [Connection]. -pub async fn establish_connection() -> Connection { +pub async fn establish_connection() -> ConnectionAsync { let mut connection = butane::db::connect_async(&ConnectionSpec::load(".butane/connection.json").unwrap()) .await @@ -24,20 +24,20 @@ pub async fn establish_connection() -> Connection { } /// Create a [Blog]. -pub async fn create_blog(conn: &Connection, name: impl Into) -> Blog { +pub async fn create_blog(conn: &ConnectionAsync, name: impl Into) -> Blog { let mut blog = Blog::new(name).unwrap(); blog.save(conn).await.unwrap(); blog } /// Create a [Post]. -pub async fn create_post(conn: &Connection, blog: &Blog, title: String, body: String) -> Post { +pub async fn create_post(conn: &ConnectionAsync, blog: &Blog, title: String, body: String) -> Post { let mut new_post = Post::new(blog, title, body); new_post.save(conn).await.unwrap(); new_post } /// Fetch the first existing [Blog] if one exists. -pub async fn existing_blog(conn: &Connection) -> Option { +pub async fn existing_blog(conn: &ConnectionAsync) -> Option { Blog::query().load_first(conn).await.unwrap() } diff --git a/examples/newtype/tests/unmigrate.rs b/examples/newtype/tests/unmigrate.rs index 798f7691..47f51769 100644 --- a/examples/newtype/tests/unmigrate.rs +++ b/examples/newtype/tests/unmigrate.rs @@ -1,11 +1,11 @@ -use butane::db::{BackendConnection, Connection}; +use butane::db::{BackendConnectionAsync, ConnectionAsync}; use butane::migrations::Migrations; use butane::DataObjectOpAsync; use butane_test_helper::*; use newtype::models::{Blog, Post, Tags}; -async fn insert_data(connection: &Connection) { +async fn insert_data(connection: &ConnectionAsync) { if connection.backend_name() == "sqlite" { // https://github.com/Electron100/butane/issues/226 return; @@ -27,7 +27,7 @@ async fn insert_data(connection: &Connection) { post.save(connection).await.unwrap(); } -async fn migrate_and_unmigrate(mut connection: Connection) { +async fn migrate_and_unmigrate(mut connection: ConnectionAsync) { // Migrate forward. let base_dir = std::path::PathBuf::from(".butane"); let migrations = butane_cli::get_migrations(&base_dir).unwrap(); From ce6ea874c69c365ef031629c241f889aec2071a6 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Wed, 28 Aug 2024 20:52:08 -0400 Subject: [PATCH 46/78] Update checklist --- async_checklist.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/async_checklist.md b/async_checklist.md index 7045157f..0c966c91 100644 --- a/async_checklist.md +++ b/async_checklist.md @@ -1,4 +1,4 @@ -* [ ] Clean up pattern for sync/async variants. Inconsistent between suffix and module +* [x] Clean up pattern for sync/async variants. Inconsistent between suffix and module * [ ] Tests should run against sync and async * [ ] Establish soundness for unsafe sections of AsyncAdapter * [ ] Consider publishing `AsyncAdapter` into its own crate From a811e2f87f93098de27df0b5265afc812316d183 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Thu, 5 Sep 2024 22:25:09 -0400 Subject: [PATCH 47/78] Support sync in `find` and `ForeignKey` again --- butane/src/lib.rs | 11 ++++++++ butane_core/src/fkey.rs | 62 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/butane/src/lib.rs b/butane/src/lib.rs index 4ba3c5b2..de9e1f0c 100644 --- a/butane/src/lib.rs +++ b/butane/src/lib.rs @@ -164,6 +164,15 @@ macro_rules! colname { /// [`NoSuchObject`]: crate::Error::NoSuchObject #[macro_export] macro_rules! find { + ($dbobj:ident, $filter:expr, $conn:expr) => { + butane::query::QueryOpSync::load(butane::query!($dbobj, $filter).limit(1), $conn) + .and_then(|mut results| results.pop().ok_or(butane::Error::NoSuchObject)) + }; +} + +/// Like [`find`], but for async +#[macro_export] +macro_rules! find_async { ($dbobj:ident, $filter:expr, $conn:expr) => { // todo sync version butane::query::QueryOpAsync::load(butane::query!($dbobj, $filter).limit(1), $conn) @@ -188,6 +197,7 @@ pub mod prelude { pub use super::prelude_common::*; pub use butane_core::db::BackendConnection; + pub use butane_core::fkey::ForeignKeyOpSync; pub use butane_core::many::ManyOpSync; pub use butane_core::query::QueryOpSync; pub use butane_core::DataObjectOpSync; @@ -200,6 +210,7 @@ pub mod prelude_async { pub use super::prelude_common::*; pub use butane_core::db::BackendConnectionAsync; + pub use butane_core::fkey::ForeignKeyOpAsync; pub use butane_core::many::ManyOpAsync; pub use butane_core::query::QueryOpAsync; pub use butane_core::DataObjectOpAsync; diff --git a/butane_core/src/fkey.rs b/butane_core/src/fkey.rs index 18e9b10a..819e336d 100644 --- a/butane_core/src/fkey.rs +++ b/butane_core/src/fkey.rs @@ -9,13 +9,16 @@ use fake::{Dummy, Faker}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use crate::{ - AsPrimaryKey, DataObject, Error, FieldType, FromSql, Result, SqlType, SqlVal, SqlValRef, ToSql, + AsPrimaryKey, ConnectionMethods, ConnectionMethodsAsync, DataObject, Error, FieldType, FromSql, + Result, SqlType, SqlVal, SqlValRef, ToSql, }; /// Used to implement a relationship between models. /// /// Initialize using `From` or `from_pk` /// +/// See [`ForeignKeyOpSync`] and [`ForeignKeyOpAsync`] for operations requiring a live database connection. +/// /// # Examples /// ```ignore /// #[model] @@ -82,11 +85,26 @@ impl ForeignKey { } } -//todo support sync load with ForeignKey too -impl ForeignKey { +/// [`Many`] operations which require a `Connection` +#[allow(async_fn_in_trait)] // Not intended to be implemented outside Butane +#[maybe_async_cfg::maybe( + idents(ConnectionMethods(sync = "ConnectionMethods"),), + sync(), + async() +)] +pub trait ForeignKeyOp { /// Loads the value referred to by this foreign key from the /// database if necessary and returns a reference to it. - pub async fn load(&self, conn: &impl crate::ConnectionMethodsAsync) -> Result<&T> { + async fn load<'a>(&'a self, conn: &impl ConnectionMethods) -> Result<&T> + where + T: 'a; +} + +impl ForeignKeyOpAsync for ForeignKey { + async fn load<'a>(&'a self, conn: &impl ConnectionMethodsAsync) -> Result<&T> + where + T: 'a, + { use crate::DataObjectOpAsync; self.val .get_or_try_init(|| async { @@ -100,6 +118,20 @@ impl ForeignKey { } } +impl ForeignKeyOpSync for ForeignKey { + fn load<'a>(&'a self, conn: &impl ConnectionMethods) -> Result<&T> + where + T: 'a, + { + use crate::DataObjectOpSync; + get_or_try_init_tokio_once_cell_sync(&self.val, || { + let pk = self.valpk.get().unwrap(); + T::get(conn, T::PKType::from_sql_ref(pk.as_ref())?).map(Box::new) + }) + .map(|v| v.as_ref()) + } +} + impl From for ForeignKey { fn from(obj: T) -> Self { let ret = Self::new_raw(); @@ -199,6 +231,28 @@ where } } +/// Wrapper around tokio's OnceCell get_or_try_init method +/// This is a sync version which provides the same semantics with a spinlock, as simultaneous initialization should be very rare. +fn get_or_try_init_tokio_once_cell_sync<'a, T, F>(cell: &'a OnceCell, f: F) -> Result<&'a T> +where + F: Fn() -> Result, +{ + match cell.get() { + Some(val) => Ok(val), + None => { + loop { + match cell.set(f()?) { + Ok(()) => break, + Err(tokio::sync::SetError::AlreadyInitializedError(_)) => break, + Err(tokio::sync::SetError::InitializingError(_)) => continue, // spinlock + } + } + // Error should be impossible here, we should have already ensured init (or returned error). + cell.get().ok_or(Error::NotInitialized) + } + } +} + #[cfg(feature = "fake")] /// Fake data support is currently limited to empty ForeignKey relationships. impl Dummy for ForeignKey { From bf4b17177ddb7f73dfd9df5ab3fae0d689f6f20f Mon Sep 17 00:00:00 2001 From: James Oakley Date: Fri, 6 Sep 2024 08:31:13 -0400 Subject: [PATCH 48/78] Fix a doctest --- butane/src/lib.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/butane/src/lib.rs b/butane/src/lib.rs index de9e1f0c..f34b13be 100644 --- a/butane/src/lib.rs +++ b/butane/src/lib.rs @@ -153,10 +153,8 @@ macro_rules! colname { /// nationality: String /// } /// -/// # tokio_test::block_on(async { -/// let conn = butane::db::connect_async(&ConnectionSpec::new("sqlite", "foo.db")).await.unwrap(); +/// let conn = butane::db::connect(&ConnectionSpec::new("sqlite", "foo.db")).unwrap(); /// let alice: Result = find!(Contestant, name == "Alice", &conn); -/// # }) ///``` /// /// [`filter]: crate::filter From d187e024266d41980df51ed13fd6964c25d6aa6c Mon Sep 17 00:00:00 2001 From: James Oakley Date: Fri, 6 Sep 2024 08:31:44 -0400 Subject: [PATCH 49/78] Standard once cell handling between fkey and many --- butane_core/src/fkey.rs | 24 +----------------------- butane_core/src/lib.rs | 4 +++- butane_core/src/many.rs | 12 +++++------- butane_core/src/sync.rs | 24 ++++++++++++++++++++++++ 4 files changed, 33 insertions(+), 31 deletions(-) create mode 100644 butane_core/src/sync.rs diff --git a/butane_core/src/fkey.rs b/butane_core/src/fkey.rs index 819e336d..474ee3f9 100644 --- a/butane_core/src/fkey.rs +++ b/butane_core/src/fkey.rs @@ -124,7 +124,7 @@ impl ForeignKeyOpSync for ForeignKey { T: 'a, { use crate::DataObjectOpSync; - get_or_try_init_tokio_once_cell_sync(&self.val, || { + crate::sync::get_or_try_init_tokio_once_cell_sync(&self.val, || { let pk = self.valpk.get().unwrap(); T::get(conn, T::PKType::from_sql_ref(pk.as_ref())?).map(Box::new) }) @@ -231,28 +231,6 @@ where } } -/// Wrapper around tokio's OnceCell get_or_try_init method -/// This is a sync version which provides the same semantics with a spinlock, as simultaneous initialization should be very rare. -fn get_or_try_init_tokio_once_cell_sync<'a, T, F>(cell: &'a OnceCell, f: F) -> Result<&'a T> -where - F: Fn() -> Result, -{ - match cell.get() { - Some(val) => Ok(val), - None => { - loop { - match cell.set(f()?) { - Ok(()) => break, - Err(tokio::sync::SetError::AlreadyInitializedError(_)) => break, - Err(tokio::sync::SetError::InitializingError(_)) => continue, // spinlock - } - } - // Error should be impossible here, we should have already ensured init (or returned error). - cell.get().ok_or(Error::NotInitialized) - } - } -} - #[cfg(feature = "fake")] /// Fake data support is currently limited to empty ForeignKey relationships. impl Dummy for ForeignKey { diff --git a/butane_core/src/lib.rs b/butane_core/src/lib.rs index 4eb3aca4..b9d1df45 100644 --- a/butane_core/src/lib.rs +++ b/butane_core/src/lib.rs @@ -19,10 +19,12 @@ pub mod migrations; pub mod query; pub mod sqlval; -mod autopk; #[cfg(feature = "uuid")] pub mod uuid; +mod autopk; +mod sync; + pub use autopk::AutoPk; use custom::SqlTypeCustom; use db::{BackendRow, Column, ConnectionMethods, ConnectionMethodsAsync}; diff --git a/butane_core/src/many.rs b/butane_core/src/many.rs index 184b13c4..be7d47a2 100644 --- a/butane_core/src/many.rs +++ b/butane_core/src/many.rs @@ -181,13 +181,11 @@ fn load_query_sync<'a, T>( where T: DataObject + 'a, { - let rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build()?; - let future = many - .all_values - .get_or_try_init(|| async { load_query_uncached_sync(many, conn, query) }); - rt.block_on(future).map(|v| v.iter()) + crate::sync::get_or_try_init_tokio_once_cell_sync(&many.all_values, || { + // TODO it would be nice to avoid this clone + load_query_uncached_sync(many, conn, query.clone()) + }) + .map(|v| v.iter()) } /// [`Many`] operations which require a `Connection` diff --git a/butane_core/src/sync.rs b/butane_core/src/sync.rs new file mode 100644 index 00000000..80b66e12 --- /dev/null +++ b/butane_core/src/sync.rs @@ -0,0 +1,24 @@ +use crate::{Error, Result}; +use tokio::sync::OnceCell; + +/// Wrapper around tokio's OnceCell get_or_try_init method +/// This is a sync version which provides the same semantics with a spinlock, as simultaneous initialization should be very rare. +pub fn get_or_try_init_tokio_once_cell_sync<'a, T, F>(cell: &'a OnceCell, f: F) -> Result<&'a T> +where + F: Fn() -> Result, +{ + match cell.get() { + Some(val) => Ok(val), + None => { + loop { + match cell.set(f()?) { + Ok(()) => break, + Err(tokio::sync::SetError::AlreadyInitializedError(_)) => break, + Err(tokio::sync::SetError::InitializingError(_)) => continue, // spinlock + } + } + // Error should be impossible here, we should have already ensured init (or returned error). + cell.get().ok_or(Error::NotInitialized) + } + } +} From 6cb184663438cbcf23d4f65ed13ee62f7d9b698e Mon Sep 17 00:00:00 2001 From: James Oakley Date: Fri, 6 Sep 2024 08:31:59 -0400 Subject: [PATCH 50/78] Unconditionally implement Clone for Query --- butane_core/src/query/mod.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/butane_core/src/query/mod.rs b/butane_core/src/query/mod.rs index 1c230ad1..253d0b3f 100644 --- a/butane_core/src/query/mod.rs +++ b/butane_core/src/query/mod.rs @@ -118,7 +118,7 @@ impl Column { /// Representation of a database query. /// See [`QueryOpSync`] and [`QueryOpAsync`] for operations requiring a live database connection. -#[derive(Clone, Debug)] +#[derive(Debug)] pub struct Query { table: TblName, filter: Option, @@ -184,6 +184,20 @@ impl Query { } } +// Explicit impl so that Clone is implemented even if T is not Clone +impl Clone for Query { + fn clone(&self) -> Self { + Query { + table: self.table.clone(), + filter: self.filter.clone(), + limit: self.limit.clone(), + offset: self.offset.clone(), + sort: self.sort.clone(), + phantom: PhantomData, + } + } +} + mod private { use super::*; From 79a4e73e010b8fb22629a146be0b5675efb9b9bd Mon Sep 17 00:00:00 2001 From: James Oakley Date: Fri, 6 Sep 2024 08:32:25 -0400 Subject: [PATCH 51/78] find_async in example --- example/src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/src/main.rs b/example/src/main.rs index e9a89db1..c1762c3f 100644 --- a/example/src/main.rs +++ b/example/src/main.rs @@ -1,7 +1,7 @@ //! Simple example with all code in a single file. use butane::db::{ConnectionAsync, ConnectionSpec}; use butane::prelude_async::*; -use butane::{find, model, query, AutoPk, Error, ForeignKey, Many}; +use butane::{find_async, model, query, AutoPk, Error, ForeignKey, Many}; type Result = std::result::Result; @@ -78,7 +78,7 @@ async fn query() -> Result<()> { .load(&conn) .await?; assert!(!tagged_posts.is_empty()); - let blog: Blog = find!(Blog, name == "Bears", &conn).unwrap(); + let blog: Blog = find_async!(Blog, name == "Bears", &conn).unwrap(); let posts_in_blog = query!(Post, blog == { &blog }).load(&conn).await?; assert!(!posts_in_blog.is_empty()); let posts_in_blog = query!(Post, blog == { blog }).load(&conn).await?; From 2ea6e5e4114d8de3ca4ea9d2160cf32422cbe591 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Thu, 10 Oct 2024 21:34:07 -0400 Subject: [PATCH 52/78] Switch to butane_test macro and run tests for both sync and async --- Cargo.lock | 15 ++ Cargo.toml | 2 + async_checklist.md | 2 +- butane/Cargo.toml | 2 + butane/tests/basic.rs | 57 ++--- butane/tests/common/blog.rs | 28 ++- butane/tests/custom_enum_derived.rs | 7 +- butane/tests/custom_pg.rs | 15 +- butane/tests/custom_type.rs | 6 +- butane/tests/fake.rs | 10 +- butane/tests/json.rs | 25 +- butane/tests/many.rs | 21 +- butane/tests/nullable.rs | 8 +- butane/tests/query.rs | 55 ++--- butane/tests/unmigrate.rs | 21 +- butane/tests/uuid.rs | 4 +- butane_core/Cargo.toml | 1 + butane_core/src/codegen/dbobj.rs | 3 +- butane_core/src/db/mod.rs | 2 +- butane_core/src/fkey.rs | 2 +- butane_core/src/query/mod.rs | 4 +- butane_core/src/sync.rs | 2 +- butane_core/tests/connection.rs | 7 +- butane_core/tests/migration.rs | 9 +- butane_core/tests/transactions.rs | 9 +- butane_test_helper/Cargo.toml | 1 + butane_test_helper/src/lib.rs | 220 ++++++++++++----- butane_test_macros/Cargo.toml | 18 ++ butane_test_macros/src/lib.rs | 231 ++++++++++++++++++ examples/getting_started/Cargo.toml | 1 + examples/getting_started/src/models.rs | 1 - examples/getting_started/tests/unmigrate.rs | 32 ++- examples/getting_started_async/Cargo.toml | 1 + .../getting_started_async/tests/rollback.rs | 3 +- examples/newtype/Cargo.toml | 1 + examples/newtype/tests/unmigrate.rs | 35 ++- 36 files changed, 647 insertions(+), 214 deletions(-) create mode 100644 butane_test_macros/Cargo.toml create mode 100644 butane_test_macros/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index d93c6bac..ba845a25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -266,6 +266,7 @@ dependencies = [ "butane_codegen", "butane_core", "butane_test_helper", + "butane_test_macros", "cfg-if", "chrono", "env_logger", @@ -273,6 +274,7 @@ dependencies = [ "fake", "geo-types", "log", + "maybe-async-cfg", "nonempty", "once_cell", "paste", @@ -328,6 +330,7 @@ dependencies = [ "async-trait", "butane_core", "butane_test_helper", + "butane_test_macros", "bytes", "cfg-if", "chrono", @@ -371,6 +374,7 @@ version = "0.6.1" dependencies = [ "block-id", "butane_core", + "env_logger", "libc", "log", "maybe-async-cfg", @@ -382,6 +386,15 @@ dependencies = [ "uuid", ] +[[package]] +name = "butane_test_macros" +version = "0.6.1" +dependencies = [ + "proc-macro2 1.0.79", + "quote 1.0.35", + "syn 2.0.58", +] + [[package]] name = "byteorder" version = "1.5.0" @@ -976,6 +989,7 @@ dependencies = [ "butane_cli", "butane_core", "butane_test_helper", + "butane_test_macros", "cfg-if", "env_logger", "log", @@ -1210,6 +1224,7 @@ dependencies = [ "butane_cli", "butane_core", "butane_test_helper", + "butane_test_macros", "cfg-if", "env_logger", "fake", diff --git a/Cargo.toml b/Cargo.toml index 11f87c81..38b9f504 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "butane_codegen", "butane_core", "butane_test_helper", + "butane_test_macros", "example", "examples/newtype", "examples/getting_started", @@ -24,6 +25,7 @@ butane_cli = { version = "0.6", path = "butane_cli" } butane_core = { version = "0.6", path = "butane_core" } butane_codegen = { version = "0.6", path = "butane_codegen" } butane_test_helper = { path = "butane_test_helper" } +butane_test_macros = { path = "butane_test_macros" } cfg-if = "^1.0" chrono = { version = "0.4.25", default-features = false, features = [ "serde", diff --git a/async_checklist.md b/async_checklist.md index 0c966c91..afb4eeec 100644 --- a/async_checklist.md +++ b/async_checklist.md @@ -1,5 +1,5 @@ * [x] Clean up pattern for sync/async variants. Inconsistent between suffix and module -* [ ] Tests should run against sync and async +* [x] Tests should run against sync and async * [ ] Establish soundness for unsafe sections of AsyncAdapter * [ ] Consider publishing `AsyncAdapter` into its own crate * [ ] Ensure Postgres works in sync diff --git a/butane/Cargo.toml b/butane/Cargo.toml index 249e4e58..f00509a4 100644 --- a/butane/Cargo.toml +++ b/butane/Cargo.toml @@ -33,6 +33,7 @@ butane_core = { workspace = true } [dev-dependencies] butane_test_helper = { workspace = true } +butane_test_macros = { workspace = true } cfg-if = { workspace = true } exec_time = { version = "0.1.4" } paste = { workspace = true } @@ -43,6 +44,7 @@ geo-types = "0.7" log.workspace = true nonempty.workspace = true quote = { workspace = true } +maybe-async-cfg.workspace = true proc-macro2 = { workspace = true } once_cell = { workspace = true } tokio = { workspace = true } diff --git a/butane/tests/basic.rs b/butane/tests/basic.rs index 2c4f6a9c..0f9a2f89 100644 --- a/butane/tests/basic.rs +++ b/butane/tests/basic.rs @@ -1,9 +1,10 @@ #![allow(clippy::disallowed_names)] -use butane::db::ConnectionAsync; -use butane::{butane_type, find, model, query, AutoPk, ForeignKey}; -use butane::{colname, prelude_async::*}; +use butane::colname; +use butane::db::{Connection, ConnectionAsync}; +use butane::{butane_type, find, find_async, model, query, AutoPk, ForeignKey}; use butane_test_helper::*; +use butane_test_macros::butane_test; #[cfg(feature = "datetime")] use chrono::{naive::NaiveDateTime, offset::Utc, DateTime}; #[cfg(feature = "sqlite")] @@ -109,6 +110,7 @@ struct TimeHolder { pub when: chrono::DateTime, } +#[butane_test] async fn basic_crud(conn: ConnectionAsync) { //create let mut foo = Foo::new(1); @@ -132,17 +134,14 @@ async fn basic_crud(conn: ConnectionAsync) { // delete assert!(foo3.delete(&conn).await.is_ok()); - if matches!( - Foo::get(&conn, 1).await.err(), - Some(butane::Error::NoSuchObject) - ) { - } else { - panic!("Expected NoSuchObject"); + match Foo::get(&conn, 1).await.err() { + Some(butane::Error::NoSuchObject) => (), + _ => panic!("Expected NoSuchObject"), } assert_eq!(None, Foo::try_get(&conn, 1).await.unwrap()); } -testall!(basic_crud); +#[butane_test] async fn basic_find(conn: ConnectionAsync) { //create let mut foo1 = Foo::new(1); @@ -155,11 +154,11 @@ async fn basic_find(conn: ConnectionAsync) { foo2.save(&conn).await.unwrap(); // find - let found: Foo = find!(Foo, bar == 43, &conn).unwrap(); + let found: Foo = find_async!(Foo, bar == 43, &conn).unwrap(); assert_eq!(found, foo2); } -testall!(basic_find); +#[butane_test] async fn basic_query(conn: ConnectionAsync) { //create let mut foo1 = Foo::new(1); @@ -180,8 +179,8 @@ async fn basic_query(conn: ConnectionAsync) { let found = query!(Foo, bar < 44).load(&conn).await.unwrap(); assert_eq!(found.len(), 2); } -testall!(basic_query); +#[butane_test] async fn basic_query_delete(conn: ConnectionAsync) { //create let mut foo1 = Foo::new(1); @@ -208,8 +207,8 @@ async fn basic_query_delete(conn: ConnectionAsync) { let cnt = query!(Foo, baz.like("hello%")).delete(&conn).await.unwrap(); assert_eq!(cnt, 2); } -testall!(basic_query_delete); +#[butane_test] async fn string_pk(conn: ConnectionAsync) { let mut foo = Foo::new(1); foo.save(&conn).await.unwrap(); @@ -219,8 +218,8 @@ async fn string_pk(conn: ConnectionAsync) { let bar2 = Bar::get(&conn, "tarzan".to_string()).await.unwrap(); assert_eq!(bar, bar2); } -testall!(string_pk); +#[butane_test] async fn foreign_key(conn: ConnectionAsync) { let mut foo = Foo::new(1); foo.save(&conn).await.unwrap(); @@ -234,8 +233,8 @@ async fn foreign_key(conn: ConnectionAsync) { let foo3: &Foo = bar2.foo.get().unwrap(); assert_eq!(foo2, foo3); } -testall!(foreign_key); +#[butane_test] async fn auto_pk(conn: ConnectionAsync) { let mut baz1 = Baz::new("baz1"); baz1.save(&conn).await.unwrap(); @@ -246,8 +245,8 @@ async fn auto_pk(conn: ConnectionAsync) { assert!(baz1.id < baz2.id); assert!(baz2.id < baz3.id); } -testall!(auto_pk); +#[butane_test] async fn only_pk(conn: ConnectionAsync) { let mut obj = HasOnlyPk::new(1); obj.save(&conn).await.unwrap(); @@ -258,8 +257,8 @@ async fn only_pk(conn: ConnectionAsync) { // verify it didnt get a new id assert_eq!(obj.id, 1); } -testall!(only_pk); +#[butane_test] async fn only_auto_pk(conn: ConnectionAsync) { let mut obj = HasOnlyAutoPk::default(); obj.save(&conn).await.unwrap(); @@ -270,8 +269,8 @@ async fn only_auto_pk(conn: ConnectionAsync) { // verify it didnt get a new id assert_eq!(obj.id, pk); } -testall!(only_auto_pk); +#[butane_test] async fn basic_committed_transaction(mut conn: ConnectionAsync) { let tr = conn.transaction().await.unwrap(); @@ -285,8 +284,8 @@ async fn basic_committed_transaction(mut conn: ConnectionAsync) { let foo2 = Foo::get(&conn, 1).await.unwrap(); assert_eq!(foo, foo2); } -testall!(basic_committed_transaction); +#[butane_test] async fn basic_dropped_transaction(mut conn: ConnectionAsync) { // Create an object with a transaction but never commit it { @@ -303,8 +302,8 @@ async fn basic_dropped_transaction(mut conn: ConnectionAsync) { Err(e) => panic!("Unexpected error {e}"), } } -testall!(basic_dropped_transaction); +#[butane_test] async fn basic_rollback_transaction(mut conn: ConnectionAsync) { let tr = conn.transaction().await.unwrap(); @@ -321,8 +320,8 @@ async fn basic_rollback_transaction(mut conn: ConnectionAsync) { Err(e) => panic!("Unexpected error {e}"), } } -testall!(basic_rollback_transaction); +#[butane_test] async fn basic_unique_field_error_on_non_unique(conn: ConnectionAsync) { let mut foo1 = Foo::new(1); foo1.bar = 42; @@ -345,8 +344,8 @@ async fn basic_unique_field_error_on_non_unique(conn: ConnectionAsync) { _ => false, }); } -testall!(basic_unique_field_error_on_non_unique); +#[butane_test] async fn fkey_same_type(conn: ConnectionAsync) { let mut o1 = SelfReferential::new(1); let mut o2 = SelfReferential::new(2); @@ -360,16 +359,16 @@ async fn fkey_same_type(conn: ConnectionAsync) { assert_eq!(inner, o2); assert!(inner.reference.is_none()); } -testall!(fkey_same_type); +#[butane_test] async fn cant_save_unsaved_fkey(conn: ConnectionAsync) { let foo = Foo::new(1); let mut bar = Bar::new("tarzan", foo); assert!(bar.save(&conn).await.is_err()); } -testall!(cant_save_unsaved_fkey); #[cfg(feature = "datetime")] +#[butane_test] async fn basic_time(conn: ConnectionAsync) { let now = Utc::now(); let mut time = TimeHolder { @@ -385,9 +384,8 @@ async fn basic_time(conn: ConnectionAsync) { // lose some precision when we go to the database. assert_eq!(time.utc.timestamp(), time2.utc.timestamp()); } -#[cfg(feature = "datetime")] -testall!(basic_time); +#[butane_test] async fn basic_load_first(conn: ConnectionAsync) { //create let mut foo1 = Foo::new(1); @@ -407,8 +405,8 @@ async fn basic_load_first(conn: ConnectionAsync) { assert_eq!(found, Some(foo1)); } -testall!(basic_load_first); +#[butane_test] async fn basic_load_first_ordered(conn: ConnectionAsync) { //create let mut foo1 = Foo::new(1); @@ -438,8 +436,8 @@ async fn basic_load_first_ordered(conn: ConnectionAsync) { assert_eq!(found_desc, Some(foo2)); } -testall!(basic_load_first_ordered); +#[butane_test] async fn save_upserts_by_default(conn: ConnectionAsync) { let mut foo = Foo::new(1); foo.bar = 42; @@ -456,4 +454,3 @@ async fn save_upserts_by_default(conn: ConnectionAsync) { let retrieved = Foo::get(&conn, 1).await.unwrap(); assert_eq!(retrieved.bar, 43); } -testall!(save_upserts_by_default); diff --git a/butane/tests/common/blog.rs b/butane/tests/common/blog.rs index 0f0497ef..fc7a581d 100644 --- a/butane/tests/common/blog.rs +++ b/butane/tests/common/blog.rs @@ -1,6 +1,9 @@ //! Helpers for several tests. -use butane::{dataresult, model, DataObject, DataObjectOpAsync}; -use butane::{db::ConnectionAsync, ForeignKey, Many}; +use butane::{dataresult, model}; +use butane::{ + db::{Connection, ConnectionAsync}, + ForeignKey, Many, +}; #[cfg(feature = "datetime")] use chrono::{naive::NaiveDateTime, offset::Utc}; #[cfg(feature = "fake")] @@ -100,7 +103,16 @@ impl Tag { } } +#[maybe_async_cfg::maybe( + sync(), + async(keep_self), + idents( + DataObjectOpAsync(async = "DataObjectOpAsync", sync = "DataObjectOpSync"), + ConnectionAsync(async = "ConnectionAsync", sync = "Connection") + ) +)] pub async fn create_tag(conn: &ConnectionAsync, name: &str) -> Tag { + use butane::DataObjectOpAsync; let mut tag = Tag::new(name); tag.save(conn).await.unwrap(); tag @@ -110,7 +122,17 @@ pub async fn create_tag(conn: &ConnectionAsync, name: &str) -> Tag { /// 1. "Cats" /// 2. "Mountains" #[allow(dead_code)] // only used by some test files -pub async fn setup_blog(conn: &ConnectionAsync) { +#[maybe_async_cfg::maybe( + sync(), + async(keep_self), + idents( + DataObjectOp, + Connection(async = "ConnectionAsync", sync = "Connection"), + create_tag(async = "create_tag", snake), + ) +)] +pub async fn setup_blog(conn: &Connection) { + use butane::DataObjectOp; let mut cats_blog = Blog::new(1, "Cats"); cats_blog.save(conn).await.unwrap(); let mut mountains_blog = Blog::new(2, "Mountains"); diff --git a/butane/tests/custom_enum_derived.rs b/butane/tests/custom_enum_derived.rs index 0c1bfe4e..b6f8ad36 100644 --- a/butane/tests/custom_enum_derived.rs +++ b/butane/tests/custom_enum_derived.rs @@ -1,9 +1,8 @@ // Tests deriving FieldType for an enum -use butane::db::ConnectionAsync; -use butane::prelude_async::*; use butane::{model, query}; use butane::{FieldType, FromSql, SqlVal, ToSql}; use butane_test_helper::*; +use butane_test_macros::butane_test; #[derive(PartialEq, Eq, Debug, Clone, FieldType)] enum Whatsit { @@ -24,6 +23,7 @@ impl HasCustomField2 { } } +#[butane_test] async fn roundtrip_custom_type(conn: ConnectionAsync) { //create let mut obj = HasCustomField2::new(1, Whatsit::Foo); @@ -33,8 +33,8 @@ async fn roundtrip_custom_type(conn: ConnectionAsync) { let obj2 = HasCustomField2::get(&conn, 1).await.unwrap(); assert_eq!(obj, obj2); } -testall!(roundtrip_custom_type); +#[butane_test] async fn query_custom_type(conn: ConnectionAsync) { //create let mut obj_foo = HasCustomField2::new(1, Whatsit::Foo); @@ -50,7 +50,6 @@ async fn query_custom_type(conn: ConnectionAsync) { assert_eq!(results.len(), 1); assert_eq!(results[0], obj_bar) } -testall!(query_custom_type); #[test] fn enum_to_sql() { diff --git a/butane/tests/custom_pg.rs b/butane/tests/custom_pg.rs index b0c7b3ed..84b5a440 100644 --- a/butane/tests/custom_pg.rs +++ b/butane/tests/custom_pg.rs @@ -2,10 +2,10 @@ #[cfg(feature = "pg")] mod custom_pg { use butane::custom::{SqlTypeCustom, SqlValRefCustom}; - use butane::prelude_async::*; - use butane::{butane_type, db::ConnectionAsync, model}; + use butane::{butane_type, model}; use butane::{AutoPk, FieldType, FromSql, SqlType, SqlVal, SqlValRef, ToSql}; - use butane_test_helper::{maketest, maketest_pg}; + use butane_test_helper::*; + use butane_test_macros::butane_test; use geo_types; use tokio_postgres as postgres; @@ -58,6 +58,7 @@ mod custom_pg { pt_to: Point, } + #[butane_test(pg)] async fn roundtrip_custom(conn: ConnectionAsync) { let mut trip = Trip { id: AutoPk::uninitialized(), @@ -69,10 +70,10 @@ mod custom_pg { let trip2 = Trip::get(&conn, trip.id).await.unwrap(); assert_eq!(trip, trip2); } - maketest_pg!(roundtrip_custom, true); /* - TODO point in postgres doesn't support normal equality, so need + TODO point in postgres doesn't support normal equality, so need + #[butane_test(pg)] fn query_custom(conn: Connection) { let origin = Point::new(0.0, 0.0); let mut trip1 = Trip { @@ -92,7 +93,5 @@ mod custom_pg { let trips = query!(Trip, pt_from ~= { origin }).load(&conn).unwrap(); assert_eq!(trips.len(), 1); assert_eq!(trip1, trips[0]); - } - - maketest_pg!(query_custom);*/ + }*/ } diff --git a/butane/tests/custom_type.rs b/butane/tests/custom_type.rs index 0af175e6..c2dc77fa 100644 --- a/butane/tests/custom_type.rs +++ b/butane/tests/custom_type.rs @@ -1,8 +1,8 @@ use butane::db::ConnectionAsync; -use butane::prelude_async::*; use butane::{butane_type, model, query}; use butane::{FieldType, FromSql, SqlType, SqlVal, SqlValRef, ToSql}; use butane_test_helper::*; +use butane_test_macros::*; #[butane_type(Text)] #[derive(PartialEq, Eq, Debug, Clone)] @@ -60,6 +60,7 @@ impl HasCustomField { } } +#[butane_test] async fn roundtrip_custom_type(conn: ConnectionAsync) { //create let mut obj = HasCustomField::new(1, Frobnozzle::Foo); @@ -69,8 +70,8 @@ async fn roundtrip_custom_type(conn: ConnectionAsync) { let obj2 = HasCustomField::get(&conn, 1).await.unwrap(); assert_eq!(obj, obj2); } -testall!(roundtrip_custom_type); +#[butane_test] async fn query_custom_type(conn: ConnectionAsync) { //create let mut obj_foo = HasCustomField::new(1, Frobnozzle::Foo); @@ -86,4 +87,3 @@ async fn query_custom_type(conn: ConnectionAsync) { assert_eq!(results.len(), 1); assert_eq!(results[0], obj_bar) } -testall!(query_custom_type); diff --git a/butane/tests/fake.rs b/butane/tests/fake.rs index d5395326..b873a350 100644 --- a/butane/tests/fake.rs +++ b/butane/tests/fake.rs @@ -1,12 +1,13 @@ -use butane::db::ConnectionAsync; -use butane::prelude_async::*; -use butane::{find, ForeignKey}; +use butane::db::{Connection, ConnectionAsync}; +use butane::{find, find_async, ForeignKey}; use butane_test_helper::*; +use butane_test_macros::butane_test; use fake::{Fake, Faker}; mod common; use common::blog::{Blog, Post, Tag}; +#[butane_test] async fn fake_blog_post(conn: ConnectionAsync) { let mut fake_blog: Blog = Faker.fake(); fake_blog.save(&conn).await.unwrap(); @@ -26,8 +27,7 @@ async fn fake_blog_post(conn: ConnectionAsync) { post.tags.add(&tag_3).unwrap(); post.save(&conn).await.unwrap(); - let post_from_db = find!(Post, id == { post.id }, &conn).unwrap(); + let post_from_db = find_async!(Post, id == { post.id }, &conn).unwrap(); assert_eq!(post_from_db.title, post.title); assert_eq!(post_from_db.tags.load(&conn).await.unwrap().count(), 3); } -testall!(fake_blog_post); diff --git a/butane/tests/json.rs b/butane/tests/json.rs index 19d2c11c..e9d5a666 100644 --- a/butane/tests/json.rs +++ b/butane/tests/json.rs @@ -3,9 +3,12 @@ use std::collections::{BTreeMap, HashMap}; use butane::model; -use butane::prelude_async::*; -use butane::{db::ConnectionAsync, FieldType}; +use butane::{ + db::{Connection, ConnectionAsync}, + FieldType, +}; use butane_test_helper::*; +use butane_test_macros::butane_test; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -26,6 +29,7 @@ impl FooJJ { } } +#[butane_test] async fn json_null(conn: ConnectionAsync) { // create let id = 4; @@ -42,8 +46,8 @@ async fn json_null(conn: ConnectionAsync) { let foo3 = FooJJ::get(&conn, id).await.unwrap(); assert_eq!(foo2, foo3); } -testall!(json_null); +#[butane_test] async fn basic_json(conn: ConnectionAsync) { // create let id = 4; @@ -71,7 +75,6 @@ async fn basic_json(conn: ConnectionAsync) { let foo3 = FooJJ::get(&conn, id).await.unwrap(); assert_eq!(foo2, foo3); } -testall!(basic_json); #[model] #[derive(PartialEq, Eq, Debug, Clone)] @@ -89,6 +92,8 @@ impl FooHH { } } } + +#[butane_test] async fn basic_hashmap(conn: ConnectionAsync) { // create let id = 4; @@ -109,7 +114,6 @@ async fn basic_hashmap(conn: ConnectionAsync) { let foo3 = FooHH::get(&conn, id).await.unwrap(); assert_eq!(foo2, foo3); } -testall!(basic_hashmap); #[model] #[derive(PartialEq, Eq, Debug, Clone)] @@ -127,6 +131,8 @@ impl FooFullPrefixHashMap { } } } + +#[butane_test] async fn basic_hashmap_full_prefix(conn: ConnectionAsync) { // create let id = 4; @@ -147,7 +153,6 @@ async fn basic_hashmap_full_prefix(conn: ConnectionAsync) { let foo3 = FooFullPrefixHashMap::get(&conn, id).await.unwrap(); assert_eq!(foo2, foo3); } -testall!(basic_hashmap_full_prefix); #[model] #[derive(PartialEq, Eq, Debug, Clone)] @@ -165,6 +170,8 @@ impl FooBTreeMap { } } } + +#[butane_test] async fn basic_btreemap(conn: ConnectionAsync) { // create let id = 4; @@ -185,7 +192,6 @@ async fn basic_btreemap(conn: ConnectionAsync) { let foo3 = FooBTreeMap::get(&conn, id).await.unwrap(); assert_eq!(foo2, foo3); } -testall!(basic_btreemap); #[derive(PartialEq, Eq, Debug, Default, Clone, serde::Deserialize, serde::Serialize)] struct HashedObject { @@ -209,6 +215,8 @@ impl FooHHO { } } } + +#[butane_test] async fn hashmap_with_object_values(conn: ConnectionAsync) { // create let id = 4; @@ -229,7 +237,6 @@ async fn hashmap_with_object_values(conn: ConnectionAsync) { let foo3 = FooHHO::get(&conn, id).await.unwrap(); assert_eq!(foo2, foo3); } -testall!(hashmap_with_object_values); #[derive(PartialEq, Eq, Debug, Clone, FieldType, Serialize, Deserialize)] struct InlineFoo { @@ -255,6 +262,7 @@ impl OuterFoo { } } +#[butane_test] async fn inline_json(conn: ConnectionAsync) { // create let id = 4; @@ -271,4 +279,3 @@ async fn inline_json(conn: ConnectionAsync) { let foo3 = OuterFoo::get(&conn, id).await.unwrap(); assert_eq!(foo2, foo3); } -testall!(inline_json); diff --git a/butane/tests/many.rs b/butane/tests/many.rs index 172b3193..ddd5e02a 100644 --- a/butane/tests/many.rs +++ b/butane/tests/many.rs @@ -1,12 +1,9 @@ -use butane::db::ConnectionAsync; -use butane::prelude_async::*; use butane::{model, query::OrderDirection, AutoPk, Many}; -use butane_test_helper::testall; -#[cfg(any(feature = "pg", feature = "sqlite"))] use butane_test_helper::*; +use butane_test_macros::butane_test; mod common; -use common::blog::{create_tag, Blog, Post, Tag}; +use common::blog::{create_tag, create_tag_sync, Blog, Post, Tag}; #[model] struct AutoPkWithMany { @@ -47,6 +44,7 @@ struct AutoItem { val: String, } +#[butane_test] async fn load_sorted_from_many(conn: ConnectionAsync) { let mut cats_blog = Blog::new(1, "Cats"); cats_blog.save(&conn).await.unwrap(); @@ -85,8 +83,8 @@ async fn load_sorted_from_many(conn: ConnectionAsync) { assert_eq!(tag_iter.next().unwrap().tag, "european"); assert_eq!(tag_iter.next().unwrap().tag, "cat"); } -testall!(load_sorted_from_many); +#[butane_test] async fn remove_one_from_many(conn: ConnectionAsync) { let mut cats_blog = Blog::new(1, "Cats"); cats_blog.save(&conn).await.unwrap(); @@ -112,8 +110,8 @@ async fn remove_one_from_many(conn: ConnectionAsync) { let post2 = Post::get(&conn, post.id).await.unwrap(); assert_eq!(post2.tags.load(&conn).await.unwrap().count(), 2); } -testall!(remove_one_from_many); +#[butane_test] async fn remove_multiple_from_many(conn: ConnectionAsync) { let mut cats_blog = Blog::new(1, "Cats"); cats_blog.save(&conn).await.unwrap(); @@ -142,8 +140,8 @@ async fn remove_multiple_from_many(conn: ConnectionAsync) { let post2 = Post::get(&conn, post.id).await.unwrap(); assert_eq!(post2.tags.load(&conn).await.unwrap().count(), 2); } -testall!(remove_multiple_from_many); +#[butane_test] async fn delete_all_from_many(conn: ConnectionAsync) { let mut cats_blog = Blog::new(1, "Cats"); cats_blog.save(&conn).await.unwrap(); @@ -169,8 +167,8 @@ async fn delete_all_from_many(conn: ConnectionAsync) { let post2 = Post::get(&conn, post.id).await.unwrap(); assert_eq!(post2.tags.load(&conn).await.unwrap().count(), 0); } -testall!(delete_all_from_many); +#[butane_test] async fn can_add_to_many_before_save(conn: ConnectionAsync) { // Verify that for an object with an auto-pk, we can add items to a Many field before we actually // save the original object (and thus get the actual pk); @@ -183,8 +181,8 @@ async fn can_add_to_many_before_save(conn: ConnectionAsync) { let tags = obj.tags.load(&conn).await.unwrap(); assert_eq!(tags.count(), 2); } -testall!(can_add_to_many_before_save); +#[butane_test] async fn cant_add_unsaved_to_many(_conn: ConnectionAsync) { let unsaved_item = AutoItem { id: AutoPk::uninitialized(), @@ -197,8 +195,8 @@ async fn cant_add_unsaved_to_many(_conn: ConnectionAsync) { .expect_err("unexpectedly not error"); assert!(matches!(err, butane::Error::ValueNotSaved)); } -testall!(cant_add_unsaved_to_many); +#[butane_test] async fn can_add_to_many_with_custom_table_name(conn: ConnectionAsync) { let mut obj = RenamedAutoPkWithMany::new(); obj.tags.add(&create_tag(&conn, "blue").await).unwrap(); @@ -209,4 +207,3 @@ async fn can_add_to_many_with_custom_table_name(conn: ConnectionAsync) { let tags = obj.tags.load(&conn).await.unwrap(); assert_eq!(tags.count(), 2); } -testall!(can_add_to_many_with_custom_table_name); diff --git a/butane/tests/nullable.rs b/butane/tests/nullable.rs index 9f791cef..b3c243d4 100644 --- a/butane/tests/nullable.rs +++ b/butane/tests/nullable.rs @@ -1,7 +1,7 @@ use butane::db::ConnectionAsync; -use butane::prelude_async::*; use butane::{model, query}; use butane_test_helper::*; +use butane_test_macros::butane_test; #[model] #[derive(PartialEq, Eq, Debug)] @@ -15,6 +15,7 @@ impl WithNullable { } } +#[butane_test] async fn basic_optional(conn: ConnectionAsync) { let mut with_none = WithNullable::new(1); with_none.save(&conn).await.unwrap(); @@ -29,8 +30,8 @@ async fn basic_optional(conn: ConnectionAsync) { let obj = WithNullable::get(&conn, 2).await.unwrap(); assert_eq!(obj.foo, Some(42)); } -testall!(basic_optional); +#[butane_test] async fn query_optional_with_some(conn: ConnectionAsync) { let mut obj = WithNullable::new(1); obj.save(&conn).await.unwrap(); @@ -53,8 +54,8 @@ async fn query_optional_with_some(conn: ConnectionAsync) { assert_eq!(objs[0].foo, Some(43)); assert_eq!(objs[1].foo, Some(44)); } -testall!(query_optional_with_some); +#[butane_test] async fn query_optional_with_none(conn: ConnectionAsync) { let mut obj = WithNullable::new(1); obj.save(&conn).await.unwrap(); @@ -67,4 +68,3 @@ async fn query_optional_with_none(conn: ConnectionAsync) { assert_eq!(objs.len(), 1); assert_eq!(objs[0].id, 1); } -testall!(query_optional_with_none); diff --git a/butane/tests/query.rs b/butane/tests/query.rs index 6534e78e..5981f518 100644 --- a/butane/tests/query.rs +++ b/butane/tests/query.rs @@ -1,8 +1,8 @@ -use butane::db::ConnectionAsync; -use butane::prelude_async::*; +use butane::db::{Connection, ConnectionAsync}; use butane::query::BoolExpr; -use butane::{colname, filter, find, query, Many}; +use butane::{colname, filter, find, find_async, query, Many}; use butane_test_helper::*; +use butane_test_macros::butane_test; #[cfg(feature = "datetime")] use chrono::{TimeZone, Utc}; @@ -10,6 +10,7 @@ mod common; use common::blog; use common::blog::{Blog, Post, PostMetadata, Tag}; +#[butane_test] async fn equality(conn: ConnectionAsync) { blog::setup_blog(&conn).await; let mut posts = query!(Post, published == true).load(&conn).await.unwrap(); @@ -19,8 +20,8 @@ async fn equality(conn: ConnectionAsync) { assert_eq!(posts[1].title, "Sir Charles"); assert_eq!(posts[2].title, "Mount Doom"); } -testall!(equality); +#[butane_test] async fn equality_separate_dataresult(conn: ConnectionAsync) { blog::setup_blog(&conn).await; let mut posts = query!(PostMetadata, published == true) @@ -33,8 +34,8 @@ async fn equality_separate_dataresult(conn: ConnectionAsync) { assert_eq!(posts[1].title, "Sir Charles"); assert_eq!(posts[2].title, "Mount Doom"); } -testall!(equality_separate_dataresult); +#[butane_test] async fn ordered(conn: ConnectionAsync) { blog::setup_blog(&conn).await; let posts = query!(Post, published == true) @@ -47,8 +48,8 @@ async fn ordered(conn: ConnectionAsync) { assert_eq!(posts[1].title, "Sir Charles"); assert_eq!(posts[2].title, "The Tiger"); } -testall!(ordered); +#[butane_test] async fn comparison(conn: ConnectionAsync) { blog::setup_blog(&conn).await; let mut posts = query!(Post, likes < 5).load(&conn).await.unwrap(); @@ -57,8 +58,8 @@ async fn comparison(conn: ConnectionAsync) { assert_eq!(posts[0].title, "The Tiger"); assert_eq!(posts[1].title, "Mt. Everest"); } -testall!(comparison); +#[butane_test] async fn like(conn: ConnectionAsync) { blog::setup_blog(&conn).await; let mut posts = query!(Post, title.like("M%")).load(&conn).await.unwrap(); @@ -67,8 +68,8 @@ async fn like(conn: ConnectionAsync) { assert_eq!(posts[0].title, "Mount Doom"); assert_eq!(posts[1].title, "Mt. Everest"); } -testall!(like); +#[butane_test] async fn combination(conn: ConnectionAsync) { blog::setup_blog(&conn).await; let posts = query!(Post, published == true && likes < 5) @@ -78,8 +79,8 @@ async fn combination(conn: ConnectionAsync) { assert_eq!(posts.len(), 1); assert_eq!(posts[0].title, "The Tiger"); } -testall!(combination); +#[butane_test] async fn combination_allof(conn: ConnectionAsync) { blog::setup_blog(&conn).await; let posts = Post::query() @@ -94,8 +95,8 @@ async fn combination_allof(conn: ConnectionAsync) { assert_eq!(posts.len(), 1); assert_eq!(posts[0].title, "The Tiger"); } -testall!(combination_allof); +#[butane_test] async fn not_found(conn: ConnectionAsync) { blog::setup_blog(&conn).await; let posts = query!(Post, published == false && likes > 5) @@ -104,24 +105,24 @@ async fn not_found(conn: ConnectionAsync) { .unwrap(); assert_eq!(posts.len(), 0); } -testall!(not_found); +#[butane_test] async fn rustval(conn: ConnectionAsync) { blog::setup_blog(&conn).await; // We don't need to escape into rust for this, but we can - let post = find!(Post, title == { "The Tiger" }, &conn).unwrap(); + let post = find_async!(Post, title == { "The Tiger" }, &conn).unwrap(); assert_eq!(post.title, "The Tiger"); // or invoke a function that returns a value let f = || "The Tiger"; - let post2 = find!(Post, title == { f() }, &conn).unwrap(); + let post2 = find_async!(Post, title == { f() }, &conn).unwrap(); assert_eq!(post, post2); } -testall!(rustval); +#[butane_test] async fn fkey_match(conn: ConnectionAsync) { blog::setup_blog(&conn).await; - let blog: Blog = find!(Blog, name == "Cats", &conn).unwrap(); + let blog: Blog = find_async!(Blog, name == "Cats", &conn).unwrap(); let mut posts = query!(Post, blog == { &blog }).load(&conn).await.unwrap(); let posts2 = query!(Post, blog == { blog }).load(&conn).await.unwrap(); let blog_id = blog.id; @@ -139,22 +140,22 @@ async fn fkey_match(conn: ConnectionAsync) { assert_eq!(posts, posts3); assert_eq!(posts, posts4); } -testall!(fkey_match); +#[butane_test] async fn many_load(conn: ConnectionAsync) { blog::setup_blog(&conn).await; - let post: Post = find!(Post, title == "The Tiger", &conn).unwrap(); + let post: Post = find_async!(Post, title == "The Tiger", &conn).unwrap(); let tags = post.tags.load(&conn).await.unwrap(); let mut tags: Vec<&Tag> = tags.collect(); tags.sort_by(|t1, t2| t1.tag.partial_cmp(&t2.tag).unwrap()); assert_eq!(tags[0].tag, "asia"); assert_eq!(tags[1].tag, "danger"); } -testall!(many_load); +#[butane_test] async fn many_serialize(conn: ConnectionAsync) { blog::setup_blog(&conn).await; - let post: Post = find!(Post, title == "The Tiger", &conn).unwrap(); + let post: Post = find_async!(Post, title == "The Tiger", &conn).unwrap(); let tags_json: String = serde_json::to_string(&post.tags).unwrap(); let tags: Many = serde_json::from_str(&tags_json).unwrap(); let tags = tags.load(&conn).await.unwrap(); @@ -163,8 +164,8 @@ async fn many_serialize(conn: ConnectionAsync) { assert_eq!(tags[0].tag, "asia"); assert_eq!(tags[1].tag, "danger"); } -testall!(many_serialize); +#[butane_test] async fn many_objects_with_tag(conn: ConnectionAsync) { blog::setup_blog(&conn).await; let mut posts = query!(Post, tags.contains("danger")) @@ -176,8 +177,8 @@ async fn many_objects_with_tag(conn: ConnectionAsync) { assert_eq!(posts[1].title, "Mount Doom"); assert_eq!(posts[2].title, "Mt. Everest"); } -testall!(many_objects_with_tag); +#[butane_test] async fn many_objects_with_tag_explicit(conn: ConnectionAsync) { blog::setup_blog(&conn).await; let mut posts = query!(Post, tags.contains(tag == "danger")) @@ -189,12 +190,12 @@ async fn many_objects_with_tag_explicit(conn: ConnectionAsync) { assert_eq!(posts[1].title, "Mount Doom"); assert_eq!(posts[2].title, "Mt. Everest"); } -testall!(many_objects_with_tag_explicit); +#[butane_test] #[cfg(feature = "datetime")] async fn by_timestamp(conn: ConnectionAsync) { blog::setup_blog(&conn).await; - let mut post = find!(Post, title == "Sir Charles", &conn).unwrap(); + let mut post = find_async!(Post, title == "Sir Charles", &conn).unwrap(); // Pretend this post was published in 1970 post.pub_time = Some( Utc.with_ymd_and_hms(1970, 1, 1, 1, 1, 1) @@ -204,7 +205,7 @@ async fn by_timestamp(conn: ConnectionAsync) { ); post.save(&conn).await.unwrap(); // And pretend another post was later in 1971 - let mut post = find!(Post, title == "The Tiger", &conn).unwrap(); + let mut post = find_async!(Post, title == "The Tiger", &conn).unwrap(); post.pub_time = Some( Utc.with_ymd_and_hms(1970, 5, 1, 1, 1, 1) .single() @@ -231,9 +232,8 @@ async fn by_timestamp(conn: ConnectionAsync) { assert_eq!(posts[0].title, "The Tiger"); assert_eq!(posts[1].title, "Sir Charles"); } -#[cfg(feature = "datetime")] -testall!(by_timestamp); +#[butane_test] async fn limit(conn: ConnectionAsync) { blog::setup_blog(&conn).await; let posts = Post::query() @@ -246,8 +246,8 @@ async fn limit(conn: ConnectionAsync) { assert_eq!(posts[0].title, "Mount Doom"); assert_eq!(posts[1].title, "Mt. Everest"); } -testall!(limit); +#[butane_test] async fn offset(conn: ConnectionAsync) { blog::setup_blog(&conn).await; // Now get the more posts after the two we got in the limit test above @@ -261,4 +261,3 @@ async fn offset(conn: ConnectionAsync) { assert_eq!(posts[0].title, "Sir Charles"); assert_eq!(posts[1].title, "The Tiger"); } -testall!(offset); diff --git a/butane/tests/unmigrate.rs b/butane/tests/unmigrate.rs index 234ed899..85463bcf 100644 --- a/butane/tests/unmigrate.rs +++ b/butane/tests/unmigrate.rs @@ -1,11 +1,13 @@ //! Test the "current" migration created by the butane_test_helper due to //! all of the other tests in the butane/tests directory. #![cfg(test)] -use butane::db::{BackendConnectionAsync, ConnectionAsync}; +use butane::db::{Connection, ConnectionAsync}; use butane::migrations::{Migration, Migrations}; use butane_test_helper::*; +use butane_test_macros::*; -async fn unmigrate(mut connection: ConnectionAsync) { +#[butane_test(async)] +async fn unmigrate_async(mut connection: ConnectionAsync) { let mem_migrations = create_current_migrations(connection.backend()); connection @@ -23,4 +25,17 @@ async fn unmigrate(mut connection: ConnectionAsync) { .await .unwrap(); } -testall!(unmigrate); + +#[butane_test(sync)] +fn unmigrate_sync(mut conn: Connection) { + let mem_migrations = create_current_migrations(conn.backend()); + + let migrations = mem_migrations.unapplied_migrations(&conn).unwrap(); + assert_eq!(migrations.len(), 0); + + let migration = mem_migrations.latest().unwrap(); + migration.downgrade(&mut conn).unwrap(); + + let migrations = mem_migrations.unapplied_migrations(&conn).unwrap(); + assert_eq!(migrations.len(), 1); +} diff --git a/butane/tests/uuid.rs b/butane/tests/uuid.rs index d55c1d9b..5914afb0 100644 --- a/butane/tests/uuid.rs +++ b/butane/tests/uuid.rs @@ -1,7 +1,7 @@ use butane::db::ConnectionAsync; use butane::model; -use butane::prelude_async::*; use butane_test_helper::*; +use butane_test_macros::butane_test; use uuid_for_test::Uuid; #[model] @@ -16,6 +16,7 @@ impl FooUU { } } +#[butane_test] async fn basic_uuid(conn: ConnectionAsync) { //create let id = Uuid::new_v4(); @@ -34,4 +35,3 @@ async fn basic_uuid(conn: ConnectionAsync) { let foo3 = FooUU::get(&conn, id).await.unwrap(); assert_eq!(foo2, foo3); } -testall!(basic_uuid); diff --git a/butane_core/Cargo.toml b/butane_core/Cargo.toml index 0dedac73..e350333b 100644 --- a/butane_core/Cargo.toml +++ b/butane_core/Cargo.toml @@ -62,6 +62,7 @@ uuid = { workspace = true, optional = true } butane_core = { workspace = true, features = ["log"] } assert_matches = "1.5" butane_test_helper = { workspace = true } +butane_test_macros.workspace = true env_logger = { workspace = true } paste = { workspace = true } tempfile.workspace = true diff --git a/butane_core/src/codegen/dbobj.rs b/butane_core/src/codegen/dbobj.rs index ad808fa1..3716fef7 100644 --- a/butane_core/src/codegen/dbobj.rs +++ b/butane_core/src/codegen/dbobj.rs @@ -203,6 +203,7 @@ pub fn impl_dataresult(ast_struct: &ItemStruct, dbo: &Ident, config: &Config) -> #cols ]; fn from_row(row: &dyn butane::db::BackendRow) -> butane::Result { + use butane::DataObject; if row.len() != #numdbfields { return Err(butane::Error::BoundsError( "Found unexpected number of columns in row for DataResult".to_string() @@ -444,7 +445,7 @@ fn impl_many_save(ast_struct: &ItemStruct, config: &Config, is_async: bool) -> T quote!( self.#ident.ensure_init( #many_table_lit, - butane::ToSql::to_sql(self.pk()), + butane::ToSql::to_sql(butane::DataObject::pk(self)), #pksqltype, ); #save_with_conn diff --git a/butane_core/src/db/mod.rs b/butane_core/src/db/mod.rs index b82ce44c..36f4c5ad 100644 --- a/butane_core/src/db/mod.rs +++ b/butane_core/src/db/mod.rs @@ -213,7 +213,7 @@ impl Connection { pub fn new(conn: Box) -> Self { Self { conn } } - pub async fn execute(&mut self, sql: impl AsRef) -> Result<()> { + pub async fn execute(&self, sql: impl AsRef) -> Result<()> { self.conn.execute(sql.as_ref()).await } // For use with connection_method_wrapper macro diff --git a/butane_core/src/fkey.rs b/butane_core/src/fkey.rs index 474ee3f9..e8729bbb 100644 --- a/butane_core/src/fkey.rs +++ b/butane_core/src/fkey.rs @@ -85,7 +85,7 @@ impl ForeignKey { } } -/// [`Many`] operations which require a `Connection` +/// [`ForeignKey`] operations which require a `Connection` #[allow(async_fn_in_trait)] // Not intended to be implemented outside Butane #[maybe_async_cfg::maybe( idents(ConnectionMethods(sync = "ConnectionMethods"),), diff --git a/butane_core/src/query/mod.rs b/butane_core/src/query/mod.rs index 253d0b3f..678c21df 100644 --- a/butane_core/src/query/mod.rs +++ b/butane_core/src/query/mod.rs @@ -190,8 +190,8 @@ impl Clone for Query { Query { table: self.table.clone(), filter: self.filter.clone(), - limit: self.limit.clone(), - offset: self.offset.clone(), + limit: self.limit, + offset: self.offset, sort: self.sort.clone(), phantom: PhantomData, } diff --git a/butane_core/src/sync.rs b/butane_core/src/sync.rs index 80b66e12..4fc0e408 100644 --- a/butane_core/src/sync.rs +++ b/butane_core/src/sync.rs @@ -3,7 +3,7 @@ use tokio::sync::OnceCell; /// Wrapper around tokio's OnceCell get_or_try_init method /// This is a sync version which provides the same semantics with a spinlock, as simultaneous initialization should be very rare. -pub fn get_or_try_init_tokio_once_cell_sync<'a, T, F>(cell: &'a OnceCell, f: F) -> Result<&'a T> +pub fn get_or_try_init_tokio_once_cell_sync(cell: &OnceCell, f: F) -> Result<&T> where F: Fn() -> Result, { diff --git a/butane_core/tests/connection.rs b/butane_core/tests/connection.rs index d724bbde..f6c78657 100644 --- a/butane_core/tests/connection.rs +++ b/butane_core/tests/connection.rs @@ -1,10 +1,11 @@ -use butane_core::db::{connect_async, BackendConnectionAsync, ConnectionAsync, ConnectionSpec}; +use butane_core::db::{connect_async, ConnectionAsync, ConnectionSpec}; use butane_test_helper::*; +use butane_test_macros::butane_test; +#[butane_test(nomigrate)] async fn connection_not_closed(conn: ConnectionAsync) { assert!(!conn.is_closed()); } -testall_no_migrate!(connection_not_closed); #[test] fn persist_invalid_connection_backend() { @@ -61,6 +62,7 @@ async fn unreachable_pg_connection() { } } +#[butane_test(nomigrate)] async fn debug_connection(conn: ConnectionAsync) { let backend_name = conn.backend_name(); @@ -71,7 +73,6 @@ async fn debug_connection(conn: ConnectionAsync) { assert!(debug_str.contains("path: Some(\"\")")); } } -testall_no_migrate!(debug_connection); #[test] fn wont_load_connection_spec_from_missing_path() { diff --git a/butane_core/tests/migration.rs b/butane_core/tests/migration.rs index 37365643..ffa886d1 100644 --- a/butane_core/tests/migration.rs +++ b/butane_core/tests/migration.rs @@ -1,7 +1,8 @@ -use butane_core::db::{BackendConnectionAsync, ConnectionAsync, ConnectionMethodsAsync}; +use butane_core::db::ConnectionAsync; use butane_core::migrations::adb::*; use butane_core::SqlType; use butane_test_helper::*; +use butane_test_macros::butane_test; #[test] fn empty_diff() { @@ -211,6 +212,7 @@ fn add_table_fkey() { /// This is the same as test "add_table_fkey", except that it /// runs the DDL on a database, and then deletes the column. +#[butane_test(nomigrate)] async fn add_table_fkey_delete_column(conn: ConnectionAsync) { let known_int_type = DeferredSqlType::KnownId(TypeIdentifier::Ty(SqlType::Int)); @@ -293,11 +295,11 @@ async fn add_table_fkey_delete_column(conn: ConnectionAsync) { .unwrap(); conn.execute(&sql).await.unwrap(); } -testall_no_migrate!(add_table_fkey_delete_column); /// This is the same as test "add_table_fkey", except that it /// intentionally links a column on table a to table b, and /// it runs the DDL on a database. +#[butane_test(nomigrate)] async fn add_table_fkey_back_reference(conn: ConnectionAsync) { let known_int_type = DeferredSqlType::KnownId(TypeIdentifier::Ty(SqlType::Int)); @@ -384,10 +386,10 @@ async fn add_table_fkey_back_reference(conn: ConnectionAsync) { conn.execute("SELECT * from a").await.unwrap(); conn.execute("SELECT * from b").await.unwrap(); } -testall_no_migrate!(add_table_fkey_back_reference); /// This is the same as test "add_table_fkey", except that it /// creates a table with multiple fkey constraints. +#[butane_test(nomigrate)] async fn add_table_fkey_multiple(conn: ConnectionAsync) { let known_int_type = DeferredSqlType::KnownId(TypeIdentifier::Ty(SqlType::Int)); @@ -495,7 +497,6 @@ async fn add_table_fkey_multiple(conn: ConnectionAsync) { conn.execute("SELECT * from a").await.unwrap(); conn.execute("SELECT * from b").await.unwrap(); } -testall_no_migrate!(add_table_fkey_multiple); /// Creates the test case for adding a foreign key, returning the migration operations, /// the target ADB, and the tables which should be expected to be created. diff --git a/butane_core/tests/transactions.rs b/butane_core/tests/transactions.rs index 670afcb3..6d72731e 100644 --- a/butane_core/tests/transactions.rs +++ b/butane_core/tests/transactions.rs @@ -1,6 +1,8 @@ -use butane_core::db::{BackendConnectionAsync, ConnectionAsync}; +use butane_core::db::ConnectionAsync; use butane_test_helper::*; +use butane_test_macros::butane_test; +#[butane_test(nomigrate)] async fn commit_empty_transaction(mut conn: ConnectionAsync) { assert!(!conn.is_closed()); @@ -10,8 +12,8 @@ async fn commit_empty_transaction(mut conn: ConnectionAsync) { // it is impossible to reuse the transaction after this. // i.e. already_consumed is unreachable. } -testall_no_migrate!(commit_empty_transaction); +#[butane_test(nomigrate)] async fn rollback_empty_transaction(mut conn: ConnectionAsync) { let tr = conn.transaction().await.unwrap(); @@ -19,8 +21,8 @@ async fn rollback_empty_transaction(mut conn: ConnectionAsync) { // it is impossible to reuse the transaction after this. // i.e. already_consumed is unreachable. } -testall_no_migrate!(rollback_empty_transaction); +#[butane_test(nomigrate)] async fn debug_transaction_before_consuming(mut conn: ConnectionAsync) { let backend_name = conn.backend_name(); @@ -34,4 +36,3 @@ async fn debug_transaction_before_consuming(mut conn: ConnectionAsync) { assert!(tr.commit().await.is_ok()); } -testall_no_migrate!(debug_transaction_before_consuming); diff --git a/butane_test_helper/Cargo.toml b/butane_test_helper/Cargo.toml index 9256a888..f569de0a 100644 --- a/butane_test_helper/Cargo.toml +++ b/butane_test_helper/Cargo.toml @@ -14,6 +14,7 @@ documentation = "https://docs.rs/butane/" [dependencies] block-id = "0.2" butane_core = { features = ["pg", "sqlite"], workspace = true } +env_logger.workspace = true libc = "0.2" log.workspace = true maybe-async-cfg.workspace = true diff --git a/butane_test_helper/src/lib.rs b/butane_test_helper/src/lib.rs index ac280e33..832a8021 100644 --- a/butane_test_helper/src/lib.rs +++ b/butane_test_helper/src/lib.rs @@ -3,12 +3,13 @@ #![deny(missing_docs)] use butane_core::db::{ - connect, connect_async, get_backend, pg, sqlite, Backend, BackendConnection, - BackendConnectionAsync, Connection, ConnectionAsync, ConnectionSpec, + connect, connect_async, get_backend, pg, pg::PgBackend, sqlite, sqlite::SQLiteBackend, Backend, + ConnectionSpec, }; use butane_core::migrations::{self, MemMigrations, Migration, Migrations, MigrationsMut}; use once_cell::sync::Lazy; +use std::future::Future; use std::io::{BufRead, BufReader, Read, Write}; use std::ops::Deref; use std::path::PathBuf; @@ -18,6 +19,103 @@ use std::sync::Mutex; use block_id::{Alphabet, BlockId}; use uuid::Uuid; +pub use butane_core::db::{BackendConnection, BackendConnectionAsync, Connection, ConnectionAsync}; +pub use maybe_async_cfg; + +/// Trait for running a test +#[allow(async_fn_in_trait)] // Not truly public, only used in butane for testing. +pub trait BackendTestInstance { + /// Run a synchronous test. + fn run_test_sync(test: impl FnOnce(Connection), migrate: bool); + /// Run an asynchronous test. + async fn run_test_async(test: impl FnOnce(ConnectionAsync) -> Fut, migrate: bool) + where + Fut: Future; +} + +/// Instance of a Postgres test. +#[derive(Default)] +pub struct PgTestInstance {} + +impl BackendTestInstance for PgTestInstance { + fn run_test_sync(test: impl FnOnce(Connection), migrate: bool) { + common_setup(); + let backend = PgBackend::new(); + let setup_data = pg_setup_sync(); + let connstr = setup_data.connstr; + log::info!("connecting to {}..", connstr); + let mut conn = backend + .connect(&connstr) + .expect("Could not connect backend"); + if migrate { + setup_db(&mut conn); + } + log::info!("running test on {}...", connstr); + test(conn); + } + async fn run_test_async(test: impl FnOnce(ConnectionAsync) -> Fut, migrate: bool) + where + Fut: Future, + { + common_setup(); + let backend = PgBackend::new(); + let setup_data = pg_setup().await; + let connstr = setup_data.connstr(); + log::info!("connecting to {}..", connstr); + let mut conn = backend + .connect_async(connstr) + .await + .expect("Could not connect pg backend"); + if migrate { + setup_db_async(&mut conn).await; + } + log::info!("running test on {}...", connstr); + test(conn).await; + } +} + +/// Instance of a SQLite test. +#[derive(Default)] +pub struct SQLiteTestInstance {} + +impl BackendTestInstance for SQLiteTestInstance { + fn run_test_sync(test: impl FnOnce(Connection), migrate: bool) { + common_setup(); + log::info!("connecting to sqlite memory database.."); + let mut conn = SQLiteBackend::new() + .connect(":memory:") + .expect("Could not connect sqlite backend"); + if migrate { + setup_db(&mut conn); + } + log::info!("running sqlite test"); + test(conn); + } + async fn run_test_async(test: impl FnOnce(ConnectionAsync) -> Fut, migrate: bool) + where + Fut: Future, + { + common_setup(); + log::info!("connecting to sqlite memory database..."); + let mut conn = SQLiteBackend::new() + .connect_async(":memory:") + .await + .expect("Could not connect sqlite backend"); + if migrate { + setup_db_async(&mut conn).await; + } + log::info!("running sqlite test"); + test(conn).await; + } +} + +/// Used with `run_test` and `run_test_async`. Result of a backend-specific setup function. Provides a connection string, and also passed to the backend-specific teardown function +pub trait SetupData { + /// Return the connection string to use when establishing a + /// database connection. + fn connstr(&self) -> &str; +} + /// Create a postgres [`Connection`]. pub fn pg_connection() -> (Connection, PgSetupData) { let backend = get_backend(pg::BACKEND_NAME).unwrap(); @@ -65,6 +163,11 @@ pub struct PgSetupData { /// Connection string pub connstr: String, } +impl SetupData for PgSetupData { + fn connstr(&self) -> &str { + &self.connstr + } +} /// Create and start a temporary postgres server instance. pub fn create_tmp_server() -> PgServerState { @@ -165,7 +268,7 @@ pub fn pg_setup_sync() -> PgSetupData { let new_dbname = format!("butane_test_{}", Uuid::new_v4().simple()); log::info!("new db is `{}`", &new_dbname); - let mut conn = connect(&ConnectionSpec::new("pg", &connstr)).unwrap(); + let conn = connect(&ConnectionSpec::new("pg", &connstr)).unwrap(); log::debug!("closed is {}", BackendConnection::is_closed(&conn)); conn.execute(format!("CREATE DATABASE {new_dbname};")) .unwrap(); @@ -192,7 +295,7 @@ pub async fn pg_setup() -> PgSetupData { let new_dbname = format!("butane_test_{}", Uuid::new_v4().simple()); log::info!("new db is `{}`", &new_dbname); - let mut conn = connect_async(&ConnectionSpec::new("pg", &connstr)) + let conn = connect_async(&ConnectionSpec::new("pg", &connstr)) .await .unwrap(); log::debug!( @@ -271,29 +374,68 @@ pub fn sqlite_connspec() -> ConnectionSpec { ConnectionSpec::new(sqlite::BACKEND_NAME, ":memory:") } +/// Concrete [SetupData] for SQLite. +pub struct SQLiteSetupData {} + +impl SetupData for SQLiteSetupData { + fn connstr(&self) -> &str { + ":memory:" + } +} + /// Setup the test sqlite database. -pub async fn sqlite_setup() {} +pub async fn sqlite_setup() -> SQLiteSetupData { + SQLiteSetupData {} +} /// Tear down the test sqlite database. -pub fn sqlite_teardown(_: ()) {} +pub fn sqlite_teardown(_: SQLiteSetupData) {} + +fn common_setup() { + env_logger::try_init().ok(); +} + +/// Run a test function with a wrapper to set up and tear down the connection +pub async fn run_test_async( + backend_name: &str, + setup: impl FnOnce() -> Fut, + teardown: impl FnOnce(T), + migrate: bool, + test: impl FnOnce(ConnectionAsync) -> Fut2, +) where + T: SetupData, + Fut: Future, + Fut2: Future, +{ + env_logger::try_init().ok(); + let backend = get_backend(backend_name).expect("Could not find backend"); + let setup_data = setup().await; + let connstr = setup_data.connstr(); + log::info!("connecting to {}..", connstr); + let mut conn = backend + .connect_async(connstr) + .await + .expect("Could not connect backend"); + if migrate { + setup_db_async(&mut conn).await; + } + log::info!("running test on {}...", connstr); + test(conn).await; + teardown(setup_data); +} /// Wrap `$fname` in a `#[test]` with a `Connection` to `$connstr`. #[macro_export] macro_rules! maketest { - ($fname:ident, $backend:expr, $connstr:expr, $dataname:ident, $migrate:expr) => { + ($fname:ident, $backend:expr, $migrate:expr) => { paste::item! { #[tokio::test] pub async fn [<$fname _ $backend>]() { - env_logger::try_init().ok(); - let backend = butane_core::db::get_backend(&stringify!($backend)).expect("Could not find backend"); - let $dataname = butane_test_helper::[<$backend _setup>]().await; - log::info!("connecting to {}..", &$connstr); - let mut conn = backend.connect_async(&$connstr).await.expect("Could not connect backend"); - if $migrate { - butane_test_helper::setup_db_async(&mut conn).await; - } - log::info!("running test on {}..", &$connstr); - $fname(conn).await; - butane_test_helper::[<$backend _teardown>]($dataname); + use butane_test_helper::*; + match stringify!($backend) { + "pg" => PgTestInstance::run_test_async($fname, $migrate).await, + "sqlite" => SQLiteTestInstance::run_test_async($fname, $migrate).await, + _ => panic!("Unknown backend $backend") + }; } } }; @@ -303,46 +445,6 @@ macro_rules! maketest { #[macro_export] macro_rules! maketest_pg { ($fname:ident, $migrate:expr) => { - maketest!( - $fname, - pg, - &butane_test_helper::pg_connstr(&setup_data), - setup_data, - $migrate - ); - }; -} - -/// Create a sqlite and postgres `#[test]` that each invoke `$fname` with a [`Connection`] containing the schema. -#[macro_export] -macro_rules! testall { - ($fname:ident) => { - cfg_if::cfg_if! { - if #[cfg(feature = "sqlite")] { - maketest!($fname, sqlite, &format!(":memory:"), setup_data, true); - } - } - cfg_if::cfg_if! { - if #[cfg(feature = "pg")] { - maketest_pg!($fname, true); - } - } - }; -} - -/// Create a sqlite and postgres `#[test]` that each invoke `$fname` with a [`Connection`] with no schema. -#[macro_export] -macro_rules! testall_no_migrate { - ($fname:ident) => { - cfg_if::cfg_if! { - if #[cfg(feature = "sqlite")] { - maketest!($fname, sqlite, &format!(":memory:"), setup_data, false); - } - } - cfg_if::cfg_if! { - if #[cfg(feature = "pg")] { - maketest_pg!($fname, false); - } - } + maketest!($fname, pg, $migrate); }; } diff --git a/butane_test_macros/Cargo.toml b/butane_test_macros/Cargo.toml new file mode 100644 index 00000000..6e7158be --- /dev/null +++ b/butane_test_macros/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "butane_test_macros" +version.workspace = true +authors = ["James Oakley "] +edition.workspace = true +description = "Macros for Butane tests." +publish = false +license.workspace = true +repository.workspace = true + + +[dependencies] +proc-macro2 = { workspace = true } +quote = { workspace = true } +syn = { workspace = true, features=["parsing"] } + +[lib] +proc-macro = true diff --git a/butane_test_macros/src/lib.rs b/butane_test_macros/src/lib.rs new file mode 100644 index 00000000..449993c4 --- /dev/null +++ b/butane_test_macros/src/lib.rs @@ -0,0 +1,231 @@ +//! Macros for butane tests + +use proc_macro::TokenStream; +use proc_macro2::Span; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::{ext::IdentExt, parse_macro_input, punctuated::Punctuated, Ident, ItemFn, Stmt, Token}; + +/// Create a sqlite and postgres `#[test]` that each invoke `$fname` with a `Connection` with no schema. +#[proc_macro_attribute] +pub fn butane_test(args: TokenStream, input: TokenStream) -> TokenStream { + let input: TokenStream2 = input.into(); + let func: ItemFn = syn::parse2(input.clone()).unwrap(); + let fname = func.sig.ident.to_string(); + + // Handle arguments + let options: Vec = + parse_macro_input!(args with Punctuated::::parse_terminated) + .into_iter() + .collect(); + let include_sync = !options.contains(&TestOption::Async); + let include_async = !options.contains(&TestOption::Sync); + let migrate = !options.contains(&TestOption::NoMigrate); + + let mut func_sync = func.clone(); + + // Using butane_core rather than butane::prelude because the butane_test macro is used for butane_core tests too + let sync_prelude: Stmts = syn::parse2(quote!( + use butane_core::DataObject; + use butane_core::DataResult; + use butane_core::db::BackendConnection; + use butane_core::fkey::ForeignKeyOpSync; + use butane_core::many::ManyOpSync; + use butane_core::query::QueryOpSync; + use butane_core::DataObjectOpSync; + )) + .unwrap(); + + func_sync.block.stmts = sync_prelude + .into_iter() + .chain(func_sync.block.stmts) + .collect(); + + let async_prelude: Stmts = syn::parse2(quote!( + use butane_core::DataObject; + use butane_core::DataResult; + use butane_core::db::BackendConnectionAsync; + use butane_core::fkey::ForeignKeyOpAsync; + use butane_core::many::ManyOpAsync; + use butane_core::query::QueryOpAsync; + use butane_core::DataObjectOpAsync; + )) + .unwrap(); + + let mut func_async = func; + func_async.block.stmts = async_prelude + .into_iter() + .chain(func_async.block.stmts) + .collect(); + + let mut funcs = Vec::::new(); + if include_sync { + funcs.push(quote!( + #[maybe_async_cfg::maybe(sync(),idents(ConnectionAsync(sync="Connection"), find_async(sync="find"), setup_blog(sync="setup_blog_sync"), create_tag(sync="create_tag_sync")))] + #func_sync + )); + } + if include_async { + funcs.push(quote!( + #[maybe_async_cfg::maybe(async())] + #func_async + )); + } + + let mut backends: Vec<(&'static str, &'static str)> = Vec::new(); + backends.push(("pg", "PgTestInstance")); + if !options.contains(&TestOption::PgOnly) { + backends.push(("sqlite", "SQLiteTestInstance")); + } + + let tests = backends + .into_iter() + .map(|b| make_tests(&fname, b.0, b.1, include_sync, include_async, migrate)); + + quote! { + #(#funcs)* + #(#tests)* + } + .into() +} + +// Make both sync and async tests +fn make_tests( + fname_base: &str, + backend_name: &str, + instance_name: &str, + include_sync: bool, + include_async: bool, + migrate: bool, +) -> TokenStream2 { + if include_sync { + let mut tstream = make_sync_test(fname_base, backend_name, instance_name, migrate); + if include_async { + tstream.extend([make_async_test( + fname_base, + backend_name, + instance_name, + migrate, + )]); + } + tstream + } else if include_async { + make_async_test(fname_base, backend_name, instance_name, migrate) + } else { + panic!("Either sync or async must be supported") + } +} + +fn make_async_test( + fname_base: &str, + backend_name: &str, + instance_name: &str, + migrate: bool, +) -> TokenStream2 { + let fname_full = make_ident(&format!("{fname_base}_async_{backend_name}")); + let fname_async = make_ident(&format!("{fname_base}_async")); + let instance_ident = make_ident(instance_name); + quote! { + cfg_if::cfg_if! { + if #[cfg(feature = #backend_name)] { + #[tokio::test] + pub async fn #fname_full () { + use butane_test_helper::*; + #instance_ident::run_test_async(#fname_async, #migrate).await; + } + } + } + } +} + +fn make_sync_test( + fname_base: &str, + backend_name: &str, + instance_name: &str, + migrate: bool, +) -> TokenStream2 { + let fname_full = Ident::new( + &format!("{fname_base}_sync_{backend_name}"), + Span::call_site(), + ); + let fname_sync = Ident::new(&format!("{fname_base}_sync"), Span::call_site()); + let instance_ident = Ident::new(instance_name, Span::call_site()); + quote! { + cfg_if::cfg_if! { + if #[cfg(feature = #backend_name)] { + #[test] + pub fn #fname_full () { + use butane_test_helper::*; + #instance_ident::run_test_sync(#fname_sync, #migrate); + } + } + } + } +} + +fn make_ident(name: &str) -> Ident { + Ident::new(name, Span::call_site()) +} + +/// Options for butane_test +#[derive(PartialEq, Eq)] +enum TestOption { + Sync, + Async, + NoMigrate, + PgOnly, +} + +impl Parse for TestOption { + fn parse(input: ParseStream) -> syn::Result { + let lookahead = input.lookahead1(); + if lookahead.peek(::peek_any) { + let name: Ident = input.call(IdentExt::parse_any)?; + if name == "async" { + Ok(TestOption::Async) + } else if name == "sync" { + Ok(TestOption::Sync) + } else if name == "nomigrate" { + Ok(TestOption::NoMigrate) + } else if name == "pg" { + Ok(TestOption::PgOnly) + } else { + Err(syn::Error::new( + name.span(), + "Unknown option for butane_test", + )) + } + } else { + Err(lookahead.error()) + } + } +} + +struct Stmts { + stmts: Vec, +} + +impl Parse for Stmts { + fn parse(input: ParseStream) -> syn::Result { + let mut stmts = Vec::new(); + while !input.is_empty() { + stmts.push(input.parse()?); + } + Ok(Self { stmts }) + } +} + +impl From for Vec { + fn from(stmts: Stmts) -> Vec { + stmts.stmts + } +} + +impl IntoIterator for Stmts { + type Item = Stmt; + type IntoIter = std::vec::IntoIter; + fn into_iter(self) -> Self::IntoIter { + self.stmts.into_iter() + } +} diff --git a/examples/getting_started/Cargo.toml b/examples/getting_started/Cargo.toml index da7ace86..7621719b 100644 --- a/examples/getting_started/Cargo.toml +++ b/examples/getting_started/Cargo.toml @@ -34,6 +34,7 @@ butane.workspace = true butane_cli.workspace = true butane_core.workspace = true butane_test_helper.workspace = true +butane_test_macros.workspace = true cfg-if.workspace = true env_logger.workspace = true log.workspace = true diff --git a/examples/getting_started/src/models.rs b/examples/getting_started/src/models.rs index c5655bbf..29611951 100644 --- a/examples/getting_started/src/models.rs +++ b/examples/getting_started/src/models.rs @@ -1,6 +1,5 @@ //! Models for the getting_started example. -use butane::prelude::*; use butane::AutoPk; use butane::{model, ForeignKey, Many}; diff --git a/examples/getting_started/tests/unmigrate.rs b/examples/getting_started/tests/unmigrate.rs index 35b06f16..45a586aa 100644 --- a/examples/getting_started/tests/unmigrate.rs +++ b/examples/getting_started/tests/unmigrate.rs @@ -1,30 +1,27 @@ -// todo re-enable this when we have a sync version of the test machinery available - -/* use butane::db::{BackendConnection, Connection}; use butane::migrations::Migrations; -use butane::DataObjectOpAsync; +use butane::DataObjectOpSync; use butane_test_helper::*; - +use butane_test_macros::butane_test; use getting_started::models::{Blog, Post, Tag}; -async fn create_tag(connection: &Connection, name: &str) -> Tag { +fn create_tag(connection: &Connection, name: &str) -> Tag { let mut tag = Tag::new(name); - tag.save(connection).await.unwrap(); + tag.save(connection).unwrap(); tag } -async fn insert_data(connection: &Connection) { +fn insert_data(connection: &Connection) { if connection.backend_name() == "sqlite" { // https://github.com/Electron100/butane/issues/226 return; } let mut cats_blog = Blog::new("Cats"); - cats_blog.save(connection).await.unwrap(); + cats_blog.save(connection).unwrap(); - let tag_asia = create_tag(connection, "asia").await; - let tag_danger = create_tag(connection, "danger").await; + let tag_asia = create_tag(connection, "asia"); + let tag_danger = create_tag(connection, "danger"); let mut post = Post::new( &cats_blog, @@ -35,20 +32,19 @@ async fn insert_data(connection: &Connection) { post.likes = 4; post.tags.add(&tag_danger).unwrap(); post.tags.add(&tag_asia).unwrap(); - post.save(connection).await.unwrap(); + post.save(connection).unwrap(); } -async fn migrate_and_unmigrate(mut connection: Connection) { +#[butane_test(sync, nomigrate)] +fn migrate_and_unmigrate(mut connection: Connection) { // Migrate forward. let base_dir = std::path::PathBuf::from(".butane"); let migrations = butane_cli::get_migrations(&base_dir).unwrap(); - migrations.migrate(&mut connection).await.unwrap(); + migrations.migrate(&mut connection).unwrap(); - insert_data(&connection).await; + insert_data(&connection); // Undo migrations. - migrations.unmigrate(&mut connection).await.unwrap(); + migrations.unmigrate(&mut connection).unwrap(); } -testall_no_migrate!(migrate_and_unmigrate); -*/ diff --git a/examples/getting_started_async/Cargo.toml b/examples/getting_started_async/Cargo.toml index 8fe7e801..de59c2a8 100644 --- a/examples/getting_started_async/Cargo.toml +++ b/examples/getting_started_async/Cargo.toml @@ -43,6 +43,7 @@ tokio = { workspace = true, features = ["macros"] } butane_cli.workspace = true butane_core.workspace = true butane_test_helper.workspace = true +butane_test_macros.workspace = true cfg-if.workspace = true env_logger.workspace = true log.workspace = true diff --git a/examples/getting_started_async/tests/rollback.rs b/examples/getting_started_async/tests/rollback.rs index bea1190e..043460af 100644 --- a/examples/getting_started_async/tests/rollback.rs +++ b/examples/getting_started_async/tests/rollback.rs @@ -2,6 +2,7 @@ use butane::db::{BackendConnection, Connection}; use butane::migrations::Migrations; use butane::DataObjectOpAsync; use butane_test_helper::*; +use butane_test_macros::butane_test; use getting_started::models::{Blog, Post, Tag}; @@ -34,6 +35,7 @@ async fn insert_data(connection: &Connection) { post.save(connection).await.unwrap(); } +#[butane_test] async fn migrate_and_unmigrate(mut connection: Connection) { // Migrate forward. let base_dir = std::path::PathBuf::from(".butane"); @@ -46,4 +48,3 @@ async fn migrate_and_unmigrate(mut connection: Connection) { // Undo migrations. migrations.unmigrate(&mut connection).await.unwrap(); } -testall_no_migrate!(migrate_and_unmigrate); diff --git a/examples/newtype/Cargo.toml b/examples/newtype/Cargo.toml index cbc7c371..cf1e986f 100644 --- a/examples/newtype/Cargo.toml +++ b/examples/newtype/Cargo.toml @@ -26,6 +26,7 @@ uuid = { workspace = true, features = ["serde", "v4"] } butane_cli.workspace = true butane_core.workspace = true butane_test_helper.workspace = true +butane_test_macros.workspace = true cfg-if.workspace = true env_logger.workspace = true log.workspace = true diff --git a/examples/newtype/tests/unmigrate.rs b/examples/newtype/tests/unmigrate.rs index 47f51769..189dcb2c 100644 --- a/examples/newtype/tests/unmigrate.rs +++ b/examples/newtype/tests/unmigrate.rs @@ -1,11 +1,20 @@ -use butane::db::{BackendConnectionAsync, ConnectionAsync}; +use butane::db::{BackendConnectionAsync, Connection, ConnectionAsync}; use butane::migrations::Migrations; -use butane::DataObjectOpAsync; use butane_test_helper::*; +use butane_test_macros::butane_test; use newtype::models::{Blog, Post, Tags}; -async fn insert_data(connection: &ConnectionAsync) { +#[maybe_async_cfg::maybe( + sync(), + async(), + idents( + Connection(sync = "Connection", async = "ConnectionAsync"), + DataObjectOp(sync = "DataObjectOpSync", async = "DataObjectOpAsync") + ) +)] +async fn insert_data(connection: &Connection) { + use butane::DataObjectOp; if connection.backend_name() == "sqlite" { // https://github.com/Electron100/butane/issues/226 return; @@ -27,16 +36,30 @@ async fn insert_data(connection: &ConnectionAsync) { post.save(connection).await.unwrap(); } -async fn migrate_and_unmigrate(mut connection: ConnectionAsync) { +#[butane_test(async, nomigrate)] +async fn migrate_and_unmigrate_async(mut connection: ConnectionAsync) { // Migrate forward. let base_dir = std::path::PathBuf::from(".butane"); let migrations = butane_cli::get_migrations(&base_dir).unwrap(); migrations.migrate_async(&mut connection).await.unwrap(); - insert_data(&connection).await; + insert_data_async(&connection).await; // Undo migrations. migrations.unmigrate_async(&mut connection).await.unwrap(); } -testall_no_migrate!(migrate_and_unmigrate); + +#[butane_test(sync, nomigrate)] +fn migrate_and_unmigrate_sync(mut connection: Connection) { + // Migrate forward. + let base_dir = std::path::PathBuf::from(".butane"); + let migrations = butane_cli::get_migrations(&base_dir).unwrap(); + + migrations.migrate(&mut connection).unwrap(); + + insert_data_sync(&connection); + + // Undo migrations. + migrations.unmigrate(&mut connection).unwrap(); +} From 780cedd50545d269714ba2162e96888e5daeb0a1 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Thu, 10 Oct 2024 21:35:21 -0400 Subject: [PATCH 53/78] Update checklist --- async_checklist.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/async_checklist.md b/async_checklist.md index afb4eeec..82734e91 100644 --- a/async_checklist.md +++ b/async_checklist.md @@ -2,11 +2,12 @@ * [x] Tests should run against sync and async * [ ] Establish soundness for unsafe sections of AsyncAdapter * [ ] Consider publishing `AsyncAdapter` into its own crate -* [ ] Ensure Postgres works in sync +* [x] Ensure Postgres works in sync * [x] Re-enable R2D2 for sync -* [x] Integrate deadpool or bb8 for async connection poll +* [ ] Integrate deadpool or bb8 for async connection poll * [ ] Fix `#[async_trait(?Send)]` to set up Send bound again as it's required for e.g. `tokio::spawn` * [ ] Separate sync and async examples * [ ] Should async_adapter be under a separate feature? Do we need it for migrations? * [x] Ensure sqlite works in async * [x] Fully support sync too. Using async should not be required +* [ ] Clean up miscellaneous TODOs From 76b2e5529ef344a13f087299ddf8a34b915a23c5 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Tue, 15 Oct 2024 21:50:02 -0400 Subject: [PATCH 54/78] async_trait: add back Send bound --- async_checklist.md | 2 +- butane/tests/basic.rs | 10 ++++++++ butane_core/src/db/adapter.rs | 8 +++---- butane_core/src/db/connmethods.rs | 10 +++++--- butane_core/src/db/dummy.rs | 4 ++-- butane_core/src/db/macros.rs | 2 +- butane_core/src/db/mod.rs | 26 ++++++++++++++------- butane_core/src/db/pg.rs | 6 ++--- butane_core/src/migrations/fsmigrations.rs | 1 - butane_core/src/migrations/memmigrations.rs | 1 - 10 files changed, 46 insertions(+), 24 deletions(-) diff --git a/async_checklist.md b/async_checklist.md index 82734e91..88d4b723 100644 --- a/async_checklist.md +++ b/async_checklist.md @@ -5,7 +5,7 @@ * [x] Ensure Postgres works in sync * [x] Re-enable R2D2 for sync * [ ] Integrate deadpool or bb8 for async connection poll -* [ ] Fix `#[async_trait(?Send)]` to set up Send bound again as it's required for e.g. `tokio::spawn` +* [x] Fix `#[async_trait(?Send)]` to set up Send bound again as it's required for e.g. `tokio::spawn` * [ ] Separate sync and async examples * [ ] Should async_adapter be under a separate feature? Do we need it for migrations? * [x] Ensure sqlite works in async diff --git a/butane/tests/basic.rs b/butane/tests/basic.rs index 0f9a2f89..17243b04 100644 --- a/butane/tests/basic.rs +++ b/butane/tests/basic.rs @@ -454,3 +454,13 @@ async fn save_upserts_by_default(conn: ConnectionAsync) { let retrieved = Foo::get(&conn, 1).await.unwrap(); assert_eq!(retrieved.bar, 43); } + +#[butane_test(async)] +async fn tokio_spawn(conn: ConnectionAsync) { + // This test exists mostly to make sure it compiles. Verifies that + // we can Send the futures from ConnectionMethodsAsync. + tokio::spawn(async move { + let mut foo = Foo::new(1); + foo.save(&conn).await.unwrap(); + }); +} diff --git a/butane_core/src/db/adapter.rs b/butane_core/src/db/adapter.rs index 3199fb61..e3a785fe 100644 --- a/butane_core/src/db/adapter.rs +++ b/butane_core/src/db/adapter.rs @@ -171,7 +171,7 @@ where } } unsafe impl Send for SyncSendPtrMut {} -unsafe impl Sync for SyncSendPtrMut {} +unsafe impl Sync for SyncSendPtrMut {} impl Debug for SyncSendPtrMut { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { @@ -259,7 +259,7 @@ impl Drop for AsyncAdapter { } } -#[async_trait(?Send)] +#[async_trait] impl ConnectionMethodsAsync for AsyncAdapter where T: ConnectionMethods + ?Sized, @@ -338,7 +338,7 @@ where } } -#[async_trait(?Send)] +#[async_trait] impl BackendConnectionAsync for AsyncAdapter where T: BackendConnection, @@ -383,7 +383,7 @@ where } } -#[async_trait(?Send)] +#[async_trait] impl BackendTransactionAsync<'c> for AsyncAdapter where T: BackendTransaction<'c> + ?Sized, diff --git a/butane_core/src/db/connmethods.rs b/butane_core/src/db/connmethods.rs index 4b748bc8..bb45a293 100644 --- a/butane_core/src/db/connmethods.rs +++ b/butane_core/src/db/connmethods.rs @@ -11,9 +11,13 @@ use crate::{Result, SqlType, SqlVal, SqlValRef}; /// to call these methods directly and will instead use methods on /// [DataObject][crate::DataObject] or the `query!` macro. This trait is /// implemented by both database connections and transactions. -#[maybe_async_cfg::maybe(sync(keep_self), async(self = "ConnectionMethodsAsync"))] -#[async_trait(?Send)] -pub trait ConnectionMethods { +#[maybe_async_cfg::maybe( + sync(keep_self), + async(self = "ConnectionMethodsAsync"), + idents(AsyncRequiresSync) +)] +#[async_trait] +pub trait ConnectionMethods: super::internal::AsyncRequiresSync { async fn execute(&self, sql: &str) -> Result<()>; async fn query<'c>( &'c self, diff --git a/butane_core/src/db/dummy.rs b/butane_core/src/db/dummy.rs index 00a5ac26..9087babf 100644 --- a/butane_core/src/db/dummy.rs +++ b/butane_core/src/db/dummy.rs @@ -41,7 +41,7 @@ impl DummyConnection { sync(), async() )] -#[async_trait(?Send)] +#[async_trait] impl ConnectionMethods for DummyConnection { async fn execute(&self, sql: &str) -> Result<()> { Err(Error::PoisonedConnection) @@ -110,7 +110,7 @@ impl ConnectionMethods for DummyConnection { sync(), async() )] -#[async_trait(?Send)] +#[async_trait] impl BackendConnection for DummyConnection { async fn transaction(&mut self) -> Result> { Err(Error::PoisonedConnection) diff --git a/butane_core/src/db/macros.rs b/butane_core/src/db/macros.rs index 2d7bbbc6..a2c26ca4 100644 --- a/butane_core/src/db/macros.rs +++ b/butane_core/src/db/macros.rs @@ -10,7 +10,7 @@ macro_rules! connection_method_wrapper { sync(keep_self), async() )] - #[async_trait::async_trait(?Send)] + #[async_trait::async_trait] impl ConnectionMethods for $ty { async fn execute(&self, sql: &str) -> Result<()> { ConnectionMethods::execute(self.wrapped_connection_methods()?, sql).await diff --git a/butane_core/src/db/mod.rs b/butane_core/src/db/mod.rs index 36f4c5ad..41e9f143 100644 --- a/butane_core/src/db/mod.rs +++ b/butane_core/src/db/mod.rs @@ -65,6 +65,16 @@ mod internal { pub trait AsyncRequiresSend: Send {} #[maybe_async_cfg::maybe(idents(AsyncRequiresSend), async())] impl AsyncRequiresSend for T {} + + #[maybe_async_cfg::maybe(sync())] + pub trait AsyncRequiresSync {} + #[maybe_async_cfg::maybe(idents(AsyncRequiresSync), sync())] + impl AsyncRequiresSync for T {} + + #[maybe_async_cfg::maybe(async())] + pub trait AsyncRequiresSync: Sync {} + #[maybe_async_cfg::maybe(idents(AsyncRequiresSync), async())] + impl AsyncRequiresSync for T {} } /// Database connection. @@ -77,7 +87,7 @@ mod internal { sync(self = "BackendConnection"), async(self = "BackendConnectionAsync") )] -#[async_trait(?Send)] +#[async_trait] pub trait BackendConnection: ConnectionMethods + Debug + Send { /// Begin a database transaction. The transaction object must be /// used in place of this connection until it is committed or aborted. @@ -101,7 +111,7 @@ pub trait BackendConnection: ConnectionMethods + Debug + Send { sync(), async() )] -#[async_trait(?Send)] +#[async_trait] impl BackendConnection for Box { async fn transaction(&mut self) -> Result { self.deref_mut().transaction().await @@ -126,7 +136,7 @@ impl BackendConnection for Box { sync(), async() )] -#[async_trait(?Send)] +#[async_trait] impl ConnectionMethods for Box { async fn execute(&self, sql: &str) -> Result<()> { self.deref().execute(sql).await @@ -274,7 +284,7 @@ impl ConnectionAsync { sync(keep_self), async() )] -#[async_trait(?Send)] +#[async_trait] impl BackendConnection for Connection { async fn transaction(&mut self) -> Result { self.conn.transaction().await @@ -296,7 +306,7 @@ connection_method_wrapper!(Connection); sync(keep_self), async() )] -#[async_trait(?Send)] +#[async_trait] pub trait BackendTransaction<'c>: ConnectionMethods + internal::AsyncRequiresSend + Debug { /// Commit the transaction Unfortunately because we use this as a /// trait object, we can't consume self. It should be understood @@ -364,7 +374,7 @@ connection_method_wrapper!(Transaction<'_>); sync(keep_self), async() )] -#[async_trait(?Send)] +#[async_trait] impl<'c> BackendTransaction<'c> for Transaction<'c> { async fn commit(&mut self) -> Result<()> { self.trans.commit().await @@ -386,7 +396,7 @@ impl<'c> BackendTransaction<'c> for Transaction<'c> { sync(), async() )] -#[async_trait(?Send)] +#[async_trait] impl<'c> BackendTransaction<'c> for Box + 'c> { async fn commit(&mut self) -> Result<()> { self.deref_mut().commit().await @@ -408,7 +418,7 @@ impl<'c> BackendTransaction<'c> for Box + 'c> { sync(), async() )] -#[async_trait(?Send)] +#[async_trait] impl<'bt> ConnectionMethods for Box + 'bt> { async fn execute(&self, sql: &str) -> Result<()> { self.deref().execute(sql).await diff --git a/butane_core/src/db/pg.rs b/butane_core/src/db/pg.rs index ec77be85..5bf141c4 100644 --- a/butane_core/src/db/pg.rs +++ b/butane_core/src/db/pg.rs @@ -108,7 +108,7 @@ impl PgConnectionLike for PgConnection { } } -#[async_trait(?Send)] +#[async_trait] impl BackendConnection for PgConnection { async fn transaction(&mut self) -> Result> { let trans: postgres::Transaction<'_> = self.client.transaction().await?; @@ -153,7 +153,7 @@ trait PgConnectionLike { fn client(&self) -> Result<&Self::Client>; } -#[async_trait(?Send)] +#[async_trait] impl ConnectionMethods for T where T: PgConnectionLike + std::marker::Sync, @@ -385,7 +385,7 @@ impl<'c> PgConnectionLike for PgTransaction<'c> { } } -#[async_trait(?Send)] +#[async_trait] impl<'c> BackendTransaction<'c> for PgTransaction<'c> { async fn commit(&mut self) -> Result<()> { match self.trans.take() { diff --git a/butane_core/src/migrations/fsmigrations.rs b/butane_core/src/migrations/fsmigrations.rs index 3bbe4cdf..0e070d83 100644 --- a/butane_core/src/migrations/fsmigrations.rs +++ b/butane_core/src/migrations/fsmigrations.rs @@ -447,7 +447,6 @@ impl Migrations for FsMigrations { } } -#[async_trait::async_trait(?Send)] impl MigrationsMut for FsMigrations { fn current(&mut self) -> &mut Self::M { &mut self.current diff --git a/butane_core/src/migrations/memmigrations.rs b/butane_core/src/migrations/memmigrations.rs index 56dc03f1..4583f68c 100644 --- a/butane_core/src/migrations/memmigrations.rs +++ b/butane_core/src/migrations/memmigrations.rs @@ -139,7 +139,6 @@ impl Migrations for MemMigrations { } } -#[async_trait::async_trait(?Send)] impl MigrationsMut for MemMigrations { fn current(&mut self) -> &mut Self::M { &mut self.current From b31e3dbba7731e51d84a86f215c501dca949dbaf Mon Sep 17 00:00:00 2001 From: James Oakley Date: Sun, 20 Oct 2024 15:21:15 -0400 Subject: [PATCH 55/78] Pluralize, Ops instead of Op --- butane/src/lib.rs | 24 +++++++++--------- butane/tests/common/blog.rs | 8 +++--- butane_core/src/codegen/dbobj.rs | 4 +-- butane_core/src/fkey.rs | 12 ++++----- butane_core/src/lib.rs | 10 ++++---- butane_core/src/many.rs | 14 +++++------ butane_core/src/query/mod.rs | 28 ++++++++++----------- butane_test_macros/src/lib.rs | 16 ++++++------ examples/getting_started/tests/unmigrate.rs | 2 +- examples/newtype/tests/unmigrate.rs | 4 +-- 10 files changed, 61 insertions(+), 61 deletions(-) diff --git a/butane/src/lib.rs b/butane/src/lib.rs index f34b13be..6c9ab7e4 100644 --- a/butane/src/lib.rs +++ b/butane/src/lib.rs @@ -9,7 +9,7 @@ pub use butane_codegen::{butane_type, dataresult, model, FieldType, PrimaryKeyType}; pub use butane_core::custom; pub use butane_core::fkey::ForeignKey; -pub use butane_core::many::{Many, ManyOpAsync, ManyOpSync}; +pub use butane_core::many::{Many, ManyOpsAsync, ManyOpsSync}; pub use butane_core::migrations; pub use butane_core::query; pub use butane_core::{ @@ -17,7 +17,7 @@ pub use butane_core::{ Result, SqlType, SqlVal, SqlValRef, ToSql, }; // todo put ops in a separate package? -pub use butane_core::{DataObjectOpAsync, DataObjectOpSync}; +pub use butane_core::{DataObjectOpsAsync, DataObjectOpsSync}; pub mod db { //! Database helpers @@ -163,7 +163,7 @@ macro_rules! colname { #[macro_export] macro_rules! find { ($dbobj:ident, $filter:expr, $conn:expr) => { - butane::query::QueryOpSync::load(butane::query!($dbobj, $filter).limit(1), $conn) + butane::query::QueryOpsSync::load(butane::query!($dbobj, $filter).limit(1), $conn) .and_then(|mut results| results.pop().ok_or(butane::Error::NoSuchObject)) }; } @@ -173,7 +173,7 @@ macro_rules! find { macro_rules! find_async { ($dbobj:ident, $filter:expr, $conn:expr) => { // todo sync version - butane::query::QueryOpAsync::load(butane::query!($dbobj, $filter).limit(1), $conn) + butane::query::QueryOpsAsync::load(butane::query!($dbobj, $filter).limit(1), $conn) .await .and_then(|mut results| results.pop().ok_or(butane::Error::NoSuchObject)) }; @@ -195,10 +195,10 @@ pub mod prelude { pub use super::prelude_common::*; pub use butane_core::db::BackendConnection; - pub use butane_core::fkey::ForeignKeyOpSync; - pub use butane_core::many::ManyOpSync; - pub use butane_core::query::QueryOpSync; - pub use butane_core::DataObjectOpSync; + pub use butane_core::fkey::ForeignKeyOpsSync; + pub use butane_core::many::ManyOpsSync; + pub use butane_core::query::QueryOpsSync; + pub use butane_core::DataObjectOpsSync; } pub mod prelude_async { @@ -208,10 +208,10 @@ pub mod prelude_async { pub use super::prelude_common::*; pub use butane_core::db::BackendConnectionAsync; - pub use butane_core::fkey::ForeignKeyOpAsync; - pub use butane_core::many::ManyOpAsync; - pub use butane_core::query::QueryOpAsync; - pub use butane_core::DataObjectOpAsync; + pub use butane_core::fkey::ForeignKeyOpsAsync; + pub use butane_core::many::ManyOpsAsync; + pub use butane_core::query::QueryOpsAsync; + pub use butane_core::DataObjectOpsAsync; } pub mod internal { diff --git a/butane/tests/common/blog.rs b/butane/tests/common/blog.rs index fc7a581d..56d0869e 100644 --- a/butane/tests/common/blog.rs +++ b/butane/tests/common/blog.rs @@ -107,12 +107,12 @@ impl Tag { sync(), async(keep_self), idents( - DataObjectOpAsync(async = "DataObjectOpAsync", sync = "DataObjectOpSync"), + DataObjectOpsAsync(async = "DataObjectOpsAsync", sync = "DataObjectOpsSync"), ConnectionAsync(async = "ConnectionAsync", sync = "Connection") ) )] pub async fn create_tag(conn: &ConnectionAsync, name: &str) -> Tag { - use butane::DataObjectOpAsync; + use butane::DataObjectOpsAsync; let mut tag = Tag::new(name); tag.save(conn).await.unwrap(); tag @@ -126,13 +126,13 @@ pub async fn create_tag(conn: &ConnectionAsync, name: &str) -> Tag { sync(), async(keep_self), idents( - DataObjectOp, + DataObjectOps, Connection(async = "ConnectionAsync", sync = "Connection"), create_tag(async = "create_tag", snake), ) )] pub async fn setup_blog(conn: &Connection) { - use butane::DataObjectOp; + use butane::DataObjectOps; let mut cats_blog = Blog::new(1, "Cats"); cats_blog.save(conn).await.unwrap(); let mut mountains_blog = Blog::new(2, "Mountains"); diff --git a/butane_core/src/codegen/dbobj.rs b/butane_core/src/codegen/dbobj.rs index 3716fef7..afe44e28 100644 --- a/butane_core/src/codegen/dbobj.rs +++ b/butane_core/src/codegen/dbobj.rs @@ -436,9 +436,9 @@ fn impl_many_save(ast_struct: &ItemStruct, config: &Config, is_async: bool) -> T quote!(<::PKType as butane::FieldType>::SQLTYPE); let save_with_conn = if is_async { - quote!(butane::ManyOpAsync::save(&mut self.#ident, conn).await?;) + quote!(butane::ManyOpsAsync::save(&mut self.#ident, conn).await?;) } else { - quote!(butane::ManyOpSync::save(&mut self.#ident, conn)?;) + quote!(butane::ManyOpsSync::save(&mut self.#ident, conn)?;) }; // Save needs to ensure_initialized diff --git a/butane_core/src/fkey.rs b/butane_core/src/fkey.rs index e8729bbb..37e16007 100644 --- a/butane_core/src/fkey.rs +++ b/butane_core/src/fkey.rs @@ -17,7 +17,7 @@ use crate::{ /// /// Initialize using `From` or `from_pk` /// -/// See [`ForeignKeyOpSync`] and [`ForeignKeyOpAsync`] for operations requiring a live database connection. +/// See [`ForeignKeyOpsSync`] and [`ForeignKeyOpsAsync`] for operations requiring a live database connection. /// /// # Examples /// ```ignore @@ -92,7 +92,7 @@ impl ForeignKey { sync(), async() )] -pub trait ForeignKeyOp { +pub trait ForeignKeyOps { /// Loads the value referred to by this foreign key from the /// database if necessary and returns a reference to it. async fn load<'a>(&'a self, conn: &impl ConnectionMethods) -> Result<&T> @@ -100,12 +100,12 @@ pub trait ForeignKeyOp { T: 'a; } -impl ForeignKeyOpAsync for ForeignKey { +impl ForeignKeyOpsAsync for ForeignKey { async fn load<'a>(&'a self, conn: &impl ConnectionMethodsAsync) -> Result<&T> where T: 'a, { - use crate::DataObjectOpAsync; + use crate::DataObjectOpsAsync; self.val .get_or_try_init(|| async { let pk = self.valpk.get().unwrap(); @@ -118,12 +118,12 @@ impl ForeignKeyOpAsync for ForeignKey { } } -impl ForeignKeyOpSync for ForeignKey { +impl ForeignKeyOpsSync for ForeignKey { fn load<'a>(&'a self, conn: &impl ConnectionMethods) -> Result<&T> where T: 'a, { - use crate::DataObjectOpSync; + use crate::DataObjectOpsSync; crate::sync::get_or_try_init_tokio_once_cell_sync(&self.val, || { let pk = self.valpk.get().unwrap(); T::get(conn, T::PKType::from_sql_ref(pk.as_ref())?).map(Box::new) diff --git a/butane_core/src/lib.rs b/butane_core/src/lib.rs index b9d1df45..dd7d3b96 100644 --- a/butane_core/src/lib.rs +++ b/butane_core/src/lib.rs @@ -118,12 +118,12 @@ pub trait DataObject: DataResult + internal::DataObjectInternal + Sy idents( ConnectionMethods(sync = "ConnectionMethods"), save_many_to_many(sync = "save_many_to_many_sync", async = "save_many_to_many_async"), - QueryOp, + QueryOps, ), sync(), async() )] -pub trait DataObjectOp { +pub trait DataObjectOps { /// Find this object in the database based on primary key. /// Returns `Error::NoSuchObject` if the primary key does not exist. async fn get(conn: &impl ConnectionMethods, id: impl ToSql) -> Result @@ -140,7 +140,7 @@ pub trait DataObjectOp { where Self: DataObject + Sized, { - use crate::query::QueryOp; + use crate::query::QueryOps; Ok(::query() .filter(query::BoolExpr::Eq( T::PKCOL, @@ -223,8 +223,8 @@ pub trait DataObjectOp { } } -impl DataObjectOpSync for T where T: DataObject {} -impl DataObjectOpAsync for T where T: DataObject {} +impl DataObjectOpsSync for T where T: DataObject {} +impl DataObjectOpsAsync for T where T: DataObject {} /// ASYNC TODO is this still necessary pub trait ModelTyped { diff --git a/butane_core/src/many.rs b/butane_core/src/many.rs index be7d47a2..65e2c110 100644 --- a/butane_core/src/many.rs +++ b/butane_core/src/many.rs @@ -22,7 +22,7 @@ fn default_oc() -> OnceCell> { /// U::PKType. Table name is T_foo_Many where foo is the name of /// the Many field /// -/// See [`ManyOpSync`] and [`ManyOpAsync`] for operations requiring a live database connection. +/// See [`ManyOpsSync`] and [`ManyOpsAsync`] for operations requiring a live database connection. // #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Many @@ -127,7 +127,7 @@ where } #[maybe_async_cfg::maybe( - idents(ConnectionMethods(sync, async = "ConnectionMethodsAsync"), QueryOp), + idents(ConnectionMethods(sync, async = "ConnectionMethodsAsync"), QueryOps), sync(), async() )] @@ -141,7 +141,7 @@ async fn load_query_uncached<'a, T>( where T: DataObject + 'a, { - use crate::query::QueryOp; + use crate::query::QueryOps; let mut vals: Vec = query.load(conn).await?; // Now add in the values for things not saved to the db yet if !many.new_values.is_empty() { @@ -195,7 +195,7 @@ where sync(), async() )] -pub trait ManyOp { +pub trait ManyOps { /// Used by macro-generated code. You do not need to call this directly. async fn save(&mut self, conn: &impl ConnectionMethods) -> Result<()>; @@ -225,15 +225,15 @@ pub trait ManyOp { #[maybe_async_cfg::maybe( idents( ConnectionMethods(sync = "ConnectionMethods"), - ManyOpInternal, - ManyOp, + ManyOpsInternal, + ManyOps, load_query(sync = "load_query_sync", async = "load_query_async"), ), keep_self, sync(), async() )] -impl ManyOp for Many { +impl ManyOps for Many { async fn save(&mut self, conn: &impl ConnectionMethods) -> Result<()> { let owner = self.owner.as_ref().ok_or(Error::NotInitialized)?; while !self.new_values.is_empty() { diff --git a/butane_core/src/query/mod.rs b/butane_core/src/query/mod.rs index 678c21df..31637245 100644 --- a/butane_core/src/query/mod.rs +++ b/butane_core/src/query/mod.rs @@ -117,7 +117,7 @@ impl Column { } /// Representation of a database query. -/// See [`QueryOpSync`] and [`QueryOpAsync`] for operations requiring a live database connection. +/// See [`QueryOpsSync`] and [`QueryOpsAsync`] for operations requiring a live database connection. #[derive(Debug)] pub struct Query { table: TblName, @@ -201,14 +201,14 @@ impl Clone for Query { mod private { use super::*; - /// Internal QueryOp helpers + /// Internal QueryOps helpers #[allow(async_fn_in_trait)] // Not truly a public trait #[maybe_async_cfg::maybe( idents(ConnectionMethods(sync = "ConnectionMethods")), sync(), async() )] - pub trait QueryOpInternal { + pub trait QueryOpsInternal { async fn fetch( self, conn: &impl ConnectionMethods, @@ -216,12 +216,12 @@ mod private { ) -> Result>; } #[maybe_async_cfg::maybe( - idents(ConnectionMethods(sync = "ConnectionMethods"), QueryOpInternal), + idents(ConnectionMethods(sync = "ConnectionMethods"), QueryOpsInternal), keep_self, sync(), async() )] - impl QueryOpInternal for Query { + impl QueryOpsInternal for Query { async fn fetch( self, conn: &impl ConnectionMethods, @@ -244,17 +244,17 @@ mod private { } } } -use private::QueryOpInternalAsync; -use private::QueryOpInternalSync; +use private::QueryOpsInternalAsync; +use private::QueryOpsInternalSync; /// [`Query`] operations which require a `Connection` #[allow(async_fn_in_trait)] // Not intended to be implemented outside Butane #[maybe_async_cfg::maybe( - idents(ConnectionMethods(sync = "ConnectionMethods"), QueryOpInternal), + idents(ConnectionMethods(sync = "ConnectionMethods"), QueryOpsInternal), sync(), async() )] -pub trait QueryOp: QueryOpInternal { +pub trait QueryOps: QueryOpsInternal { /// Executes the query against `conn` and returns the first result (if any). async fn load_first(self, conn: &impl ConnectionMethods) -> Result>; @@ -268,23 +268,23 @@ pub trait QueryOp: QueryOpInternal { #[maybe_async_cfg::maybe( idents( ConnectionMethods(sync = "ConnectionMethods"), - QueryOp, - QueryOpInternal + QueryOps, + QueryOpsInternal ), keep_self, sync(), async() )] -impl QueryOp for Query { +impl QueryOps for Query { async fn load_first(self, conn: &impl ConnectionMethods) -> Result> { - QueryOpInternal::fetch(self, conn, Some(1)) + QueryOpsInternal::fetch(self, conn, Some(1)) .await? .mapped(T::from_row) .nth(0) } async fn load(self, conn: &impl ConnectionMethods) -> Result> { let limit = self.limit.to_owned(); - QueryOpInternal::fetch(self, conn, limit) + QueryOpsInternal::fetch(self, conn, limit) .await? .mapped(T::from_row) .collect() diff --git a/butane_test_macros/src/lib.rs b/butane_test_macros/src/lib.rs index 449993c4..e0d30529 100644 --- a/butane_test_macros/src/lib.rs +++ b/butane_test_macros/src/lib.rs @@ -30,10 +30,10 @@ pub fn butane_test(args: TokenStream, input: TokenStream) -> TokenStream { use butane_core::DataObject; use butane_core::DataResult; use butane_core::db::BackendConnection; - use butane_core::fkey::ForeignKeyOpSync; - use butane_core::many::ManyOpSync; - use butane_core::query::QueryOpSync; - use butane_core::DataObjectOpSync; + use butane_core::fkey::ForeignKeyOpsSync; + use butane_core::many::ManyOpsSync; + use butane_core::query::QueryOpsSync; + use butane_core::DataObjectOpsSync; )) .unwrap(); @@ -46,10 +46,10 @@ pub fn butane_test(args: TokenStream, input: TokenStream) -> TokenStream { use butane_core::DataObject; use butane_core::DataResult; use butane_core::db::BackendConnectionAsync; - use butane_core::fkey::ForeignKeyOpAsync; - use butane_core::many::ManyOpAsync; - use butane_core::query::QueryOpAsync; - use butane_core::DataObjectOpAsync; + use butane_core::fkey::ForeignKeyOpsAsync; + use butane_core::many::ManyOpsAsync; + use butane_core::query::QueryOpsAsync; + use butane_core::DataObjectOpsAsync; )) .unwrap(); diff --git a/examples/getting_started/tests/unmigrate.rs b/examples/getting_started/tests/unmigrate.rs index 45a586aa..3e6bd5f7 100644 --- a/examples/getting_started/tests/unmigrate.rs +++ b/examples/getting_started/tests/unmigrate.rs @@ -1,6 +1,6 @@ use butane::db::{BackendConnection, Connection}; use butane::migrations::Migrations; -use butane::DataObjectOpSync; +use butane::DataObjectOpsSync; use butane_test_helper::*; use butane_test_macros::butane_test; diff --git a/examples/newtype/tests/unmigrate.rs b/examples/newtype/tests/unmigrate.rs index 189dcb2c..bc8ab827 100644 --- a/examples/newtype/tests/unmigrate.rs +++ b/examples/newtype/tests/unmigrate.rs @@ -10,11 +10,11 @@ use newtype::models::{Blog, Post, Tags}; async(), idents( Connection(sync = "Connection", async = "ConnectionAsync"), - DataObjectOp(sync = "DataObjectOpSync", async = "DataObjectOpAsync") + DataObjectOps(sync = "DataObjectOpsSync", async = "DataObjectOpsAsync") ) )] async fn insert_data(connection: &Connection) { - use butane::DataObjectOp; + use butane::DataObjectOps; if connection.backend_name() == "sqlite" { // https://github.com/Electron100/butane/issues/226 return; From 8c8414fff7075cf51350260125614a8693c88cfb Mon Sep 17 00:00:00 2001 From: James Oakley Date: Sun, 20 Oct 2024 18:09:04 -0400 Subject: [PATCH 56/78] Remove tokio dependency from ForeignKey and Many Previously the tokio dependency was present even under sync --- butane_core/src/fkey.rs | 33 ++++++++++++++-------------- butane_core/src/lib.rs | 2 +- butane_core/src/many.rs | 48 ++++++++++++++++------------------------- butane_core/src/sync.rs | 24 --------------------- butane_core/src/util.rs | 35 ++++++++++++++++++++++++++++++ 5 files changed, 71 insertions(+), 71 deletions(-) delete mode 100644 butane_core/src/sync.rs create mode 100644 butane_core/src/util.rs diff --git a/butane_core/src/fkey.rs b/butane_core/src/fkey.rs index e8729bbb..ef217fb3 100644 --- a/butane_core/src/fkey.rs +++ b/butane_core/src/fkey.rs @@ -1,8 +1,9 @@ //! Implementation of foreign key relationships between models. #![deny(missing_docs)] +use crate::util::{get_or_init_once_lock, get_or_init_once_lock_async}; use std::borrow::Cow; use std::fmt::Debug; -use tokio::sync::OnceCell; +use std::sync::OnceLock; #[cfg(feature = "fake")] use fake::{Dummy, Faker}; @@ -37,8 +38,9 @@ where { // At least one must be initialized (enforced internally by this // type), but both need not be - val: OnceCell>, - valpk: OnceCell, + // Using OnceLock instead of OnceCell because of Sync requirements when working with async. + val: OnceLock>, + valpk: OnceLock, } impl ForeignKey { /// Create a value from a reference to the primary key of the value @@ -68,8 +70,8 @@ impl ForeignKey { fn new_raw() -> Self { ForeignKey { - val: OnceCell::new(), - valpk: OnceCell::new(), + val: OnceLock::new(), + valpk: OnceLock::new(), } } @@ -106,15 +108,14 @@ impl ForeignKeyOpAsync for ForeignKey { T: 'a, { use crate::DataObjectOpAsync; - self.val - .get_or_try_init(|| async { - let pk = self.valpk.get().unwrap(); - T::get(conn, T::PKType::from_sql_ref(pk.as_ref())?) - .await - .map(Box::new) - }) - .await - .map(|v| v.as_ref()) + get_or_init_once_lock_async(&self.val, || async { + let pk = self.valpk.get().unwrap(); + T::get(conn, T::PKType::from_sql_ref(pk.as_ref())?) + .await + .map(Box::new) + }) + .await + .map(|v| v.as_ref()) } } @@ -124,7 +125,7 @@ impl ForeignKeyOpSync for ForeignKey { T: 'a, { use crate::DataObjectOpSync; - crate::sync::get_or_try_init_tokio_once_cell_sync(&self.val, || { + get_or_init_once_lock(&self.val, || { let pk = self.valpk.get().unwrap(); T::get(conn, T::PKType::from_sql_ref(pk.as_ref())?).map(Box::new) }) @@ -185,7 +186,7 @@ where fn from_sql_ref(valref: SqlValRef) -> Result { Ok(ForeignKey { valpk: SqlVal::from(valref).into(), - val: OnceCell::new(), + val: OnceLock::new(), }) } } diff --git a/butane_core/src/lib.rs b/butane_core/src/lib.rs index b9d1df45..c11b72b1 100644 --- a/butane_core/src/lib.rs +++ b/butane_core/src/lib.rs @@ -23,7 +23,7 @@ pub mod sqlval; pub mod uuid; mod autopk; -mod sync; +mod util; pub use autopk::AutoPk; use custom::SqlTypeCustom; diff --git a/butane_core/src/many.rs b/butane_core/src/many.rs index be7d47a2..bfedbc88 100644 --- a/butane_core/src/many.rs +++ b/butane_core/src/many.rs @@ -2,17 +2,18 @@ #![deny(missing_docs)] use crate::db::{Column, ConnectionMethods, ConnectionMethodsAsync}; use crate::query::{BoolExpr, Expr, OrderDirection, Query}; +use crate::util::{get_or_init_once_lock, get_or_init_once_lock_async}; use crate::{sqlval::PrimaryKeyType, DataObject, Error, FieldType, Result, SqlType, SqlVal, ToSql}; use serde::{Deserialize, Serialize}; use std::borrow::Cow; -use tokio::sync::OnceCell; +use std::sync::OnceLock; #[cfg(feature = "fake")] use fake::{Dummy, Faker}; -fn default_oc() -> OnceCell> { +fn default_oc() -> OnceLock> { // Same as impl Default for once_cell::unsync::OnceCell - OnceCell::new() + OnceLock::new() } /// Used to implement a many-to-many relationship between models. @@ -38,7 +39,7 @@ where removed_values: Vec, #[serde(skip)] #[serde(default = "default_oc")] - all_values: OnceCell>, + all_values: OnceLock>, } impl Many where @@ -57,7 +58,7 @@ where owner_type: SqlType::Int, new_values: Vec::new(), removed_values: Vec::new(), - all_values: OnceCell::new(), + all_values: OnceLock::new(), } } @@ -69,7 +70,7 @@ where self.item_table = Cow::Borrowed(item_table); self.owner = Some(owner); self.owner_type = owner_type; - self.all_values = OnceCell::new(); + self.all_values = OnceLock::new(); } /// Adds a value. Returns Err(ValueNotSaved) if the @@ -82,7 +83,7 @@ where } // all_values is now out of date, so clear it - self.all_values = OnceCell::new(); + self.all_values = OnceLock::new(); self.new_values.push(new_val.pk().to_sql()); Ok(()) } @@ -90,7 +91,7 @@ where /// Removes a value. pub fn remove(&mut self, val: &T) { // all_values is now out of date, so clear it - self.all_values = OnceCell::new(); + self.all_values = OnceLock::new(); self.removed_values.push(val.pk().to_sql()) } @@ -157,37 +158,24 @@ where /// Loads the values referred to by this many relationship from a /// database query if necessary and returns a reference to them. -async fn load_query_async<'a, T>( +#[maybe_async_cfg::maybe( + idents(load_query_uncached(snake)), + sync(), + async(idents(get_or_init_once_lock(snake), ConnectionMethods)) +)] +async fn load_query<'a, T>( many: &'a Many, - conn: &impl ConnectionMethodsAsync, + conn: &impl ConnectionMethods, query: Query, ) -> Result> where T: DataObject + 'a, { - many.all_values - .get_or_try_init(|| load_query_uncached_async(many, conn, query)) + get_or_init_once_lock(&many.all_values, || load_query_uncached(many, conn, query)) .await .map(|v| v.iter()) } -/// Loads the values referred to by this many relationship from a -/// database query if necessary and returns a reference to them. -fn load_query_sync<'a, T>( - many: &'a Many, - conn: &impl ConnectionMethods, - query: Query, -) -> Result> -where - T: DataObject + 'a, -{ - crate::sync::get_or_try_init_tokio_once_cell_sync(&many.all_values, || { - // TODO it would be nice to avoid this clone - load_query_uncached_sync(many, conn, query.clone()) - }) - .map(|v| v.iter()) -} - /// [`Many`] operations which require a `Connection` #[allow(async_fn_in_trait)] // Not intended to be implemented outside Butane #[maybe_async_cfg::maybe( @@ -268,7 +256,7 @@ impl ManyOp for Many { self.new_values.clear(); self.removed_values.clear(); // all_values is now out of date, so clear it - self.all_values = OnceCell::new(); + self.all_values = OnceLock::new(); Ok(()) } diff --git a/butane_core/src/sync.rs b/butane_core/src/sync.rs deleted file mode 100644 index 4fc0e408..00000000 --- a/butane_core/src/sync.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::{Error, Result}; -use tokio::sync::OnceCell; - -/// Wrapper around tokio's OnceCell get_or_try_init method -/// This is a sync version which provides the same semantics with a spinlock, as simultaneous initialization should be very rare. -pub fn get_or_try_init_tokio_once_cell_sync(cell: &OnceCell, f: F) -> Result<&T> -where - F: Fn() -> Result, -{ - match cell.get() { - Some(val) => Ok(val), - None => { - loop { - match cell.set(f()?) { - Ok(()) => break, - Err(tokio::sync::SetError::AlreadyInitializedError(_)) => break, - Err(tokio::sync::SetError::InitializingError(_)) => continue, // spinlock - } - } - // Error should be impossible here, we should have already ensured init (or returned error). - cell.get().ok_or(Error::NotInitialized) - } - } -} diff --git a/butane_core/src/util.rs b/butane_core/src/util.rs new file mode 100644 index 00000000..3483aeb0 --- /dev/null +++ b/butane_core/src/util.rs @@ -0,0 +1,35 @@ +use crate::Result; +use std::sync::OnceLock; + +pub fn get_or_init_once_lock(cell: &OnceLock, f: impl FnOnce() -> Result) -> Result<&T> { + if let Some(val) = cell.get() { + return Ok(val); + } + let val = f()?; + let _ = cell.set(val); + match cell.get() { + Some(val) => Ok(val), + _ => panic!("Cell was already set, cannot be empty"), + } +} + +pub async fn get_or_init_once_lock_async( + cell: &OnceLock, + f: impl FnOnce() -> Fut, +) -> Result<&T> +where + Fut: std::future::Future>, +{ + if let Some(val) = cell.get() { + return Ok(val); + } + let val = f().await?; + // Note that theoretically this can block, which we shouldn't do + // under async, but the cases when we expect multiple async jobs + // to be operating on this are very rare (are they even allowed by the type system?). + let _ = cell.set(val); + match cell.get() { + Some(val) => Ok(val), + _ => panic!("Cell was already set, cannot be empty"), + } +} From f5cf8d9e82985508e5f73796d3c35f79aa449b7c Mon Sep 17 00:00:00 2001 From: James Oakley Date: Sat, 26 Oct 2024 10:48:41 -0400 Subject: [PATCH 57/78] Minor misc. work on async todos --- async_checklist.md | 10 +++++----- butane/src/lib.rs | 3 +-- butane_core/src/db/dummy.rs | 7 ++++++- butane_core/src/db/mod.rs | 2 +- butane_core/src/lib.rs | 6 ------ examples/newtype/src/lib.rs | 2 -- 6 files changed, 13 insertions(+), 17 deletions(-) diff --git a/async_checklist.md b/async_checklist.md index 88d4b723..08abb248 100644 --- a/async_checklist.md +++ b/async_checklist.md @@ -1,13 +1,13 @@ * [x] Clean up pattern for sync/async variants. Inconsistent between suffix and module * [x] Tests should run against sync and async -* [ ] Establish soundness for unsafe sections of AsyncAdapter -* [ ] Consider publishing `AsyncAdapter` into its own crate * [x] Ensure Postgres works in sync * [x] Re-enable R2D2 for sync -* [ ] Integrate deadpool or bb8 for async connection poll * [x] Fix `#[async_trait(?Send)]` to set up Send bound again as it's required for e.g. `tokio::spawn` -* [ ] Separate sync and async examples -* [ ] Should async_adapter be under a separate feature? Do we need it for migrations? +* [x] Separate sync and async examples * [x] Ensure sqlite works in async * [x] Fully support sync too. Using async should not be required * [ ] Clean up miscellaneous TODOs +* [ ] Establish soundness for unsafe sections of AsyncAdapter +* [ ] Consider publishing `AsyncAdapter` into its own crate +* [ ] Should async and/or async_adapter be under a separate feature? +* [ ] Integrate deadpool or bb8 for async connection pool diff --git a/butane/src/lib.rs b/butane/src/lib.rs index 6c9ab7e4..e9333268 100644 --- a/butane/src/lib.rs +++ b/butane/src/lib.rs @@ -16,7 +16,7 @@ pub use butane_core::{ AsPrimaryKey, AutoPk, DataObject, DataResult, Error, FieldType, FromSql, PrimaryKeyType, Result, SqlType, SqlVal, SqlValRef, ToSql, }; -// todo put ops in a separate package? + pub use butane_core::{DataObjectOpsAsync, DataObjectOpsSync}; pub mod db { @@ -172,7 +172,6 @@ macro_rules! find { #[macro_export] macro_rules! find_async { ($dbobj:ident, $filter:expr, $conn:expr) => { - // todo sync version butane::query::QueryOpsAsync::load(butane::query!($dbobj, $filter).limit(1), $conn) .await .and_then(|mut results| results.pop().ok_or(butane::Error::NoSuchObject)) diff --git a/butane_core/src/db/dummy.rs b/butane_core/src/db/dummy.rs index 9087babf..dc7a053b 100644 --- a/butane_core/src/db/dummy.rs +++ b/butane_core/src/db/dummy.rs @@ -1,5 +1,5 @@ +//! Provides a dummy backend which always fails and which is used as a return type in certain failure scenarios (see also [super::ConnectionAsync]'s `with_sync` method) #![allow(unused)] -// TODO needs module comment use async_trait::async_trait; @@ -11,6 +11,8 @@ use crate::{Error, Result, SqlVal, SqlValRef}; #[derive(Clone, Debug)] struct DummyBackend {} +/// Provides a backend implementation which fails all operations with [Error::PoisonedConnection]. +/// Exists so that it can be returned from the [BackendConnection] implementation of [DummyConnection]. #[async_trait] impl Backend for DummyBackend { fn name(&self) -> &'static str { @@ -27,6 +29,9 @@ impl Backend for DummyBackend { } } +/// Provides a connection implementation which fails all operations with [Error::PoisonedConnection]. [ConnectionAsync] provides a `with_sync` method which allows running a non-async function +/// which takes synchronous [Connection]. This is implemented using std::mem::swap to satisfy the borrow checker. The original async connection is replaced with a dummy one while the +/// sync operation is being run. #[derive(Clone, Debug)] pub(crate) struct DummyConnection {} impl DummyConnection { diff --git a/butane_core/src/db/mod.rs b/butane_core/src/db/mod.rs index 41e9f143..4e4031ec 100644 --- a/butane_core/src/db/mod.rs +++ b/butane_core/src/db/mod.rs @@ -240,7 +240,7 @@ impl Connection { /// Runs the provided function with a synchronous wrapper around this /// asynchronous connection. /// Because this relies on some (safe) memory gymnastics, - /// there is a small but nonzero risk that if tokio fails at + /// there is a small but nonzero risk that if certain tokio calls fail unexpectedly at /// the wrong place the the connection will be poisoned -- all subsequent calls /// to all methods will fail. #[maybe_async_cfg::only_if(key = "async")] diff --git a/butane_core/src/lib.rs b/butane_core/src/lib.rs index 6f5626a2..284031cd 100644 --- a/butane_core/src/lib.rs +++ b/butane_core/src/lib.rs @@ -226,12 +226,6 @@ pub trait DataObjectOps { impl DataObjectOpsSync for T where T: DataObject {} impl DataObjectOpsAsync for T where T: DataObject {} -/// ASYNC TODO is this still necessary -pub trait ModelTyped { - /// ASYNC TODO - type Model: DataObject; -} - /// Butane errors. #[allow(missing_docs)] #[derive(Debug, ThisError)] diff --git a/examples/newtype/src/lib.rs b/examples/newtype/src/lib.rs index 709a3ccf..6536f811 100644 --- a/examples/newtype/src/lib.rs +++ b/examples/newtype/src/lib.rs @@ -10,8 +10,6 @@ use butane::migrations::Migrations; use butane::prelude_async::*; use models::{Blog, Post}; -// todo this example doesn't necessarily need to be async - /// Load a [Connection]. pub async fn establish_connection() -> ConnectionAsync { let mut connection = From 7212248dbbd316d56316168aedec3f8a26687ff9 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Sat, 26 Oct 2024 11:27:54 -0400 Subject: [PATCH 58/78] Add README notes on breaking changes --- README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/README.md b/README.md index 347360b1..6d7be870 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,35 @@ enabled: you will want to enable `sqlite` and/or `pg`: straightforward API and eking out the smallest possible overhead, the API will win. +## Breaking Changes & Version Migrations +### 0.8 (not yet released) + +This is a major release which adds Async support. Effort has been made +to keep the sync experience as unchanged as possible. Async versions +of many types have been added, but the sync ones generally retain +their previous names. In order to allow sync and async code to look as +similar as possible for types and traits which do not otherwise need +separate sync and async variants, several "Ops" traits have been +introduced which contain methods split off from prior types and traits. + +For example, if `obj` is an instance of +[`DataObject`](https://docs.rs/butane/latest/butane/trait.DataObject.html), +then you may call `obj.save(conn)` (sync) or `obj.save(conn).await` +(async). The `save` method no longer lives on `DataObject`. Instead, +you must use either `butane::DataObjectOpsSync` or +`butane::DataObjectOpsAsync`. Which trait is in scope will determine +whether the `save` method is sync or async. + +The Ops traits are: +* `DataObjectOpsSync` / `DataObjectOpsAsync` (for use with [`DataObject`](https://docs.rs/butane/latest/butane/trait.DataObject.html)) +* `QueryOpsSync` / `QueryOpsSync` (for use with [`Query`](https://docs.rs/butane/latest/butane/query/struct.Query.html), less commonly needed directly if you use the [`query`](https://docs.rs/butane/latest/butane/macro.query.html) or [`filter`](https://docs.rs/butane/latest/butane/macro.filter.html) macros) +* `ForeignKeyOpsSync` / `ForeignKeyOpsAsync` (for use with [`ForeignKey`](https://docs.rs/butane/latest/butane/struct.ForeignKey.html)) +* `ManyOpsSync` / `ManyOpsAsync` (for use with [`Many`](https://docs.rs/butane/latest/butane/struct.Many.html)) + +### 0.7 +* Replace all occurrences of the `#[auto]` attribute (within a model) with the [`AutoPk`](https://docs.rs/butane/latest/butane/struct.AutoPk.html) type as a wrapper. +* The `ObjectState` type and the auto-created `state` field for each model have been removed. Delete all references to the `state` field. + ## Roadmap Butane is young. The following features are currently missing, but planned From 4225c423ddc5a4c48c358a8a9f2233c8b50e5aaf Mon Sep 17 00:00:00 2001 From: James Oakley Date: Sat, 26 Oct 2024 11:30:00 -0400 Subject: [PATCH 59/78] Another readme update --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6d7be870..ad18b142 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,9 @@ enabled: you will want to enable `sqlite` and/or `pg`: This is a major release which adds Async support. Effort has been made to keep the sync experience as unchanged as possible. Async versions of many types have been added, but the sync ones generally retain -their previous names. In order to allow sync and async code to look as +their previous names. + +In order to allow sync and async code to look as similar as possible for types and traits which do not otherwise need separate sync and async variants, several "Ops" traits have been introduced which contain methods split off from prior types and traits. From 708280a7cfba5939ffd685a5195b4c6589cfe8d8 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Sun, 27 Oct 2024 07:16:18 +0800 Subject: [PATCH 60/78] fix editorconfig-checker issues --- README.md | 8 +++++--- butane/tests/migration-tests.rs | 10 +++++----- butane_core/src/codegen/dbobj.rs | 10 ++++++++-- butane_core/src/db/adapter.rs | 8 ++++---- butane_core/src/db/dummy.rs | 11 +++++++---- butane_test_helper/src/lib.rs | 3 ++- butane_test_macros/src/lib.rs | 10 +++++++++- .../migrations/20201229_144636751_init/pg_up.sql | 2 +- 8 files changed, 41 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 0ce45d56..b3c9af2f 100644 --- a/README.md +++ b/README.md @@ -100,12 +100,12 @@ enabled: you will want to enable `sqlite` and/or `pg`: This is a major release which adds Async support. Effort has been made to keep the sync experience as unchanged as possible. Async versions of many types have been added, but the sync ones generally retain -their previous names. +their previous names. In order to allow sync and async code to look as similar as possible for types and traits which do not otherwise need separate sync and async variants, several "Ops" traits have been -introduced which contain methods split off from prior types and traits. +introduced which contain methods split off from prior types and traits. For example, if `obj` is an instance of [`DataObject`](https://docs.rs/butane/latest/butane/trait.DataObject.html), @@ -117,7 +117,9 @@ whether the `save` method is sync or async. The Ops traits are: * `DataObjectOpsSync` / `DataObjectOpsAsync` (for use with [`DataObject`](https://docs.rs/butane/latest/butane/trait.DataObject.html)) -* `QueryOpsSync` / `QueryOpsSync` (for use with [`Query`](https://docs.rs/butane/latest/butane/query/struct.Query.html), less commonly needed directly if you use the [`query`](https://docs.rs/butane/latest/butane/macro.query.html) or [`filter`](https://docs.rs/butane/latest/butane/macro.filter.html) macros) +* `QueryOpsSync` / `QueryOpsSync` (for use with [`Query`](https://docs.rs/butane/latest/butane/query/struct.Query.html), + less commonly needed directly if you use the [`query`](https://docs.rs/butane/latest/butane/macro.query.html) or + [`filter`](https://docs.rs/butane/latest/butane/macro.filter.html) macros) * `ForeignKeyOpsSync` / `ForeignKeyOpsAsync` (for use with [`ForeignKey`](https://docs.rs/butane/latest/butane/struct.ForeignKey.html)) * `ManyOpsSync` / `ManyOpsAsync` (for use with [`Many`](https://docs.rs/butane/latest/butane/struct.Many.html)) diff --git a/butane/tests/migration-tests.rs b/butane/tests/migration-tests.rs index 56f8731a..95d7a388 100644 --- a/butane/tests/migration-tests.rs +++ b/butane/tests/migration-tests.rs @@ -210,8 +210,8 @@ fn migration_add_field_with_default_sqlite() { "ALTER TABLE Foo ADD COLUMN baz INTEGER NOT NULL DEFAULT 42;", // See comments on migration_add_field_sqlite r#"CREATE TABLE Foo__butane_tmp (id INTEGER NOT NULL PRIMARY KEY,bar TEXT NOT NULL); - INSERT INTO Foo__butane_tmp SELECT id, bar FROM Foo; - DROP TABLE Foo;ALTER TABLE Foo__butane_tmp RENAME TO Foo;"#, + INSERT INTO Foo__butane_tmp SELECT id, bar FROM Foo; + DROP TABLE Foo;ALTER TABLE Foo__butane_tmp RENAME TO Foo;"#, ) } @@ -287,9 +287,9 @@ fn migration_add_and_remove_field_sqlite() { INSERT INTO Foo__butane_tmp SELECT id, baz FROM Foo; DROP TABLE Foo;ALTER TABLE Foo__butane_tmp RENAME TO Foo;"#, r#"ALTER TABLE Foo ADD COLUMN bar TEXT NOT NULL DEFAULT ''; - CREATE TABLE Foo__butane_tmp (id INTEGER NOT NULL PRIMARY KEY,bar TEXT NOT NULL); - INSERT INTO Foo__butane_tmp SELECT id, bar FROM Foo;DROP TABLE Foo; - ALTER TABLE Foo__butane_tmp RENAME TO Foo;"#, + CREATE TABLE Foo__butane_tmp (id INTEGER NOT NULL PRIMARY KEY,bar TEXT NOT NULL); + INSERT INTO Foo__butane_tmp SELECT id, bar FROM Foo;DROP TABLE Foo; + ALTER TABLE Foo__butane_tmp RENAME TO Foo;"#, ) } diff --git a/butane_core/src/codegen/dbobj.rs b/butane_core/src/codegen/dbobj.rs index afe44e28..5416b7fe 100644 --- a/butane_core/src/codegen/dbobj.rs +++ b/butane_core/src/codegen/dbobj.rs @@ -83,11 +83,17 @@ pub fn impl_dbobject(ast_struct: &ItemStruct, config: &Config) -> TokenStream2 { fn pk_mut(&mut self) -> &mut impl butane::PrimaryKeyType { &mut self.#pkident } - async fn save_many_to_many_async(&mut self, #conn_arg_name: &impl butane::db::ConnectionMethodsAsync) -> butane::Result<()> { + async fn save_many_to_many_async( + &mut self, + #conn_arg_name: &impl butane::db::ConnectionMethodsAsync, + ) -> butane::Result<()> { #many_save_async Ok(()) } - fn save_many_to_many_sync(&mut self, #conn_arg_name: &impl butane::db::ConnectionMethods) -> butane::Result<()> { + fn save_many_to_many_sync( + &mut self, + #conn_arg_name: &impl butane::db::ConnectionMethods, + ) -> butane::Result<()> { #many_save_sync Ok(()) } diff --git a/butane_core/src/db/adapter.rs b/butane_core/src/db/adapter.rs index e3a785fe..65bdc518 100644 --- a/butane_core/src/db/adapter.rs +++ b/butane_core/src/db/adapter.rs @@ -67,8 +67,8 @@ impl AsyncAdapterEnv { let static_func: Box = unsafe { std::mem::transmute(boxed_func) }; self.sender.send(Command::Func(static_func))?; - // https://stackoverflow.com/questions/52424449/is-there-a-way-to-express-same-generic-type-with-different-lifetime-bound - //https://docs.rs/crossbeam/0.8.2/crossbeam/fn.scope.html + // https://stackoverflow.com/questions/52424449/ + // https://docs.rs/crossbeam/0.8.2/crossbeam/fn.scope.html // TODO ensure soundness and document why rx.await? } @@ -94,8 +94,8 @@ impl AsyncAdapterEnv { let static_func: Box = unsafe { std::mem::transmute(boxed_func) }; self.sender.send(Command::Func(static_func))?; - // https://stackoverflow.com/questions/52424449/is-there-a-way-to-express-same-generic-type-with-different-lifetime-bound - //https://docs.rs/crossbeam/0.8.2/crossbeam/fn.scope.html + // https://stackoverflow.com/questions/52424449/ + // https://docs.rs/crossbeam/0.8.2/crossbeam/fn.scope.html // TODO ensure soundness and document why rx.await? } diff --git a/butane_core/src/db/dummy.rs b/butane_core/src/db/dummy.rs index dc7a053b..10a5905c 100644 --- a/butane_core/src/db/dummy.rs +++ b/butane_core/src/db/dummy.rs @@ -1,4 +1,5 @@ -//! Provides a dummy backend which always fails and which is used as a return type in certain failure scenarios (see also [super::ConnectionAsync]'s `with_sync` method) +//! Provides a dummy backend which always fails and which is used as a return type in certain failure scenarios +//! (see also [super::ConnectionAsync]'s `with_sync` method). #![allow(unused)] use async_trait::async_trait; @@ -29,9 +30,11 @@ impl Backend for DummyBackend { } } -/// Provides a connection implementation which fails all operations with [Error::PoisonedConnection]. [ConnectionAsync] provides a `with_sync` method which allows running a non-async function -/// which takes synchronous [Connection]. This is implemented using std::mem::swap to satisfy the borrow checker. The original async connection is replaced with a dummy one while the -/// sync operation is being run. +/// Provides a connection implementation which fails all operations with [Error::PoisonedConnection]. +/// +/// [ConnectionAsync] provides a `with_sync` method which allows running a non-async function +/// which takes synchronous [Connection]. This is implemented using std::mem::swap to satisfy the borrow checker. +/// The original async connection is replaced with a dummy one while the sync operation is being run. #[derive(Clone, Debug)] pub(crate) struct DummyConnection {} impl DummyConnection { diff --git a/butane_test_helper/src/lib.rs b/butane_test_helper/src/lib.rs index 832a8021..10fe1b44 100644 --- a/butane_test_helper/src/lib.rs +++ b/butane_test_helper/src/lib.rs @@ -109,7 +109,8 @@ impl BackendTestInstance for SQLiteTestInstance { } } -/// Used with `run_test` and `run_test_async`. Result of a backend-specific setup function. Provides a connection string, and also passed to the backend-specific teardown function +/// Used with `run_test` and `run_test_async`. Result of a backend-specific setup function. +/// Provides a connection string, and also passed to the backend-specific teardown function. pub trait SetupData { /// Return the connection string to use when establishing a /// database connection. diff --git a/butane_test_macros/src/lib.rs b/butane_test_macros/src/lib.rs index e0d30529..4fe56d3f 100644 --- a/butane_test_macros/src/lib.rs +++ b/butane_test_macros/src/lib.rs @@ -62,7 +62,15 @@ pub fn butane_test(args: TokenStream, input: TokenStream) -> TokenStream { let mut funcs = Vec::::new(); if include_sync { funcs.push(quote!( - #[maybe_async_cfg::maybe(sync(),idents(ConnectionAsync(sync="Connection"), find_async(sync="find"), setup_blog(sync="setup_blog_sync"), create_tag(sync="create_tag_sync")))] + #[maybe_async_cfg::maybe( + sync(), + idents( + ConnectionAsync(sync="Connection"), + find_async(sync="find"), + setup_blog(sync="setup_blog_sync"), + create_tag(sync="create_tag_sync"), + ) + )] #func_sync )); } diff --git a/examples/getting_started_async/.butane/migrations/20201229_144636751_init/pg_up.sql b/examples/getting_started_async/.butane/migrations/20201229_144636751_init/pg_up.sql index 00d311c8..536c949e 100644 --- a/examples/getting_started_async/.butane/migrations/20201229_144636751_init/pg_up.sql +++ b/examples/getting_started_async/.butane/migrations/20201229_144636751_init/pg_up.sql @@ -8,7 +8,7 @@ title TEXT NOT NULL, body TEXT NOT NULL, published BOOLEAN NOT NULL, blog BIGINT NOT NULL, -byline TEXT +byline TEXT ); CREATE TABLE Post_tags_Many ( owner INTEGER NOT NULL, From 74cfdd472a21df7c76464b91e6870ff655acff66 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Sat, 26 Oct 2024 21:19:23 -0400 Subject: [PATCH 61/78] Address new lints Note that elided_named_lifetimes is only present on nightly and not on stable, hence why we allow it explicitly when invoking nightly rustdoc rather than more generally. --- Makefile | 2 +- butane_core/src/fkey.rs | 6 +++--- butane_core/src/migrations/fsmigrations.rs | 6 ++---- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 2a7272c1..d88025cc 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ spellcheck : typos doclint : - RUSTDOCFLAGS="-D warnings" $(CARGO_NIGHTLY) doc --no-deps --all-features + RUSTDOCFLAGS="-D warnings" RUSTFLAGS="-A elided_named_lifetimes" $(CARGO_NIGHTLY) doc --no-deps --all-features doc : cd butane && $(CARGO_NIGHTLY) doc --no-deps --all-features diff --git a/butane_core/src/fkey.rs b/butane_core/src/fkey.rs index 84a9ac2f..21b63166 100644 --- a/butane_core/src/fkey.rs +++ b/butane_core/src/fkey.rs @@ -97,13 +97,13 @@ impl ForeignKey { pub trait ForeignKeyOps { /// Loads the value referred to by this foreign key from the /// database if necessary and returns a reference to it. - async fn load<'a>(&'a self, conn: &impl ConnectionMethods) -> Result<&T> + async fn load<'a>(&'a self, conn: &impl ConnectionMethods) -> Result<&'a T> where T: 'a; } impl ForeignKeyOpsAsync for ForeignKey { - async fn load<'a>(&'a self, conn: &impl ConnectionMethodsAsync) -> Result<&T> + async fn load<'a>(&'a self, conn: &impl ConnectionMethodsAsync) -> Result<&'a T> where T: 'a, { @@ -120,7 +120,7 @@ impl ForeignKeyOpsAsync for ForeignKey { } impl ForeignKeyOpsSync for ForeignKey { - fn load<'a>(&'a self, conn: &impl ConnectionMethods) -> Result<&T> + fn load<'a>(&'a self, conn: &impl ConnectionMethods) -> Result<&'a T> where T: 'a, { diff --git a/butane_core/src/migrations/fsmigrations.rs b/butane_core/src/migrations/fsmigrations.rs index 0e070d83..d8e00d6e 100644 --- a/butane_core/src/migrations/fsmigrations.rs +++ b/butane_core/src/migrations/fsmigrations.rs @@ -244,9 +244,8 @@ impl MigrationMut for FsMigration { let typefile = self.root.join(TYPES_FILENAME); let mut types: SqlTypeMap = match self.fs.read(&typefile) { - Ok(reader) => serde_json::from_reader(reader).map_err(|e| { + Ok(reader) => serde_json::from_reader(reader).inspect_err(|_| { eprintln!("failed to read types {typefile:?}"); - e })?, Err(_) => BTreeMap::new(), }; @@ -254,9 +253,8 @@ impl MigrationMut for FsMigration { self.write_contents( TYPES_FILENAME, serde_json::to_string(&types) - .map_err(|e| { + .inspect_err(|_| { eprintln!("failed to write types {typefile:?}"); - e })? .as_bytes(), )?; From 3fb8502df11ab2546765ac7067d4353a16ed70cd Mon Sep 17 00:00:00 2001 From: James Oakley Date: Sat, 26 Oct 2024 21:34:49 -0400 Subject: [PATCH 62/78] Use github version of maybe-async-cfg to avoid proc-macro-error Avoid the proc-macro-error dependency which fails cargo-deny due to https://rustsec.org/advisories/RUSTSEC-2024-0370 --- Cargo.lock | 50 ++++++++++++++++++++++++++++++-------------------- Cargo.toml | 4 +++- 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2076dac8..77143014 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1102,13 +1102,36 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "manyhow" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587" +dependencies = [ + "manyhow-macros", + "proc-macro2", + "quote", + "syn 1.0.109", + "syn 2.0.75", +] + +[[package]] +name = "manyhow-macros" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", +] + [[package]] name = "maybe-async-cfg" version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e083394889336bc66a4eaf1011ffbfa74893e910f902a9f271fa624c61e1b2" +source = "git+https://github.com/nvksv/maybe-async-cfg.git?rev=b35f2f42e9b12a25fc731376fe6cdcf41869b2da#b35f2f42e9b12a25fc731376fe6cdcf41869b2da" dependencies = [ - "proc-macro-error", + "manyhow", "proc-macro2", "pulldown-cmark", "quote", @@ -1439,27 +1462,14 @@ dependencies = [ ] [[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" +name = "proc-macro-utils" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071" dependencies = [ "proc-macro2", "quote", - "version_check", + "smallvec", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 7ff84221..00fc5aef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,9 @@ crossbeam-channel = "0.5" env_logger = "0.11" fake = "2.6" log = "0.4" -maybe-async-cfg = { version = "0.2" } +# Switch back to crates.io version once one is published containing +# the referenced commit. Anything newer than 0.2.4 should have it. +maybe-async-cfg = { git = "https://github.com/nvksv/maybe-async-cfg.git", rev = "b35f2f42e9b12a25fc731376fe6cdcf41869b2da" } nonempty = "0.10" once_cell = "1.5.2" paste = "1.0.11" From 6853af10ff11e8db80bf681e65ae313325780ee5 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Sat, 26 Oct 2024 21:44:55 -0400 Subject: [PATCH 63/78] Fix line length in Cargo.toml --- Cargo.toml | 9 ++++++--- Makefile | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 00fc5aef..cd45ca9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,9 +35,6 @@ crossbeam-channel = "0.5" env_logger = "0.11" fake = "2.6" log = "0.4" -# Switch back to crates.io version once one is published containing -# the referenced commit. Anything newer than 0.2.4 should have it. -maybe-async-cfg = { git = "https://github.com/nvksv/maybe-async-cfg.git", rev = "b35f2f42e9b12a25fc731376fe6cdcf41869b2da" } nonempty = "0.10" once_cell = "1.5.2" paste = "1.0.11" @@ -56,6 +53,12 @@ tokio = { version = "1"} tokio-test = { version = "0.4"} uuid = "1.2" +# Switch back to crates.io version once one is published containing +# the referenced commit. Anything newer than 0.2.4 should have it. +[workspace.dependencies.maybe-async-cfg] +git = "https://github.com/nvksv/maybe-async-cfg.git" +rev = "b35f2f42e9b12a25fc731376fe6cdcf41869b2da" + [workspace.metadata.release] allow-branch = ["master"] push = false diff --git a/Makefile b/Makefile index d88025cc..338752d8 100644 --- a/Makefile +++ b/Makefile @@ -31,6 +31,7 @@ fmt : check-fmt : $(CARGO_NIGHTLY) fmt --check + editorconfig-checker spellcheck : typos From 32370ef5348a6512b53ffe5085383e783a1dff33 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Sat, 26 Oct 2024 21:58:45 -0400 Subject: [PATCH 64/78] Self-review part 1 --- butane/Cargo.toml | 2 +- butane/src/lib.rs | 16 ++++++---------- butane/tests/common/blog.rs | 2 +- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/butane/Cargo.toml b/butane/Cargo.toml index c5e7b815..63794b2f 100644 --- a/butane/Cargo.toml +++ b/butane/Cargo.toml @@ -27,7 +27,7 @@ tls = ["butane_core/tls"] uuid = ["butane_codegen/uuid", "butane_core/uuid"] [dependencies] -async-trait = "0.1" +async-trait = { workspace = true } butane_codegen = { workspace = true } butane_core = { workspace = true } diff --git a/butane/src/lib.rs b/butane/src/lib.rs index e9333268..f2489396 100644 --- a/butane/src/lib.rs +++ b/butane/src/lib.rs @@ -13,12 +13,10 @@ pub use butane_core::many::{Many, ManyOpsAsync, ManyOpsSync}; pub use butane_core::migrations; pub use butane_core::query; pub use butane_core::{ - AsPrimaryKey, AutoPk, DataObject, DataResult, Error, FieldType, FromSql, PrimaryKeyType, - Result, SqlType, SqlVal, SqlValRef, ToSql, + AsPrimaryKey, AutoPk, DataObject, DataObjectOpsAsync, DataObjectOpsSync, DataResult, Error, + FieldType, FromSql, PrimaryKeyType, Result, SqlType, SqlVal, SqlValRef, ToSql, }; -pub use butane_core::{DataObjectOpsAsync, DataObjectOpsSync}; - pub mod db { //! Database helpers pub use butane_core::db::*; @@ -71,12 +69,10 @@ pub mod db { /// rank: i32, /// nationality: String /// } -/// # tokio_test::block_on(async { -/// let e: BoolExpr = filter!(Contestant, nationality == "US" && rank < 42); -/// let first_place = 1; -/// let e2 = filter!(Contestant, rank == { first_place }); -/// let e3 = filter!(Contestant, name.like("A%")); -/// # }) +/// let e: BoolExpr = filter!(Contestant, nationality == "US" && rank < 42); +/// let first_place = 1; +/// let e2 = filter!(Contestant, rank == { first_place }); +/// let e3 = filter!(Contestant, name.like("A%")); ///``` /// /// [`BoolExpr`]: crate::query::BoolExpr diff --git a/butane/tests/common/blog.rs b/butane/tests/common/blog.rs index b8b5f25e..1bb630ff 100644 --- a/butane/tests/common/blog.rs +++ b/butane/tests/common/blog.rs @@ -73,7 +73,7 @@ impl Post { #[cfg(feature = "datetime")] #[dataresult(Post)] -#[allow(unused)] +#[allow(unused)] // Not all test files use it. pub struct PostMetadata { pub id: i64, pub title: String, From 72ccfc5fc9359bf367295dc4a232e9f6236b3026 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Sat, 26 Oct 2024 22:05:58 -0400 Subject: [PATCH 65/78] Fix error with tokio::test not found --- butane_core/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/butane_core/Cargo.toml b/butane_core/Cargo.toml index e350333b..7975d799 100644 --- a/butane_core/Cargo.toml +++ b/butane_core/Cargo.toml @@ -66,6 +66,7 @@ butane_test_macros.workspace = true env_logger = { workspace = true } paste = { workspace = true } tempfile.workspace = true +tokio = { workspace = true, features = ["macros"] } [[test]] name = "uuid" From e1b62559945ea231f6343048125bab72a0545c55 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Sat, 26 Oct 2024 22:09:49 -0400 Subject: [PATCH 66/78] Same fix for main lib --- butane/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/butane/Cargo.toml b/butane/Cargo.toml index 63794b2f..de5c94dc 100644 --- a/butane/Cargo.toml +++ b/butane/Cargo.toml @@ -55,6 +55,7 @@ rusqlite = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } sqlparser = { workspace = true } +tokio = { workspace = true, features = ["macros"] } uuid_for_test = { package = "uuid", version = "1.2", features = ["v4"] } [package.metadata.docs.rs] From 9ac8ee23b9e5932a6ef54148b18fc66a249305d9 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Sat, 26 Oct 2024 22:16:11 -0400 Subject: [PATCH 67/78] Fix Cargo.toml again --- butane/Cargo.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/butane/Cargo.toml b/butane/Cargo.toml index de5c94dc..4382bdef 100644 --- a/butane/Cargo.toml +++ b/butane/Cargo.toml @@ -46,7 +46,7 @@ quote = { workspace = true } maybe-async-cfg.workspace = true proc-macro2 = { workspace = true } once_cell = { workspace = true } -tokio = { workspace = true } +tokio = { workspace = true, features = ["macros"] } tokio-postgres = { features = ["with-geo-types-0_7"], workspace = true } tokio-test = { workspace = true } rand = { workspace = true } @@ -55,7 +55,6 @@ rusqlite = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } sqlparser = { workspace = true } -tokio = { workspace = true, features = ["macros"] } uuid_for_test = { package = "uuid", version = "1.2", features = ["v4"] } [package.metadata.docs.rs] From 900cd6553fd053eac3bbc0c288fa10fb34195e7c Mon Sep 17 00:00:00 2001 From: James Oakley Date: Sat, 26 Oct 2024 22:16:25 -0400 Subject: [PATCH 68/78] Fix a DeepSource warning --- butane_core/src/db/sync_adapter.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/butane_core/src/db/sync_adapter.rs b/butane_core/src/db/sync_adapter.rs index 464b4ac2..dac17884 100644 --- a/butane_core/src/db/sync_adapter.rs +++ b/butane_core/src/db/sync_adapter.rs @@ -51,7 +51,7 @@ impl SyncAdapter { fn chain(&self, inner: S) -> SyncAdapter { SyncAdapter { runtime_handle: self.runtime_handle.clone(), - _runtime: self._runtime.as_ref().map(|r| r.clone()), + _runtime: self._runtime.as_ref().cloned(), inner, } } @@ -146,7 +146,7 @@ where // We can't use chain because of the lifetimes and mutable borrows below, // so set up these runtime clones now. let runtime_handle = self.runtime_handle.clone(); - let runtime = self._runtime.as_ref().map(|r| r.clone()); + let runtime = self._runtime.as_ref().cloned(); let transaction: TransactionAsync = self.runtime_handle.block_on(self.inner.transaction())?; let transaction_adapter = SyncAdapter { From 1402544efd287a589efacaec8700a30acd9667e9 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Sat, 26 Oct 2024 22:22:08 -0400 Subject: [PATCH 69/78] getting_started_async to workspace --- Cargo.lock | 16 ++++++++++++++++ Cargo.toml | 1 + 2 files changed, 17 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 77143014..bdf07510 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -950,6 +950,22 @@ dependencies = [ "paste", ] +[[package]] +name = "getting_started_async" +version = "0.1.0" +dependencies = [ + "butane", + "butane_cli", + "butane_core", + "butane_test_helper", + "butane_test_macros", + "cfg-if", + "env_logger", + "log", + "paste", + "tokio", +] + [[package]] name = "gimli" version = "0.29.0" diff --git a/Cargo.toml b/Cargo.toml index cd45ca9a..0a4ab271 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "example", "examples/newtype", "examples/getting_started", + "examples/getting_started_async", ] [workspace.package] From 6770f4f4f77762766cc08c1f8a1700194de6ec80 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Sat, 26 Oct 2024 22:31:32 -0400 Subject: [PATCH 70/78] Fully remove tokio from CLI --- Cargo.lock | 1 - butane_cli/Cargo.toml | 1 - butane_cli/src/main.rs | 3 +-- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bdf07510..0b433a8d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -306,7 +306,6 @@ dependencies = [ "quote", "serde", "serde_json", - "tokio", ] [[package]] diff --git a/butane_cli/Cargo.toml b/butane_cli/Cargo.toml index 291e7679..b21d1cd0 100644 --- a/butane_cli/Cargo.toml +++ b/butane_cli/Cargo.toml @@ -36,4 +36,3 @@ nonempty.workspace = true quote = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -tokio = { workspace = true, features = ["macros"] } diff --git a/butane_cli/src/main.rs b/butane_cli/src/main.rs index a3faacea..a5cf1971 100644 --- a/butane_cli/src/main.rs +++ b/butane_cli/src/main.rs @@ -135,8 +135,7 @@ enum DeleteCommands { }, } -#[tokio::main(flavor = "current_thread")] -async fn main() { +fn main() { let cli = Cli::parse(); env_logger::Builder::new() From 2bbf1ef30555270f64af9da6add43577b9310499 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Sat, 26 Oct 2024 22:31:46 -0400 Subject: [PATCH 71/78] Fix getting_started_async --- examples/getting_started_async/src/lib.rs | 16 +++++++--------- examples/getting_started_async/src/models.rs | 1 - .../getting_started_async/tests/rollback.rs | 18 +++++++++--------- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/examples/getting_started_async/src/lib.rs b/examples/getting_started_async/src/lib.rs index 6ebbd3ba..4b004c50 100644 --- a/examples/getting_started_async/src/lib.rs +++ b/examples/getting_started_async/src/lib.rs @@ -5,39 +5,37 @@ pub mod butane_migrations; pub mod models; -use butane::db::{Connection, ConnectionSpec}; -use butane::migrations; +use butane::db::{ConnectionAsync, ConnectionSpec}; +use butane::migrations::Migrations; use butane::prelude_async::*; use models::{Blog, Post}; /// Load a [Connection]. -pub async fn establish_connection() -> Connection { +pub async fn establish_connection() -> ConnectionAsync { let mut connection = butane::db::connect_async(&ConnectionSpec::load(".butane/connection.json").unwrap()) .await .unwrap(); let migrations = butane_migrations::get_migrations().unwrap(); - migrations::apply_unapplied_migrations_async(migrations, &mut connection) - .await - .unwrap(); + migrations.migrate_async(&mut connection).await.unwrap(); connection } /// Create a [Blog]. -pub async fn create_blog(conn: &Connection, name: impl Into) -> Blog { +pub async fn create_blog(conn: &ConnectionAsync, name: impl Into) -> Blog { let mut blog = Blog::new(name); blog.save(conn).await.unwrap(); blog } /// Create a [Post]. -pub async fn create_post(conn: &Connection, blog: &Blog, title: String, body: String) -> Post { +pub async fn create_post(conn: &ConnectionAsync, blog: &Blog, title: String, body: String) -> Post { let mut new_post = Post::new(blog, title, body); new_post.save(conn).await.unwrap(); new_post } /// Fetch the first existing [Blog] if one exists. -pub async fn existing_blog(conn: &Connection) -> Option { +pub async fn existing_blog(conn: &ConnectionAsync) -> Option { Blog::query().load_first(conn).await.unwrap() } diff --git a/examples/getting_started_async/src/models.rs b/examples/getting_started_async/src/models.rs index c5655bbf..29611951 100644 --- a/examples/getting_started_async/src/models.rs +++ b/examples/getting_started_async/src/models.rs @@ -1,6 +1,5 @@ //! Models for the getting_started example. -use butane::prelude::*; use butane::AutoPk; use butane::{model, ForeignKey, Many}; diff --git a/examples/getting_started_async/tests/rollback.rs b/examples/getting_started_async/tests/rollback.rs index 043460af..ecb068a5 100644 --- a/examples/getting_started_async/tests/rollback.rs +++ b/examples/getting_started_async/tests/rollback.rs @@ -1,18 +1,18 @@ -use butane::db::{BackendConnection, Connection}; +use butane::db::{BackendConnectionAsync, ConnectionAsync}; use butane::migrations::Migrations; -use butane::DataObjectOpAsync; +use butane::DataObjectOpsAsync; use butane_test_helper::*; use butane_test_macros::butane_test; -use getting_started::models::{Blog, Post, Tag}; +use getting_started_async::models::{Blog, Post, Tag}; -async fn create_tag(connection: &Connection, name: &str) -> Tag { +async fn create_tag(connection: &ConnectionAsync, name: &str) -> Tag { let mut tag = Tag::new(name); tag.save(connection).await.unwrap(); tag } -async fn insert_data(connection: &Connection) { +async fn insert_data(connection: &ConnectionAsync) { if connection.backend_name() == "sqlite" { // https://github.com/Electron100/butane/issues/226 return; @@ -35,16 +35,16 @@ async fn insert_data(connection: &Connection) { post.save(connection).await.unwrap(); } -#[butane_test] -async fn migrate_and_unmigrate(mut connection: Connection) { +#[butane_test(async)] +async fn migrate_and_unmigrate(mut connection: ConnectionAsync) { // Migrate forward. let base_dir = std::path::PathBuf::from(".butane"); let migrations = butane_cli::get_migrations(&base_dir).unwrap(); - migrations.migrate(&mut connection).await.unwrap(); + migrations.migrate_async(&mut connection).await.unwrap(); insert_data(&connection).await; // Undo migrations. - migrations.unmigrate(&mut connection).await.unwrap(); + migrations.unmigrate_async(&mut connection).await.unwrap(); } From d859b5136a6b2ec5f12473150c8e5344fb841c21 Mon Sep 17 00:00:00 2001 From: James Oakley Date: Sat, 26 Oct 2024 22:43:33 -0400 Subject: [PATCH 72/78] Fix getting_started_async tests --- .../pg_down.sql | 30 ++--------------- .../pg_up.sql | 33 ++----------------- .../src/butane_migrations.rs | 6 ++-- .../tests/{rollback.rs => unmigrate.rs} | 2 +- 4 files changed, 10 insertions(+), 61 deletions(-) rename examples/getting_started_async/tests/{rollback.rs => unmigrate.rs} (97%) diff --git a/examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/pg_down.sql b/examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/pg_down.sql index b62d930f..1eb956aa 100644 --- a/examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/pg_down.sql +++ b/examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/pg_down.sql @@ -1,27 +1,3 @@ -CREATE TABLE Post__butane_tmp ( -id SERIAL NOT NULL PRIMARY KEY, -title TEXT NOT NULL, -body TEXT NOT NULL, -published BOOLEAN NOT NULL, -blog BIGINT NOT NULL, -byline TEXT , -likes INTEGER NOT NULL -); -INSERT INTO Post__butane_tmp SELECT id, title, body, published, blog, byline, likes FROM Post; -DROP TABLE Post; -ALTER TABLE Post__butane_tmp RENAME TO Post; -CREATE TABLE Post_tags_Many__butane_tmp ( -owner INTEGER NOT NULL, -has TEXT NOT NULL -); -ALTER TABLE Post_tags_Many__butane_tmp ADD FOREIGN KEY (owner) REFERENCES Post(id); -INSERT INTO Post_tags_Many__butane_tmp SELECT owner, has FROM Post_tags_Many; -DROP TABLE Post_tags_Many; -ALTER TABLE Post_tags_Many__butane_tmp RENAME TO Post_tags_Many; -CREATE TABLE Post_tags_Many__butane_tmp ( -owner INTEGER NOT NULL, -has TEXT NOT NULL -); -INSERT INTO Post_tags_Many__butane_tmp SELECT owner, has FROM Post_tags_Many; -DROP TABLE Post_tags_Many; -ALTER TABLE Post_tags_Many__butane_tmp RENAME TO Post_tags_Many; +ALTER TABLE Post DROP CONSTRAINT Post_blog_fkey; +ALTER TABLE Post_tags_Many DROP CONSTRAINT Post_tags_Many_has_fkey; +ALTER TABLE Post_tags_Many DROP CONSTRAINT Post_tags_Many_owner_fkey; diff --git a/examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/pg_up.sql b/examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/pg_up.sql index a023e1e0..773142e0 100644 --- a/examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/pg_up.sql +++ b/examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/pg_up.sql @@ -1,30 +1,3 @@ -CREATE TABLE Post__butane_tmp ( -id SERIAL NOT NULL PRIMARY KEY, -title TEXT NOT NULL, -body TEXT NOT NULL, -published BOOLEAN NOT NULL, -blog BIGINT NOT NULL, -byline TEXT , -likes INTEGER NOT NULL -); -ALTER TABLE Post__butane_tmp ADD FOREIGN KEY (blog) REFERENCES Blog(id); -INSERT INTO Post__butane_tmp SELECT id, title, body, published, blog, byline, likes FROM Post; -DROP TABLE Post; -ALTER TABLE Post__butane_tmp RENAME TO Post; -CREATE TABLE Post_tags_Many__butane_tmp ( -owner INTEGER NOT NULL, -has TEXT NOT NULL -); -ALTER TABLE Post_tags_Many__butane_tmp ADD FOREIGN KEY (has) REFERENCES Tag(tag); -INSERT INTO Post_tags_Many__butane_tmp SELECT owner, has FROM Post_tags_Many; -DROP TABLE Post_tags_Many; -ALTER TABLE Post_tags_Many__butane_tmp RENAME TO Post_tags_Many; -CREATE TABLE Post_tags_Many__butane_tmp ( -owner INTEGER NOT NULL, -has TEXT NOT NULL -); -ALTER TABLE Post_tags_Many__butane_tmp ADD FOREIGN KEY (owner) REFERENCES Post(id); -ALTER TABLE Post_tags_Many__butane_tmp ADD FOREIGN KEY (has) REFERENCES Tag(tag); -INSERT INTO Post_tags_Many__butane_tmp SELECT owner, has FROM Post_tags_Many; -DROP TABLE Post_tags_Many; -ALTER TABLE Post_tags_Many__butane_tmp RENAME TO Post_tags_Many; +ALTER TABLE Post ADD FOREIGN KEY (blog) REFERENCES Blog(id); +ALTER TABLE Post_tags_Many ADD FOREIGN KEY (has) REFERENCES Tag(tag); +ALTER TABLE Post_tags_Many ADD FOREIGN KEY (owner) REFERENCES Post(id); diff --git a/examples/getting_started_async/src/butane_migrations.rs b/examples/getting_started_async/src/butane_migrations.rs index 5c045ef9..b6cd1073 100644 --- a/examples/getting_started_async/src/butane_migrations.rs +++ b/examples/getting_started_async/src/butane_migrations.rs @@ -156,7 +156,7 @@ pub fn get_migrations() -> Result { }, "from": null, "up": { - "pg": "CREATE TABLE Blog (\nid BIGSERIAL NOT NULL PRIMARY KEY,\n\"name\" TEXT NOT NULL\n);\nCREATE TABLE Post (\nid SERIAL NOT NULL PRIMARY KEY,\ntitle TEXT NOT NULL,\nbody TEXT NOT NULL,\npublished BOOLEAN NOT NULL,\nblog BIGINT NOT NULL,\nbyline TEXT \n);\nCREATE TABLE Post_tags_Many (\nowner INTEGER NOT NULL,\nhas TEXT NOT NULL\n);\nCREATE TABLE Tag (\ntag TEXT NOT NULL PRIMARY KEY\n);\nCREATE TABLE IF NOT EXISTS butane_migrations (\n\"name\" TEXT NOT NULL PRIMARY KEY\n);\n", + "pg": "CREATE TABLE Blog (\nid BIGSERIAL NOT NULL PRIMARY KEY,\n\"name\" TEXT NOT NULL\n);\nCREATE TABLE Post (\nid SERIAL NOT NULL PRIMARY KEY,\ntitle TEXT NOT NULL,\nbody TEXT NOT NULL,\npublished BOOLEAN NOT NULL,\nblog BIGINT NOT NULL,\nbyline TEXT\n);\nCREATE TABLE Post_tags_Many (\nowner INTEGER NOT NULL,\nhas TEXT NOT NULL\n);\nCREATE TABLE Tag (\ntag TEXT NOT NULL PRIMARY KEY\n);\nCREATE TABLE IF NOT EXISTS butane_migrations (\n\"name\" TEXT NOT NULL PRIMARY KEY\n);\n", "sqlite": "CREATE TABLE Blog (\nid INTEGER NOT NULL PRIMARY KEY,\n\"name\" TEXT NOT NULL\n);\nCREATE TABLE Post (\nid INTEGER NOT NULL PRIMARY KEY,\ntitle TEXT NOT NULL,\nbody TEXT NOT NULL,\npublished INTEGER NOT NULL,\nblog INTEGER NOT NULL,\nbyline TEXT\n);\nCREATE TABLE Post_tags_Many (\nowner INTEGER NOT NULL,\nhas TEXT NOT NULL\n);\nCREATE TABLE Tag (\ntag TEXT NOT NULL PRIMARY KEY\n);\nCREATE TABLE IF NOT EXISTS butane_migrations (\n\"name\" TEXT NOT NULL PRIMARY KEY\n);\n" }, "down": { @@ -530,11 +530,11 @@ pub fn get_migrations() -> Result { }, "from": "20201229_171630604_likes", "up": { - "pg": "CREATE TABLE Post__butane_tmp (\nid SERIAL NOT NULL PRIMARY KEY,\ntitle TEXT NOT NULL,\nbody TEXT NOT NULL,\npublished BOOLEAN NOT NULL,\nblog BIGINT NOT NULL,\nbyline TEXT ,\nlikes INTEGER NOT NULL\n);\nALTER TABLE Post__butane_tmp ADD FOREIGN KEY (blog) REFERENCES Blog(id);\nINSERT INTO Post__butane_tmp SELECT id, title, body, published, blog, byline, likes FROM Post;\nDROP TABLE Post;\nALTER TABLE Post__butane_tmp RENAME TO Post;\nCREATE TABLE Post_tags_Many__butane_tmp (\nowner INTEGER NOT NULL,\nhas TEXT NOT NULL\n);\nALTER TABLE Post_tags_Many__butane_tmp ADD FOREIGN KEY (has) REFERENCES Tag(tag);\nINSERT INTO Post_tags_Many__butane_tmp SELECT owner, has FROM Post_tags_Many;\nDROP TABLE Post_tags_Many;\nALTER TABLE Post_tags_Many__butane_tmp RENAME TO Post_tags_Many;\nCREATE TABLE Post_tags_Many__butane_tmp (\nowner INTEGER NOT NULL,\nhas TEXT NOT NULL\n);\nALTER TABLE Post_tags_Many__butane_tmp ADD FOREIGN KEY (owner) REFERENCES Post(id);\nALTER TABLE Post_tags_Many__butane_tmp ADD FOREIGN KEY (has) REFERENCES Tag(tag);\nINSERT INTO Post_tags_Many__butane_tmp SELECT owner, has FROM Post_tags_Many;\nDROP TABLE Post_tags_Many;\nALTER TABLE Post_tags_Many__butane_tmp RENAME TO Post_tags_Many;\n", + "pg": "ALTER TABLE Post ADD FOREIGN KEY (blog) REFERENCES Blog(id);\nALTER TABLE Post_tags_Many ADD FOREIGN KEY (has) REFERENCES Tag(tag);\nALTER TABLE Post_tags_Many ADD FOREIGN KEY (owner) REFERENCES Post(id);\n", "sqlite": "CREATE TABLE Post__butane_tmp (\nid INTEGER NOT NULL PRIMARY KEY,\ntitle TEXT NOT NULL,\nbody TEXT NOT NULL,\npublished INTEGER NOT NULL,\nblog INTEGER NOT NULL,\nbyline TEXT,\nlikes INTEGER NOT NULL,\nFOREIGN KEY (blog) REFERENCES Blog(id)\n);\nINSERT INTO Post__butane_tmp SELECT id, title, body, published, blog, byline, likes FROM Post;\nDROP TABLE Post;\nALTER TABLE Post__butane_tmp RENAME TO Post;\nCREATE TABLE Post_tags_Many__butane_tmp (\nowner INTEGER NOT NULL,\nhas TEXT NOT NULL,\nFOREIGN KEY (has) REFERENCES Tag(tag)\n);\nINSERT INTO Post_tags_Many__butane_tmp SELECT owner, has FROM Post_tags_Many;\nDROP TABLE Post_tags_Many;\nALTER TABLE Post_tags_Many__butane_tmp RENAME TO Post_tags_Many;\nCREATE TABLE Post_tags_Many__butane_tmp (\nowner INTEGER NOT NULL,\nhas TEXT NOT NULL,\nFOREIGN KEY (owner) REFERENCES Post(id)\nFOREIGN KEY (has) REFERENCES Tag(tag)\n);\nINSERT INTO Post_tags_Many__butane_tmp SELECT owner, has FROM Post_tags_Many;\nDROP TABLE Post_tags_Many;\nALTER TABLE Post_tags_Many__butane_tmp RENAME TO Post_tags_Many;\n" }, "down": { - "pg": "CREATE TABLE Post__butane_tmp (\nid SERIAL NOT NULL PRIMARY KEY,\ntitle TEXT NOT NULL,\nbody TEXT NOT NULL,\npublished BOOLEAN NOT NULL,\nblog BIGINT NOT NULL,\nbyline TEXT ,\nlikes INTEGER NOT NULL\n);\nINSERT INTO Post__butane_tmp SELECT id, title, body, published, blog, byline, likes FROM Post;\nDROP TABLE Post;\nALTER TABLE Post__butane_tmp RENAME TO Post;\nCREATE TABLE Post_tags_Many__butane_tmp (\nowner INTEGER NOT NULL,\nhas TEXT NOT NULL\n);\nALTER TABLE Post_tags_Many__butane_tmp ADD FOREIGN KEY (owner) REFERENCES Post(id);\nINSERT INTO Post_tags_Many__butane_tmp SELECT owner, has FROM Post_tags_Many;\nDROP TABLE Post_tags_Many;\nALTER TABLE Post_tags_Many__butane_tmp RENAME TO Post_tags_Many;\nCREATE TABLE Post_tags_Many__butane_tmp (\nowner INTEGER NOT NULL,\nhas TEXT NOT NULL\n);\nINSERT INTO Post_tags_Many__butane_tmp SELECT owner, has FROM Post_tags_Many;\nDROP TABLE Post_tags_Many;\nALTER TABLE Post_tags_Many__butane_tmp RENAME TO Post_tags_Many;\n", + "pg": "ALTER TABLE Post DROP CONSTRAINT Post_blog_fkey;\nALTER TABLE Post_tags_Many DROP CONSTRAINT Post_tags_Many_has_fkey;\nALTER TABLE Post_tags_Many DROP CONSTRAINT Post_tags_Many_owner_fkey;\n", "sqlite": "CREATE TABLE Post__butane_tmp (\nid INTEGER NOT NULL PRIMARY KEY,\ntitle TEXT NOT NULL,\nbody TEXT NOT NULL,\npublished INTEGER NOT NULL,\nblog INTEGER NOT NULL,\nbyline TEXT,\nlikes INTEGER NOT NULL\n);\nINSERT INTO Post__butane_tmp SELECT id, title, body, published, blog, byline, likes FROM Post;\nDROP TABLE Post;\nALTER TABLE Post__butane_tmp RENAME TO Post;\nCREATE TABLE Post_tags_Many__butane_tmp (\nowner INTEGER NOT NULL,\nhas TEXT NOT NULL,\nFOREIGN KEY (owner) REFERENCES Post(id)\n);\nINSERT INTO Post_tags_Many__butane_tmp SELECT owner, has FROM Post_tags_Many;\nDROP TABLE Post_tags_Many;\nALTER TABLE Post_tags_Many__butane_tmp RENAME TO Post_tags_Many;\nCREATE TABLE Post_tags_Many__butane_tmp (\nowner INTEGER NOT NULL,\nhas TEXT NOT NULL\n);\nINSERT INTO Post_tags_Many__butane_tmp SELECT owner, has FROM Post_tags_Many;\nDROP TABLE Post_tags_Many;\nALTER TABLE Post_tags_Many__butane_tmp RENAME TO Post_tags_Many;\n" } } diff --git a/examples/getting_started_async/tests/rollback.rs b/examples/getting_started_async/tests/unmigrate.rs similarity index 97% rename from examples/getting_started_async/tests/rollback.rs rename to examples/getting_started_async/tests/unmigrate.rs index ecb068a5..6dab144c 100644 --- a/examples/getting_started_async/tests/rollback.rs +++ b/examples/getting_started_async/tests/unmigrate.rs @@ -35,7 +35,7 @@ async fn insert_data(connection: &ConnectionAsync) { post.save(connection).await.unwrap(); } -#[butane_test(async)] +#[butane_test(async, nomigrate)] async fn migrate_and_unmigrate(mut connection: ConnectionAsync) { // Migrate forward. let base_dir = std::path::PathBuf::from(".butane"); From f58d59e8354ef0f5090663f20890d28e1691da8e Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Sun, 27 Oct 2024 16:40:10 +0800 Subject: [PATCH 73/78] fix newtype dev-deps --- examples/newtype/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/newtype/Cargo.toml b/examples/newtype/Cargo.toml index cf1e986f..865ea478 100644 --- a/examples/newtype/Cargo.toml +++ b/examples/newtype/Cargo.toml @@ -31,7 +31,7 @@ cfg-if.workspace = true env_logger.workspace = true log.workspace = true paste.workspace = true -tokio.workspace = true +tokio = { workspace = true, features = ["macros"] } [package.metadata.release] release = false From 0009f581417620d5df67c78dd346ae00abc1d5bd Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Sun, 27 Oct 2024 17:35:44 +0800 Subject: [PATCH 74/78] Non-functional changes; docs, comments, alpha sorting --- Cargo.toml | 2 +- butane/src/lib.rs | 6 +++--- butane_core/src/db/adapter.rs | 4 ++-- butane_core/src/db/connmethods.rs | 7 ++++--- butane_core/src/db/mod.rs | 31 ++++++++++++++++-------------- butane_core/src/db/pg.rs | 8 +++----- butane_core/src/db/r2.rs | 13 +++++++------ butane_core/src/db/sqlite.rs | 11 ++++++----- butane_core/src/db/sync_adapter.rs | 9 +++++---- butane_core/src/fkey.rs | 4 ++-- butane_core/src/lib.rs | 1 - butane_core/src/many.rs | 15 ++++++++------- butane_core/src/migrations/mod.rs | 2 +- butane_core/src/query/mod.rs | 2 +- butane_core/src/util.rs | 3 ++- butane_test_helper/Cargo.toml | 1 - butane_test_helper/src/lib.rs | 17 ++++++++-------- butane_test_macros/Cargo.toml | 1 - butane_test_macros/src/lib.rs | 4 ++-- 19 files changed, 72 insertions(+), 69 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0a4ab271..20a0e399 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,8 +49,8 @@ serde_json = "1.0" sqlparser = "0.44" syn = { version = "2", features = ["extra-traits", "full"] } tempfile = "3.10" -tokio-postgres = "0.7" tokio = { version = "1"} +tokio-postgres = "0.7" tokio-test = { version = "0.4"} uuid = "1.2" diff --git a/butane/src/lib.rs b/butane/src/lib.rs index f2489396..0447efef 100644 --- a/butane/src/lib.rs +++ b/butane/src/lib.rs @@ -73,7 +73,7 @@ pub mod db { /// let first_place = 1; /// let e2 = filter!(Contestant, rank == { first_place }); /// let e3 = filter!(Contestant, name.like("A%")); -///``` +/// ``` /// /// [`BoolExpr`]: crate::query::BoolExpr /// [`Query`]: crate::query::Query @@ -151,7 +151,7 @@ macro_rules! colname { /// /// let conn = butane::db::connect(&ConnectionSpec::new("sqlite", "foo.db")).unwrap(); /// let alice: Result = find!(Contestant, name == "Alice", &conn); -///``` +/// ``` /// /// [`filter]: crate::filter /// [`Result`]: crate::Result @@ -164,7 +164,7 @@ macro_rules! find { }; } -/// Like [`find`], but for async +/// Like [`find`], but for async. #[macro_export] macro_rules! find_async { ($dbobj:ident, $filter:expr, $conn:expr) => { diff --git a/butane_core/src/db/adapter.rs b/butane_core/src/db/adapter.rs index 65bdc518..6108c603 100644 --- a/butane_core/src/db/adapter.rs +++ b/butane_core/src/db/adapter.rs @@ -297,7 +297,7 @@ where self.invoke(|conn| conn.insert_returning_pk(table, columns, pkcol, values)) .await } - /// Like `insert_returning_pk` but with no return value + /// Like `insert_returning_pk` but with no return value. async fn insert_only( &self, table: &str, @@ -307,7 +307,7 @@ where self.invoke(|conn| conn.insert_only(table, columns, values)) .await } - /// Insert unless there's a conflict on the primary key column, in which case update + /// Insert unless there's a conflict on the primary key column, in which case update. async fn insert_or_replace( &self, table: &str, diff --git a/butane_core/src/db/connmethods.rs b/butane_core/src/db/connmethods.rs index bb45a293..330d228c 100644 --- a/butane_core/src/db/connmethods.rs +++ b/butane_core/src/db/connmethods.rs @@ -1,9 +1,10 @@ //! Not expected to be called directly by most users. Used by code //! generated by `#[model]`, `query!`, and other macros. -use async_trait::async_trait; use std::ops::{Deref, DerefMut}; +use async_trait::async_trait; + use crate::query::{BoolExpr, Expr, Order}; use crate::{Result, SqlType, SqlVal, SqlValRef}; @@ -35,14 +36,14 @@ pub trait ConnectionMethods: super::internal::AsyncRequiresSync { pkcol: &Column, values: &[SqlValRef<'_>], ) -> Result; - /// Like `insert_returning_pk` but with no return value + /// Like `insert_returning_pk` but with no return value. async fn insert_only( &self, table: &str, columns: &[Column], values: &[SqlValRef<'_>], ) -> Result<()>; - /// Insert unless there's a conflict on the primary key column, in which case update + /// Insert unless there's a conflict on the primary key column, in which case update. async fn insert_or_replace( &self, table: &str, diff --git a/butane_core/src/db/mod.rs b/butane_core/src/db/mod.rs index 4e4031ec..7344e692 100644 --- a/butane_core/src/db/mod.rs +++ b/butane_core/src/db/mod.rs @@ -92,7 +92,7 @@ pub trait BackendConnection: ConnectionMethods + Debug + Send { /// Begin a database transaction. The transaction object must be /// used in place of this connection until it is committed or aborted. async fn transaction(&mut self) -> Result>; - /// Retrieve the backend for this connection + /// Retrieve the backend for this connection. fn backend(&self) -> Box; /// Retrieve the backend name for this connection. fn backend_name(&self) -> &'static str; @@ -226,7 +226,7 @@ impl Connection { pub async fn execute(&self, sql: impl AsRef) -> Result<()> { self.conn.execute(sql.as_ref()).await } - // For use with connection_method_wrapper macro + // For use with connection_method_wrapper macro. #[allow(clippy::unnecessary_wraps)] fn wrapped_connection_methods(&self) -> Result<&dyn BackendConnection> { Ok(self.conn.as_ref()) @@ -237,8 +237,8 @@ impl Connection { Ok(adapter::AsyncAdapter::new(|| Ok(self))?.into_connection()) } - /// Runs the provided function with a synchronous wrapper around this - /// asynchronous connection. + /// Runs the provided function with a synchronous wrapper around this asynchronous connection. + /// /// Because this relies on some (safe) memory gymnastics, /// there is a small but nonzero risk that if certain tokio calls fail unexpectedly at /// the wrong place the the connection will be poisoned -- all subsequent calls @@ -308,10 +308,11 @@ connection_method_wrapper!(Connection); )] #[async_trait] pub trait BackendTransaction<'c>: ConnectionMethods + internal::AsyncRequiresSend + Debug { - /// Commit the transaction Unfortunately because we use this as a - /// trait object, we can't consume self. It should be understood - /// that no methods should be called after commit. This trait is - /// not public, and that behavior is enforced by Transaction + /// Commit the transaction. + /// + /// Unfortunately because we use this as a trait object, we can't consume self. + /// It should be understood that no methods should be called after commit. + /// This trait is not public, and that behavior is enforced by Transaction. async fn commit(&mut self) -> Result<()>; /// Roll back the transaction. Same comment about consuming self as above. async fn rollback(&mut self) -> Result<()>; @@ -348,7 +349,7 @@ impl<'c> Transaction<'c> { pub(super) fn new(trans: Box + 'c>) -> Self { Transaction { trans } } - /// Commit the transaction + /// Commit the transaction. pub async fn commit(mut self) -> Result<()> { self.trans.commit().await } @@ -356,7 +357,7 @@ impl<'c> Transaction<'c> { pub async fn rollback(mut self) -> Result<()> { self.trans.deref_mut().rollback().await } - // For use with connection_method_wrapper macro + // For use with connection_method_wrapper macro. #[allow(clippy::unnecessary_wraps)] fn wrapped_connection_methods(&self) -> Result<&dyn ConnectionMethods> { let a: &dyn BackendTransaction<'c> = self.trans.as_ref(); @@ -567,8 +568,9 @@ pub fn get_backend(name: &str) -> Option> { } } -/// Connect to a database. For non-boxed connections, see individual -/// [`Backend`] implementations. +/// Connect to a database. +/// +/// For non-boxed connections, see individual [`Backend`] implementations. pub fn connect(spec: &ConnectionSpec) -> Result { let conn = get_backend(&spec.backend_name) .ok_or_else(|| Error::UnknownBackend(spec.backend_name.clone()))? @@ -576,8 +578,9 @@ pub fn connect(spec: &ConnectionSpec) -> Result { Ok(conn) } -/// Connect to a database. For non-boxed connections, see individual -/// [`Backend`] implementations. +/// Connect to a database async. +/// +/// For non-boxed connections, see individual [`Backend`] implementations. pub async fn connect_async(spec: &ConnectionSpec) -> Result { get_backend(&spec.backend_name) .ok_or_else(|| Error::UnknownBackend(spec.backend_name.clone()))? diff --git a/butane_core/src/db/pg.rs b/butane_core/src/db/pg.rs index 5bf141c4..140eeb73 100644 --- a/butane_core/src/db/pg.rs +++ b/butane_core/src/db/pg.rs @@ -6,6 +6,9 @@ use async_trait::async_trait; use bytes::BufMut; #[cfg(feature = "datetime")] use chrono::NaiveDateTime; +use futures_util::stream::StreamExt; +use tokio_postgres as postgres; +use tokio_postgres::GenericClient; use super::connmethods::VecRows; use super::helper; @@ -19,9 +22,6 @@ use crate::db::{ use crate::migrations::adb::{AColumn, ARef, ATable, Operation, TypeIdentifier, ADB}; use crate::query::{BoolExpr, Expr}; use crate::{debug, query, warn, Error, Result, SqlType, SqlVal, SqlValRef}; -use futures_util::stream::StreamExt; -use tokio_postgres as postgres; -use tokio_postgres::GenericClient; /// The name of the postgres backend. pub const BACKEND_NAME: &str = "pg"; @@ -64,8 +64,6 @@ impl Backend for PgBackend { } } -// type PgConnHandle = tokio::task::JoinHandle>; - /// Pg database connection. pub struct PgConnection { #[cfg(feature = "debug")] diff --git a/butane_core/src/db/r2.rs b/butane_core/src/db/r2.rs index 617355de..c7d27bfe 100644 --- a/butane_core/src/db/r2.rs +++ b/butane_core/src/db/r2.rs @@ -1,13 +1,14 @@ //! R2D2 support for Butane. +use std::ops::Deref; + pub use r2d2::ManageConnection; -use crate::db::{BackendConnection, Connection, ConnectionMethods}; -use crate::db::{Column, ConnectionSpec, RawQueryResult}; +use crate::db::{ + BackendConnection, Column, Connection, ConnectionMethods, ConnectionSpec, RawQueryResult, +}; use crate::{query::BoolExpr, query::Order, Result, SqlVal, SqlValRef}; -use std::ops::Deref; - /// R2D2 support for Butane. Implements [`r2d2::ManageConnection`]. #[derive(Clone, Debug)] pub struct ConnectionManager { @@ -62,11 +63,11 @@ impl ConnectionMethods for r2d2::PooledConnection { self.deref() .insert_returning_pk(table, columns, pkcol, values) } - /// Like `insert_returning_pk` but with no return value + /// Like `insert_returning_pk` but with no return value. fn insert_only(&self, table: &str, columns: &[Column], values: &[SqlValRef<'_>]) -> Result<()> { self.deref().insert_only(table, columns, values) } - /// Insert unless there's a conflict on the primary key column, in which case update + /// Insert unless there's a conflict on the primary key column, in which case update. fn insert_or_replace( &self, table: &str, diff --git a/butane_core/src/db/sqlite.rs b/butane_core/src/db/sqlite.rs index b66fbd7d..b1558d82 100644 --- a/butane_core/src/db/sqlite.rs +++ b/butane_core/src/db/sqlite.rs @@ -1,5 +1,4 @@ //! SQLite database backend -use async_trait::async_trait; use std::borrow::Cow; use std::fmt::{Debug, Write}; use std::ops::Deref; @@ -8,6 +7,12 @@ use std::pin::Pin; #[cfg(feature = "log")] use std::sync::Once; +use async_trait::async_trait; +#[cfg(feature = "datetime")] +use chrono::naive::NaiveDateTime; +use fallible_streaming_iterator::FallibleStreamingIterator; +use pin_project::pin_project; + use super::{helper, Backend, BackendRow, Column, RawQueryResult}; use super::{ BackendConnection, BackendTransaction, Connection, ConnectionAsync, ConnectionMethods, @@ -18,10 +23,6 @@ use crate::migrations::adb::ARef; use crate::migrations::adb::{AColumn, ATable, Operation, TypeIdentifier, ADB}; use crate::query::{BoolExpr, Order}; use crate::{debug, query, Error, Result, SqlType, SqlVal, SqlValRef}; -#[cfg(feature = "datetime")] -use chrono::naive::NaiveDateTime; -use fallible_streaming_iterator::FallibleStreamingIterator; -use pin_project::pin_project; #[cfg(feature = "datetime")] const SQLITE_DT_FORMAT: &str = "%Y-%m-%d %H:%M:%S"; diff --git a/butane_core/src/db/sync_adapter.rs b/butane_core/src/db/sync_adapter.rs index dac17884..f66eb639 100644 --- a/butane_core/src/db/sync_adapter.rs +++ b/butane_core/src/db/sync_adapter.rs @@ -1,3 +1,8 @@ +use std::future::Future; +use std::sync::Arc; + +use async_trait::async_trait; + use crate::db::{ Backend, BackendConnection, BackendConnectionAsync, BackendTransaction, BackendTransactionAsync, Connection, ConnectionAsync, ConnectionMethods, RawQueryResult, @@ -6,10 +11,6 @@ use crate::db::{ use crate::migrations::adb; use crate::query::{BoolExpr, Order}; use crate::{debug, Column, Result, SqlVal, SqlValRef}; -use async_trait::async_trait; - -use std::future::Future; -use std::sync::Arc; #[derive(Debug)] pub struct SyncAdapter { diff --git a/butane_core/src/fkey.rs b/butane_core/src/fkey.rs index 21b63166..e5e638e0 100644 --- a/butane_core/src/fkey.rs +++ b/butane_core/src/fkey.rs @@ -1,6 +1,5 @@ //! Implementation of foreign key relationships between models. #![deny(missing_docs)] -use crate::util::{get_or_init_once_lock, get_or_init_once_lock_async}; use std::borrow::Cow; use std::fmt::Debug; use std::sync::OnceLock; @@ -9,6 +8,7 @@ use std::sync::OnceLock; use fake::{Dummy, Faker}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use crate::util::{get_or_init_once_lock, get_or_init_once_lock_async}; use crate::{ AsPrimaryKey, ConnectionMethods, ConnectionMethodsAsync, DataObject, Error, FieldType, FromSql, Result, SqlType, SqlVal, SqlValRef, ToSql, @@ -87,7 +87,7 @@ impl ForeignKey { } } -/// [`ForeignKey`] operations which require a `Connection` +/// [`ForeignKey`] operations which require a `Connection`. #[allow(async_fn_in_trait)] // Not intended to be implemented outside Butane #[maybe_async_cfg::maybe( idents(ConnectionMethods(sync = "ConnectionMethods"),), diff --git a/butane_core/src/lib.rs b/butane_core/src/lib.rs index 284031cd..a2e67fc0 100644 --- a/butane_core/src/lib.rs +++ b/butane_core/src/lib.rs @@ -7,7 +7,6 @@ use std::borrow::Borrow; use std::cmp::{Eq, PartialEq}; use serde::{Deserialize, Serialize}; - use thiserror::Error as ThisError; pub mod codegen; diff --git a/butane_core/src/many.rs b/butane_core/src/many.rs index c21348e5..8f06eabf 100644 --- a/butane_core/src/many.rs +++ b/butane_core/src/many.rs @@ -1,15 +1,16 @@ //! Implementation of many-to-many relationships between models. #![deny(missing_docs)] -use crate::db::{Column, ConnectionMethods, ConnectionMethodsAsync}; -use crate::query::{BoolExpr, Expr, OrderDirection, Query}; -use crate::util::{get_or_init_once_lock, get_or_init_once_lock_async}; -use crate::{sqlval::PrimaryKeyType, DataObject, Error, FieldType, Result, SqlType, SqlVal, ToSql}; -use serde::{Deserialize, Serialize}; use std::borrow::Cow; use std::sync::OnceLock; #[cfg(feature = "fake")] use fake::{Dummy, Faker}; +use serde::{Deserialize, Serialize}; + +use crate::db::{Column, ConnectionMethods, ConnectionMethodsAsync}; +use crate::query::{BoolExpr, Expr, OrderDirection, Query}; +use crate::util::{get_or_init_once_lock, get_or_init_once_lock_async}; +use crate::{sqlval::PrimaryKeyType, DataObject, Error, FieldType, Result, SqlType, SqlVal, ToSql}; fn default_oc() -> OnceLock> { // Same as impl Default for once_cell::unsync::OnceCell @@ -118,7 +119,7 @@ where })) } - /// Describes the columns of the Many table + /// Describes the columns of the Many table. pub fn columns(&self) -> [Column; 2] { [ Column::new("owner", self.owner_type.clone()), @@ -176,7 +177,7 @@ where .map(|v| v.iter()) } -/// [`Many`] operations which require a `Connection` +/// [`Many`] operations which require a `Connection`. #[allow(async_fn_in_trait)] // Not intended to be implemented outside Butane #[maybe_async_cfg::maybe( idents(ConnectionMethods(sync = "ConnectionMethods"),), diff --git a/butane_core/src/migrations/mod.rs b/butane_core/src/migrations/mod.rs index c6311716..bb2f7693 100644 --- a/butane_core/src/migrations/mod.rs +++ b/butane_core/src/migrations/mod.rs @@ -5,6 +5,7 @@ use std::path::Path; +use async_trait::async_trait; use fallible_iterator::FallibleIterator; use nonempty::NonEmpty; @@ -26,7 +27,6 @@ mod fs; mod fsmigrations; pub use fsmigrations::{FsMigration, FsMigrations}; mod memmigrations; -use async_trait::async_trait; pub use memmigrations::{MemMigration, MemMigrations}; /// A collection of migrations. diff --git a/butane_core/src/query/mod.rs b/butane_core/src/query/mod.rs index 31637245..742714b5 100644 --- a/butane_core/src/query/mod.rs +++ b/butane_core/src/query/mod.rs @@ -201,7 +201,7 @@ impl Clone for Query { mod private { use super::*; - /// Internal QueryOps helpers + /// Internal QueryOps helpers. #[allow(async_fn_in_trait)] // Not truly a public trait #[maybe_async_cfg::maybe( idents(ConnectionMethods(sync = "ConnectionMethods")), diff --git a/butane_core/src/util.rs b/butane_core/src/util.rs index 3483aeb0..cf7b891f 100644 --- a/butane_core/src/util.rs +++ b/butane_core/src/util.rs @@ -1,6 +1,7 @@ -use crate::Result; use std::sync::OnceLock; +use crate::Result; + pub fn get_or_init_once_lock(cell: &OnceLock, f: impl FnOnce() -> Result) -> Result<&T> { if let Some(val) = cell.get() { return Ok(val); diff --git a/butane_test_helper/Cargo.toml b/butane_test_helper/Cargo.toml index f569de0a..3a4a3b32 100644 --- a/butane_test_helper/Cargo.toml +++ b/butane_test_helper/Cargo.toml @@ -25,6 +25,5 @@ rand.workspace = true tempfile.workspace = true uuid = { features = ["v4"], workspace = true } - [package.metadata.release] release = false diff --git a/butane_test_helper/src/lib.rs b/butane_test_helper/src/lib.rs index 10fe1b44..c9552a53 100644 --- a/butane_test_helper/src/lib.rs +++ b/butane_test_helper/src/lib.rs @@ -2,13 +2,6 @@ //! Macros depend on [`butane_core`], `env_logger` and [`log`]. #![deny(missing_docs)] -use butane_core::db::{ - connect, connect_async, get_backend, pg, pg::PgBackend, sqlite, sqlite::SQLiteBackend, Backend, - ConnectionSpec, -}; -use butane_core::migrations::{self, MemMigrations, Migration, Migrations, MigrationsMut}; -use once_cell::sync::Lazy; - use std::future::Future; use std::io::{BufRead, BufReader, Read, Write}; use std::ops::Deref; @@ -17,12 +10,18 @@ use std::process::{ChildStderr, Command, Stdio}; use std::sync::Mutex; use block_id::{Alphabet, BlockId}; +use butane_core::db::{ + connect, connect_async, get_backend, pg, pg::PgBackend, sqlite, sqlite::SQLiteBackend, Backend, + ConnectionSpec, +}; +use butane_core::migrations::{self, MemMigrations, Migration, Migrations, MigrationsMut}; +use once_cell::sync::Lazy; use uuid::Uuid; pub use butane_core::db::{BackendConnection, BackendConnectionAsync, Connection, ConnectionAsync}; pub use maybe_async_cfg; -/// Trait for running a test +/// Trait for running a test. #[allow(async_fn_in_trait)] // Not truly public, only used in butane for testing. pub trait BackendTestInstance { /// Run a synchronous test. @@ -395,7 +394,7 @@ fn common_setup() { env_logger::try_init().ok(); } -/// Run a test function with a wrapper to set up and tear down the connection +/// Run a test function with a wrapper to set up and tear down the connection. pub async fn run_test_async( backend_name: &str, setup: impl FnOnce() -> Fut, diff --git a/butane_test_macros/Cargo.toml b/butane_test_macros/Cargo.toml index 6e7158be..04adb367 100644 --- a/butane_test_macros/Cargo.toml +++ b/butane_test_macros/Cargo.toml @@ -8,7 +8,6 @@ publish = false license.workspace = true repository.workspace = true - [dependencies] proc-macro2 = { workspace = true } quote = { workspace = true } diff --git a/butane_test_macros/src/lib.rs b/butane_test_macros/src/lib.rs index 4fe56d3f..8b622d48 100644 --- a/butane_test_macros/src/lib.rs +++ b/butane_test_macros/src/lib.rs @@ -1,4 +1,4 @@ -//! Macros for butane tests +//! Macros for butane tests. use proc_macro::TokenStream; use proc_macro2::Span; @@ -176,7 +176,7 @@ fn make_ident(name: &str) -> Ident { Ident::new(name, Span::call_site()) } -/// Options for butane_test +/// Options for butane_test. #[derive(PartialEq, Eq)] enum TestOption { Sync, From ae9dbc07f5ab59431feac67a11badef3f202bd6a Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Sun, 27 Oct 2024 18:52:35 +0800 Subject: [PATCH 75/78] undo some unnecessary changes to migration-tests --- butane/tests/migration-tests.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/butane/tests/migration-tests.rs b/butane/tests/migration-tests.rs index 95d7a388..31ea8132 100644 --- a/butane/tests/migration-tests.rs +++ b/butane/tests/migration-tests.rs @@ -177,7 +177,7 @@ fn current_migration_custom_type() { #[test] fn migration_add_field_sqlite() { migration_add_field( - &mut butane_test_helper::sqlite_connection(), + &mut sqlite_connection(), "ALTER TABLE Foo ADD COLUMN baz INTEGER NOT NULL DEFAULT 0;", // The exact details of futzing a DROP COLUMN in sqlite aren't // important (e.g. the temp table naming is certainly not part @@ -188,7 +188,7 @@ fn migration_add_field_sqlite() { "CREATE TABLE Foo__butane_tmp (id INTEGER NOT NULL PRIMARY KEY,bar TEXT NOT NULL); INSERT INTO Foo__butane_tmp SELECT id, bar FROM Foo;DROP TABLE Foo; ALTER TABLE Foo__butane_tmp RENAME TO Foo;", - ) + ); } #[cfg(feature = "pg")] @@ -212,7 +212,7 @@ fn migration_add_field_with_default_sqlite() { r#"CREATE TABLE Foo__butane_tmp (id INTEGER NOT NULL PRIMARY KEY,bar TEXT NOT NULL); INSERT INTO Foo__butane_tmp SELECT id, bar FROM Foo; DROP TABLE Foo;ALTER TABLE Foo__butane_tmp RENAME TO Foo;"#, - ) + ); } #[cfg(feature = "pg")] @@ -223,7 +223,7 @@ fn migration_add_field_with_default_pg() { &mut conn, "ALTER TABLE Foo ADD COLUMN baz BIGINT NOT NULL DEFAULT 42;", "ALTER TABLE Foo DROP COLUMN baz;", - ) + ); } #[cfg(feature = "pg")] @@ -290,7 +290,7 @@ fn migration_add_and_remove_field_sqlite() { CREATE TABLE Foo__butane_tmp (id INTEGER NOT NULL PRIMARY KEY,bar TEXT NOT NULL); INSERT INTO Foo__butane_tmp SELECT id, bar FROM Foo;DROP TABLE Foo; ALTER TABLE Foo__butane_tmp RENAME TO Foo;"#, - ) + ); } #[cfg(feature = "pg")] @@ -311,7 +311,7 @@ fn migration_delete_table_sqlite() { &mut sqlite_connection(), "DROP TABLE Foo;", "CREATE TABLE Foo (id INTEGER NOT NULL PRIMARY KEY,bar TEXT NOT NULL);", - ) + ); } #[cfg(feature = "pg")] From a3715d768b8db2def39f01e98e55e30909652099 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Sun, 27 Oct 2024 19:33:55 +0800 Subject: [PATCH 76/78] rv changes to connect() --- butane_core/src/db/mod.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/butane_core/src/db/mod.rs b/butane_core/src/db/mod.rs index 7344e692..1ada6e88 100644 --- a/butane_core/src/db/mod.rs +++ b/butane_core/src/db/mod.rs @@ -572,10 +572,9 @@ pub fn get_backend(name: &str) -> Option> { /// /// For non-boxed connections, see individual [`Backend`] implementations. pub fn connect(spec: &ConnectionSpec) -> Result { - let conn = get_backend(&spec.backend_name) + get_backend(&spec.backend_name) .ok_or_else(|| Error::UnknownBackend(spec.backend_name.clone()))? - .connect(&spec.conn_str)?; - Ok(conn) + .connect(&spec.conn_str) } /// Connect to a database async. From c5d064fb7478b3721ed97021d4b890acd1c5a1b0 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Sun, 27 Oct 2024 19:49:24 +0800 Subject: [PATCH 77/78] tidy --- butane_core/src/db/sync_adapter.rs | 2 +- examples/getting_started/src/lib.rs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/butane_core/src/db/sync_adapter.rs b/butane_core/src/db/sync_adapter.rs index f66eb639..38c0914a 100644 --- a/butane_core/src/db/sync_adapter.rs +++ b/butane_core/src/db/sync_adapter.rs @@ -48,7 +48,7 @@ impl SyncAdapter { } } - /// Creates a new SyncAdapter for a different type, using the same runtime + /// Creates a new SyncAdapter for a different type, using the same runtime. fn chain(&self, inner: S) -> SyncAdapter { SyncAdapter { runtime_handle: self.runtime_handle.clone(), diff --git a/examples/getting_started/src/lib.rs b/examples/getting_started/src/lib.rs index 119c733c..ab2b7270 100644 --- a/examples/getting_started/src/lib.rs +++ b/examples/getting_started/src/lib.rs @@ -5,8 +5,7 @@ pub mod butane_migrations; pub mod models; -use butane::db::Connection; -use butane::db::ConnectionSpec; +use butane::db::{Connection, ConnectionSpec}; use butane::migrations::Migrations; use butane::prelude::*; use models::{Blog, Post}; From ece48ee399108727ef8982df598da0b4156b553c Mon Sep 17 00:00:00 2001 From: James Oakley Date: Sun, 27 Oct 2024 15:24:03 -0400 Subject: [PATCH 78/78] CR comments and continued self-CR --- butane/src/lib.rs | 4 +- butane_core/src/db/connmethods.rs | 5 +- butane_core/src/db/mod.rs | 33 ++++++++---- butane_core/src/db/pg.rs | 2 +- butane_core/src/db/sqlite.rs | 1 + butane_core/src/db/sync_adapter.rs | 1 + butane_core/src/lib.rs | 4 +- butane_core/src/migrations/mod.rs | 2 +- butane_core/src/query/mod.rs | 84 +++++++++++++----------------- 9 files changed, 69 insertions(+), 67 deletions(-) diff --git a/butane/src/lib.rs b/butane/src/lib.rs index 0447efef..4f5f0d21 100644 --- a/butane/src/lib.rs +++ b/butane/src/lib.rs @@ -210,9 +210,9 @@ pub mod prelude_async { } pub mod internal { - //! Internals which will we be used in macro-generated code. + //! Internals used in macro-generated code. //! - //! Do not use directly. + //! Do not use directly. Semver-exempt. pub use async_trait::async_trait; diff --git a/butane_core/src/db/connmethods.rs b/butane_core/src/db/connmethods.rs index 330d228c..934e021f 100644 --- a/butane_core/src/db/connmethods.rs +++ b/butane_core/src/db/connmethods.rs @@ -155,8 +155,7 @@ impl VecRows { VecRows { rows, idx: 0 } } } -//todo should we still have this feature -//#[cfg(feature = "async-adapter")] + pub(crate) fn vec_from_backend_rows<'a>( mut other: Box, columns: &[Column], @@ -197,8 +196,6 @@ pub(crate) struct VecRow { values: Vec, } -//todo should this still be a separate feature -//#[cfg(feature = "async-adapter")] impl VecRow { fn new(original: &(dyn BackendRow), columns: &[Column]) -> Result { if original.len() != columns.len() { diff --git a/butane_core/src/db/mod.rs b/butane_core/src/db/mod.rs index 1ada6e88..356d9a8d 100644 --- a/butane_core/src/db/mod.rs +++ b/butane_core/src/db/mod.rs @@ -28,8 +28,6 @@ use serde::{Deserialize, Serialize}; use crate::query::{BoolExpr, Order}; use crate::{migrations::adb, Error, Result, SqlVal, SqlValRef}; -// todo figure this out -//#[cfg(feature = "async-adapter")] mod adapter; pub(crate) mod dummy; use dummy::DummyConnection; @@ -56,6 +54,9 @@ pub mod r2; use crate::connection_method_wrapper; mod internal { + // AsyncRequiresSend and AsyncRequiresSync are used to conditionally add bounds + // to types only in their async version. + #[maybe_async_cfg::maybe(sync())] pub trait AsyncRequiresSend {} #[maybe_async_cfg::maybe(idents(AsyncRequiresSend), sync())] @@ -103,9 +104,9 @@ pub trait BackendConnection: ConnectionMethods + Debug + Send { #[maybe_async_cfg::maybe( idents( - BackendConnection(sync = "BackendConnection", async = "BackendConnectionAsync"), - Connection(sync = "Connection", async = "ConnectionAsync"), - Transaction(sync = "Transaction", async = "TransactionAsync") + BackendConnection(sync = "BackendConnection"), + Connection(sync = "Connection"), + Transaction(sync = "Transaction") ), keep_self, sync(), @@ -211,7 +212,7 @@ impl ConnectionMethods for Box { )] #[derive(Debug)] pub struct Connection { - pub(super) conn: Box, + conn: Box, } #[maybe_async_cfg::maybe( @@ -232,6 +233,10 @@ impl Connection { Ok(self.conn.as_ref()) } + /// Consume this connection and convert it into an async one. + /// Note that the under the hood this adds an adapter layer which runs + /// the synchronous connection on a separate thread -- it is not "natively" + /// async. #[maybe_async_cfg::only_if(key = "sync")] pub fn into_async(self) -> Result { Ok(adapter::AsyncAdapter::new(|| Ok(self))?.into_connection()) @@ -270,6 +275,9 @@ impl Connection { } impl ConnectionAsync { + /// Consume this connection and convert it into a synchronous one. + /// Note that the under the hood this adds an adapter layer which drives + /// the async connection -- the async machinery is not eliminated. pub fn into_sync(self) -> Result { Ok(SyncAdapter::new(self)?.into_connection()) } @@ -307,7 +315,9 @@ connection_method_wrapper!(Connection); async() )] #[async_trait] -pub trait BackendTransaction<'c>: ConnectionMethods + internal::AsyncRequiresSend + Debug { +pub(super) trait BackendTransaction<'c>: + ConnectionMethods + internal::AsyncRequiresSend + Debug +{ /// Commit the transaction. /// /// Unfortunately because we use this as a trait object, we can't consume self. @@ -369,8 +379,8 @@ connection_method_wrapper!(Transaction<'_>); #[maybe_async_cfg::maybe( idents( - BackendTransaction(sync = "BackendTransaction", async = "BackendTransactionAsync"), - ConnectionMethods(sync = "ConnectionMethods", async = "ConnectionMethodsAsync") + BackendTransaction(sync = "BackendTransaction"), + ConnectionMethods(sync = "ConnectionMethods") ), sync(keep_self), async() @@ -490,7 +500,11 @@ impl<'bt> ConnectionMethods for Box + 'bt> { pub trait Backend: Send + Sync + DynClone { fn name(&self) -> &'static str; fn create_migration_sql(&self, current: &adb::ADB, ops: Vec) -> Result; + /// Establish a new sync connection. The format of the connection + /// string is backend-dependent. fn connect(&self, conn_str: &str) -> Result; + /// Establish a new async connection. The format of the connection + /// string is backend-dependent. async fn connect_async(&self, conn_str: &str) -> Result; } @@ -540,7 +554,6 @@ fn conn_complete_if_dir(path: &Path) -> Cow { } } -/// Database backend. A boxed implementation can be returned by name via [`get_backend`]. #[async_trait] impl Backend for Box { fn name(&self) -> &'static str { diff --git a/butane_core/src/db/pg.rs b/butane_core/src/db/pg.rs index 140eeb73..ee4db80e 100644 --- a/butane_core/src/db/pg.rs +++ b/butane_core/src/db/pg.rs @@ -52,7 +52,7 @@ impl Backend for PgBackend { } fn connect(&self, path: &str) -> Result { - debug!("connecting via sync adapter"); + debug!("Postgres connecting via sync adapter"); let conn = SyncAdapter::new(self.clone())?.connect(path)?; Ok(conn) } diff --git a/butane_core/src/db/sqlite.rs b/butane_core/src/db/sqlite.rs index b1558d82..76668e2a 100644 --- a/butane_core/src/db/sqlite.rs +++ b/butane_core/src/db/sqlite.rs @@ -120,6 +120,7 @@ impl SQLiteConnection { Ok(&self.conn) } } + impl ConnectionMethods for SQLiteConnection { fn execute(&self, sql: &str) -> Result<()> { ConnectionMethods::execute(self.wrapped_connection_methods()?, sql) diff --git a/butane_core/src/db/sync_adapter.rs b/butane_core/src/db/sync_adapter.rs index 38c0914a..5f6acfdc 100644 --- a/butane_core/src/db/sync_adapter.rs +++ b/butane_core/src/db/sync_adapter.rs @@ -12,6 +12,7 @@ use crate::migrations::adb; use crate::query::{BoolExpr, Order}; use crate::{debug, Column, Result, SqlVal, SqlValRef}; +/// Adapter that allows running synchronous operations on an async type. #[derive(Debug)] pub struct SyncAdapter { runtime_handle: tokio::runtime::Handle, diff --git a/butane_core/src/lib.rs b/butane_core/src/lib.rs index a2e67fc0..c4bbeab8 100644 --- a/butane_core/src/lib.rs +++ b/butane_core/src/lib.rs @@ -1,7 +1,7 @@ //! Library providing functionality used by butane macros and tools. -#![deny(missing_docs)] #![allow(clippy::iter_nth_zero)] #![allow(clippy::upper_case_acronyms)] //grandfathered, not going to break API to rename +#![deny(missing_docs)] use std::borrow::Borrow; use std::cmp::{Eq, PartialEq}; @@ -116,7 +116,7 @@ pub trait DataObject: DataResult + internal::DataObjectInternal + Sy #[maybe_async_cfg::maybe( idents( ConnectionMethods(sync = "ConnectionMethods"), - save_many_to_many(sync = "save_many_to_many_sync", async = "save_many_to_many_async"), + save_many_to_many(snake), QueryOps, ), sync(), diff --git a/butane_core/src/migrations/mod.rs b/butane_core/src/migrations/mod.rs index bb2f7693..3efc4d8e 100644 --- a/butane_core/src/migrations/mod.rs +++ b/butane_core/src/migrations/mod.rs @@ -30,7 +30,7 @@ mod memmigrations; pub use memmigrations::{MemMigration, MemMigrations}; /// A collection of migrations. -#[async_trait] +#[allow(async_fn_in_trait)] // We don't expect to need to change the Send bounds of the future. pub trait Migrations: Clone { type M: Migration; diff --git a/butane_core/src/query/mod.rs b/butane_core/src/query/mod.rs index 742714b5..10f7d670 100644 --- a/butane_core/src/query/mod.rs +++ b/butane_core/src/query/mod.rs @@ -198,54 +198,44 @@ impl Clone for Query { } } -mod private { - use super::*; - - /// Internal QueryOps helpers. - #[allow(async_fn_in_trait)] // Not truly a public trait - #[maybe_async_cfg::maybe( - idents(ConnectionMethods(sync = "ConnectionMethods")), - sync(), - async() - )] - pub trait QueryOpsInternal { - async fn fetch( - self, - conn: &impl ConnectionMethods, - limit: Option, - ) -> Result>; - } - #[maybe_async_cfg::maybe( - idents(ConnectionMethods(sync = "ConnectionMethods"), QueryOpsInternal), - keep_self, - sync(), - async() - )] - impl QueryOpsInternal for Query { - async fn fetch( - self, - conn: &impl ConnectionMethods, - limit: Option, - ) -> Result> { - let sort = if self.sort.is_empty() { - None - } else { - Some(self.sort.as_slice()) - }; - conn.query( - &self.table, - T::COLUMNS, - self.filter, - limit, - self.offset, - sort, - ) - .await - } +/// Internal QueryOps helpers. +#[allow(async_fn_in_trait)] // Not truly a public trait +#[maybe_async_cfg::maybe(idents(ConnectionMethods(sync = "ConnectionMethods")), sync(), async())] +trait QueryOpsInternal { + async fn fetch( + self, + conn: &impl ConnectionMethods, + limit: Option, + ) -> Result>; +} +#[maybe_async_cfg::maybe( + idents(ConnectionMethods(sync = "ConnectionMethods"), QueryOpsInternal), + keep_self, + sync(), + async() +)] +impl QueryOpsInternal for Query { + async fn fetch( + self, + conn: &impl ConnectionMethods, + limit: Option, + ) -> Result> { + let sort = if self.sort.is_empty() { + None + } else { + Some(self.sort.as_slice()) + }; + conn.query( + &self.table, + T::COLUMNS, + self.filter, + limit, + self.offset, + sort, + ) + .await } } -use private::QueryOpsInternalAsync; -use private::QueryOpsInternalSync; /// [`Query`] operations which require a `Connection` #[allow(async_fn_in_trait)] // Not intended to be implemented outside Butane @@ -254,7 +244,7 @@ use private::QueryOpsInternalSync; sync(), async() )] -pub trait QueryOps: QueryOpsInternal { +pub trait QueryOps { /// Executes the query against `conn` and returns the first result (if any). async fn load_first(self, conn: &impl ConnectionMethods) -> Result>;