diff --git a/Cargo.lock b/Cargo.lock index b0de3ac6..0b433a8d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -139,6 +139,28 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.75", +] + [[package]] name = "async-trait" version = "0.1.81" @@ -147,7 +169,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.75", ] [[package]] @@ -239,19 +261,21 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" name = "butane" version = "0.7.0" dependencies = [ + "async-trait", "butane_codegen", "butane_core", "butane_test_helper", + "butane_test_macros", "cfg-if", "chrono", "env_logger", "fake", "geo-types", "log", + "maybe-async-cfg", "nonempty", "once_cell", "paste", - "postgres", "proc-macro2", "quote", "r2d2", @@ -260,6 +284,9 @@ dependencies = [ "serde", "serde_json", "sqlparser", + "tokio", + "tokio-postgres", + "tokio-test", "uuid", ] @@ -289,7 +316,7 @@ dependencies = [ "proc-macro2", "quote", "serde_variant", - "syn", + "syn 2.0.75", ] [[package]] @@ -297,24 +324,29 @@ name = "butane_core" version = "0.7.0" dependencies = [ "assert_matches", + "async-trait", "butane_core", "butane_test_helper", + "butane_test_macros", "bytes", "cfg-if", "chrono", + "crossbeam-channel", + "dyn-clone", "env_logger", "fake", "fallible-iterator 0.3.0", "fallible-streaming-iterator", "fs2", + "futures-util", "hex", "log", + "maybe-async-cfg", "native-tls", "nonempty", "once_cell", "paste", "pin-project", - "postgres", "postgres-native-tls", "proc-macro2", "quote", @@ -325,9 +357,11 @@ dependencies = [ "serde", "serde_json", "sqlparser", - "syn", + "syn 2.0.75", "tempfile", "thiserror", + "tokio", + "tokio-postgres", "uuid", ] @@ -337,16 +371,27 @@ version = "0.7.0" dependencies = [ "block-id", "butane_core", + "env_logger", "libc", "log", + "maybe-async-cfg", "nonempty", "once_cell", - "postgres", "rand", "tempfile", + "tokio-postgres", "uuid", ] +[[package]] +name = "butane_test_macros" +version = "0.7.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.75", +] + [[package]] name = "bytemuck" version = "1.17.0" @@ -476,7 +521,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.75", ] [[package]] @@ -529,6 +574,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + [[package]] name = "crypto-common" version = "0.1.6" @@ -560,7 +620,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.75", ] [[package]] @@ -571,7 +631,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn", + "syn 2.0.75", ] [[package]] @@ -612,9 +672,15 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn", + "syn 2.0.75", ] +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + [[package]] name = "env_filter" version = "0.1.2" @@ -654,6 +720,7 @@ version = "0.1.0" dependencies = [ "assert_cmd", "butane", + "tokio", ] [[package]] @@ -780,7 +847,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.75", ] [[package]] @@ -832,7 +899,7 @@ checksum = "8796f322e43105351a7ec35148807b32b5b6058a539656dafe4a5b456d5ca41f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.75", ] [[package]] @@ -875,12 +942,29 @@ dependencies = [ "butane_cli", "butane_core", "butane_test_helper", + "butane_test_macros", "cfg-if", "env_logger", "log", "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" @@ -1033,6 +1117,42 @@ 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 = "git+https://github.com/nvksv/maybe-async-cfg.git?rev=b35f2f42e9b12a25fc731376fe6cdcf41869b2da#b35f2f42e9b12a25fc731376fe6cdcf41869b2da" +dependencies = [ + "manyhow", + "proc-macro2", + "pulldown-cmark", + "quote", + "syn 1.0.109", +] + [[package]] name = "md-5" version = "0.10.6" @@ -1095,6 +1215,7 @@ dependencies = [ "butane_cli", "butane_core", "butane_test_helper", + "butane_test_macros", "cfg-if", "env_logger", "fake", @@ -1103,6 +1224,7 @@ dependencies = [ "paste", "serde", "serde_json", + "tokio", "uuid", ] @@ -1160,7 +1282,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.75", ] [[package]] @@ -1251,7 +1373,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.75", ] [[package]] @@ -1272,20 +1394,6 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" -[[package]] -name = "postgres" -version = "0.19.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c9ec84ab55b0f9e418675de50052d494ba893fd28c65769a6e68fcdacbee2b8" -dependencies = [ - "bytes", - "fallible-iterator 0.2.0", - "futures-util", - "log", - "tokio", - "tokio-postgres", -] - [[package]] name = "postgres-native-tls" version = "0.5.0" @@ -1368,6 +1476,17 @@ dependencies = [ "termtree", ] +[[package]] +name = "proc-macro-utils" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071" +dependencies = [ + "proc-macro2", + "quote", + "smallvec", +] + [[package]] name = "proc-macro2" version = "1.0.86" @@ -1377,6 +1496,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pulldown-cmark" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "679341d22c78c6c649893cbd6c3278dcbe9fc4faa62fea3a9296ae2b50c14625" +dependencies = [ + "bitflags 2.6.0", + "memchr", + "unicase", +] + [[package]] name = "quote" version = "1.0.36" @@ -1602,7 +1732,7 @@ checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.75", ] [[package]] @@ -1718,6 +1848,17 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.75" @@ -1775,7 +1916,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.75", ] [[package]] @@ -1805,9 +1946,21 @@ dependencies = [ "mio", "pin-project-lite", "socket2", + "tokio-macros", "windows-sys 0.52.0", ] +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.75", +] + [[package]] name = "tokio-native-tls" version = "0.3.1" @@ -1844,6 +1997,30 @@ dependencies = [ "whoami", ] +[[package]] +name = "tokio-stream" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-test" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" +dependencies = [ + "async-stream", + "bytes", + "futures-core", + "tokio", + "tokio-stream", +] + [[package]] name = "tokio-util" version = "0.7.11" @@ -1863,6 +2040,12 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "unicase" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" + [[package]] name = "unicode-bidi" version = "0.3.15" @@ -1964,7 +2147,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.75", "wasm-bindgen-shared", ] @@ -1986,7 +2169,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.75", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2215,5 +2398,5 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.75", ] diff --git a/Cargo.toml b/Cargo.toml index 8d18296b..20a0e399 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,9 +6,11 @@ members = [ "butane_codegen", "butane_core", "butane_test_helper", + "butane_test_macros", "example", "examples/newtype", "examples/getting_started", + "examples/getting_started_async", ] [workspace.package] @@ -18,23 +20,25 @@ repository = "https://github.com/Electron100/butane" version = "0.7.0" [workspace.dependencies] +async-trait = "0.1" butane = { version = "0.7", path = "butane" } butane_cli = { path = "butane_cli" } butane_core = { version = "0.7", path = "butane_core" } butane_codegen = { version = "0.7", 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", "std", ] } +crossbeam-channel = "0.5" env_logger = "0.11" fake = "2.6" log = "0.4" nonempty = "0.10" 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" @@ -45,8 +49,17 @@ serde_json = "1.0" sqlparser = "0.44" syn = { version = "2", features = ["extra-traits", "full"] } tempfile = "3.10" +tokio = { version = "1"} +tokio-postgres = "0.7" +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 0732dfec..338752d8 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ lint : lint-ci : doclint lint spellcheck check-fmt -check : build test doclint lint spellcheck check-fmt +check : build doclint lint spellcheck check-fmt test test : @@ -31,12 +31,13 @@ fmt : check-fmt : $(CARGO_NIGHTLY) fmt --check + editorconfig-checker 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/README.md b/README.md index 0b402633..b3c9af2f 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,34 @@ enabled: you will want to enable `sqlite` and/or `pg`: the API will win. ## Migration of Breaking Changes +### 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 #### `AutoPk` Replace model fields like diff --git a/async_checklist.md b/async_checklist.md new file mode 100644 index 00000000..08abb248 --- /dev/null +++ b/async_checklist.md @@ -0,0 +1,13 @@ +* [x] Clean up pattern for sync/async variants. Inconsistent between suffix and module +* [x] Tests should run against sync and async +* [x] Ensure Postgres works in sync +* [x] Re-enable R2D2 for sync +* [x] Fix `#[async_trait(?Send)]` to set up Send bound again as it's required for e.g. `tokio::spawn` +* [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/Cargo.toml b/butane/Cargo.toml index 7da760a1..4382bdef 100644 --- a/butane/Cargo.toml +++ b/butane/Cargo.toml @@ -27,11 +27,13 @@ tls = ["butane_core/tls"] uuid = ["butane_codegen/uuid", "butane_core/uuid"] [dependencies] +async-trait = { workspace = true } butane_codegen = { workspace = true } butane_core = { workspace = true } [dev-dependencies] butane_test_helper = { workspace = true } +butane_test_macros = { workspace = true } cfg-if = { workspace = true } paste = { workspace = true } chrono = { workspace = true } @@ -41,9 +43,12 @@ 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 } -postgres = { features = ["with-geo-types-0_7"], workspace = true } +tokio = { workspace = true, features = ["macros"] } +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 0e13f259..4f5f0d21 100644 --- a/butane/src/lib.rs +++ b/butane/src/lib.rs @@ -9,13 +9,12 @@ 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::internal; -pub use butane_core::many::Many; +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 mod db { @@ -160,23 +159,62 @@ macro_rules! colname { #[macro_export] macro_rules! find { ($dbobj:ident, $filter:expr, $conn:expr) => { - butane::query!($dbobj, $filter) - .limit(1) - .load($conn) + butane::query::QueryOpsSync::load(butane::query!($dbobj, $filter).limit(1), $conn) .and_then(|mut results| results.pop().ok_or(butane::Error::NoSuchObject)) }; } -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; +/// Like [`find`], but for async. +#[macro_export] +macro_rules! find_async { + ($dbobj:ident, $filter:expr, $conn:expr) => { + butane::query::QueryOpsAsync::load(butane::query!($dbobj, $filter).limit(1), $conn) + .await + .and_then(|mut results| results.pop().ok_or(butane::Error::NoSuchObject)) + }; +} +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 [`super::prelude_async`]. + //! + //! Its use is recommended, but not required. + + pub use super::prelude_common::*; + + pub use butane_core::db::BackendConnection; + 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 { + //! 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::BackendConnectionAsync; + 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 { + //! Internals used in macro-generated code. + //! + //! Do not use directly. Semver-exempt. + + pub use async_trait::async_trait; + + pub use butane_core::internal::*; +} diff --git a/butane/tests/basic.rs b/butane/tests/basic.rs index 790535a9..17243b04 100644 --- a/butane/tests/basic.rs +++ b/butane/tests/basic.rs @@ -1,12 +1,17 @@ #![allow(clippy::disallowed_names)] -use butane::db::Connection; -use butane::{butane_type, find, model, query, AutoPk, ForeignKey}; -use butane::{colname, prelude::*}; +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")] +use rusqlite; use serde::Serialize; +#[cfg(feature = "pg")] +use tokio_postgres as postgres; #[butane_type] pub type Whatsit = String; @@ -105,222 +110,226 @@ struct TimeHolder { pub when: chrono::DateTime, } -fn basic_crud(conn: Connection) { +#[butane_test] +async fn basic_crud(conn: ConnectionAsync) { //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 matches!(Foo::get(&conn, 1).err(), Some(butane::Error::NoSuchObject)) { - } else { - panic!("Expected NoSuchObject"); + assert!(foo3.delete(&conn).await.is_ok()); + match Foo::get(&conn, 1).await.err() { + Some(butane::Error::NoSuchObject) => (), + _ => 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) { +#[butane_test] +async fn basic_find(conn: ConnectionAsync) { //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(); + let found: Foo = find_async!(Foo, bar == 43, &conn).unwrap(); assert_eq!(found, foo2); } -testall!(basic_find); -fn basic_query(conn: Connection) { +#[butane_test] +async fn basic_query(conn: ConnectionAsync) { //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) { +#[butane_test] +async fn basic_query_delete(conn: ConnectionAsync) { //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) { +#[butane_test] +async fn string_pk(conn: ConnectionAsync) { 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) { +#[butane_test] +async fn foreign_key(conn: ConnectionAsync) { 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(); assert_eq!(foo2, foo3); } -testall!(foreign_key); -fn auto_pk(conn: Connection) { +#[butane_test] +async fn auto_pk(conn: ConnectionAsync) { 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) { +#[butane_test] +async fn only_pk(conn: ConnectionAsync) { let mut obj = HasOnlyPk::new(1); - obj.save(&conn).unwrap(); + obj.save(&conn).await.unwrap(); assert_eq!(obj.id, 1); // verify we can still save the object even though it has no // fields to modify - obj.save(&conn).unwrap(); + obj.save(&conn).await.unwrap(); // verify it didnt get a new id assert_eq!(obj.id, 1); } -testall!(only_pk); -fn only_auto_pk(conn: Connection) { +#[butane_test] +async fn only_auto_pk(conn: ConnectionAsync) { let mut obj = HasOnlyAutoPk::default(); - obj.save(&conn).unwrap(); + obj.save(&conn).await.unwrap(); let pk = obj.id; // verify we can still save the object even though it has no // fields to modify - obj.save(&conn).unwrap(); + obj.save(&conn).await.unwrap(); // verify it didnt get a new id assert_eq!(obj.id, pk); } -testall!(only_auto_pk); -fn basic_committed_transaction(mut conn: Connection) { - let tr = conn.transaction().unwrap(); +#[butane_test] +async fn basic_committed_transaction(mut conn: ConnectionAsync) { + 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) { +#[butane_test] +async fn basic_dropped_transaction(mut conn: ConnectionAsync) { // 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}"), } } -testall!(basic_dropped_transaction); -fn basic_rollback_transaction(mut conn: Connection) { - let tr = conn.transaction().unwrap(); +#[butane_test] +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 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}"), } } -testall!(basic_rollback_transaction); -fn basic_unique_field_error_on_non_unique(conn: Connection) { +#[butane_test] +async fn basic_unique_field_error_on_non_unique(conn: ConnectionAsync) { 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")] @@ -335,32 +344,32 @@ fn basic_unique_field_error_on_non_unique(conn: Connection) { _ => false, }); } -testall!(basic_unique_field_error_on_non_unique); -fn fkey_same_type(conn: Connection) { +#[butane_test] +async fn fkey_same_type(conn: ConnectionAsync) { 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 cant_save_unsaved_fkey(conn: Connection) { +#[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).is_err()); + assert!(bar.save(&conn).await.is_err()); } -testall!(cant_save_unsaved_fkey); #[cfg(feature = "datetime")] -fn basic_time(conn: Connection) { +#[butane_test] +async fn basic_time(conn: ConnectionAsync) { let now = Utc::now(); let mut time = TimeHolder { id: 1, @@ -368,49 +377,52 @@ fn basic_time(conn: Connection) { utc: now, when: now, }; - 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.utc.timestamp(), time2.utc.timestamp()); } -#[cfg(feature = "datetime")] -testall!(basic_time); -fn basic_load_first(conn: Connection) { +#[butane_test] +async fn basic_load_first(conn: ConnectionAsync) { //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) { +#[butane_test] +async fn basic_load_first_ordered(conn: ConnectionAsync) { //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)); @@ -419,16 +431,17 @@ 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)); } -testall!(basic_load_first_ordered); -fn save_upserts_by_default(conn: Connection) { +#[butane_test] +async fn save_upserts_by_default(conn: ConnectionAsync) { let mut foo = Foo::new(1); foo.bar = 42; - foo.save(&conn).unwrap(); + foo.save(&conn).await.unwrap(); // Create another foo object with the same primary key, // but a different bar value. @@ -436,9 +449,18 @@ fn save_upserts_by_default(conn: Connection) { foo.bar = 43; // Save should do an upsert, so it will update the bar value // rather than throwing a conflict - foo.save(&conn).unwrap(); + foo.save(&conn).await.unwrap(); - let retrieved = Foo::get(&conn, 1).unwrap(); + let retrieved = Foo::get(&conn, 1).await.unwrap(); assert_eq!(retrieved.bar, 43); } -testall!(save_upserts_by_default); + +#[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/tests/common/blog.rs b/butane/tests/common/blog.rs index 257a6421..1bb630ff 100644 --- a/butane/tests/common/blog.rs +++ b/butane/tests/common/blog.rs @@ -1,8 +1,10 @@ //! Helpers for several tests. #![allow(dead_code)] // not all parts used in all tests - -use butane::{dataresult, model, DataObject}; -use butane::{db::Connection, 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")] @@ -71,6 +73,7 @@ impl Post { #[cfg(feature = "datetime")] #[dataresult(Post)] +#[allow(unused)] // Not all test files use it. pub struct PostMetadata { pub id: i64, pub title: String, @@ -101,9 +104,18 @@ impl Tag { } } -pub fn create_tag(conn: &Connection, name: &str) -> Tag { +#[maybe_async_cfg::maybe( + sync(), + async(keep_self), + idents( + DataObjectOpsAsync(async = "DataObjectOpsAsync", sync = "DataObjectOpsSync"), + ConnectionAsync(async = "ConnectionAsync", sync = "Connection") + ) +)] +pub async fn create_tag(conn: &ConnectionAsync, name: &str) -> Tag { + use butane::DataObjectOpsAsync; let mut tag = Tag::new(name); - tag.save(conn).unwrap(); + tag.save(conn).await.unwrap(); tag } @@ -111,14 +123,24 @@ 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) { +#[maybe_async_cfg::maybe( + sync(), + async(keep_self), + idents( + DataObjectOps, + Connection(async = "ConnectionAsync", sync = "Connection"), + create_tag(async = "create_tag", snake), + ) +)] +pub async fn setup_blog(conn: &Connection) { + use butane::DataObjectOps; 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, @@ -134,7 +156,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, @@ -144,7 +166,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, @@ -155,7 +177,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, @@ -165,5 +187,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 3b3a7fd7..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::Connection; -use butane::prelude::*; 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,32 +23,33 @@ impl HasCustomField2 { } } -fn roundtrip_custom_type(conn: Connection) { +#[butane_test] +async fn roundtrip_custom_type(conn: ConnectionAsync) { //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) { +#[butane_test] +async fn query_custom_type(conn: ConnectionAsync) { //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) } -testall!(query_custom_type); #[test] fn enum_to_sql() { diff --git a/butane/tests/custom_pg.rs b/butane/tests/custom_pg.rs index 7e445a52..84b5a440 100644 --- a/butane/tests/custom_pg.rs +++ b/butane/tests/custom_pg.rs @@ -2,10 +2,12 @@ #[cfg(feature = "pg")] mod custom_pg { use butane::custom::{SqlTypeCustom, SqlValRefCustom}; - use butane::prelude::*; - use butane::{butane_type, db::Connection, 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; // newtype so we can implement traits for it. #[butane_type(Custom(POINT))] @@ -56,21 +58,22 @@ mod custom_pg { pt_to: Point, } - fn roundtrip_custom(conn: Connection) { + #[butane_test(pg)] + async fn roundtrip_custom(conn: ConnectionAsync) { let mut trip = Trip { id: AutoPk::uninitialized(), pt_from: Point::new(0.0, 0.0), pt_to: Point::new(8.0, 9.0), }; - 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); /* - 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 { @@ -90,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 668a1ab9..c2dc77fa 100644 --- a/butane/tests/custom_type.rs +++ b/butane/tests/custom_type.rs @@ -1,8 +1,8 @@ -use butane::db::Connection; -use butane::prelude::*; +use butane::db::ConnectionAsync; 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,29 +60,30 @@ impl HasCustomField { } } -fn roundtrip_custom_type(conn: Connection) { +#[butane_test] +async fn roundtrip_custom_type(conn: ConnectionAsync) { //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) { +#[butane_test] +async fn query_custom_type(conn: ConnectionAsync) { //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) } -testall!(query_custom_type); diff --git a/butane/tests/fake.rs b/butane/tests/fake.rs index 42cf9ef4..b873a350 100644 --- a/butane/tests/fake.rs +++ b/butane/tests/fake.rs @@ -1,32 +1,33 @@ -use butane::db::Connection; -use butane::{find, DataObject, 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}; -fn fake_blog_post(conn: Connection) { +#[butane_test] +async fn fake_blog_post(conn: ConnectionAsync) { 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(); + 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).unwrap().count(), 3); + 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 73d1bc37..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::*; -use butane::{db::Connection, 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,25 +29,26 @@ impl FooJJ { } } -fn json_null(conn: Connection) { +#[butane_test] +async fn json_null(conn: ConnectionAsync) { // 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) { +#[butane_test] +async fn basic_json(conn: ConnectionAsync) { // create let id = 4; let mut foo = FooJJ::new(id); @@ -59,19 +63,18 @@ 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); #[model] #[derive(PartialEq, Eq, Debug, Clone)] @@ -89,7 +92,9 @@ impl FooHH { } } } -fn basic_hashmap(conn: Connection) { + +#[butane_test] +async fn basic_hashmap(conn: ConnectionAsync) { // create let id = 4; let mut foo = FooHH::new(id); @@ -97,19 +102,18 @@ 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); #[model] #[derive(PartialEq, Eq, Debug, Clone)] @@ -127,7 +131,9 @@ impl FooFullPrefixHashMap { } } } -fn basic_hashmap_full_prefix(conn: Connection) { + +#[butane_test] +async fn basic_hashmap_full_prefix(conn: ConnectionAsync) { // create let id = 4; let mut foo = FooFullPrefixHashMap::new(id); @@ -135,19 +141,18 @@ fn basic_hashmap_full_prefix(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 = FooFullPrefixHashMap::get(&conn, id).unwrap(); + let mut foo2 = FooFullPrefixHashMap::get(&conn, id).await.unwrap(); assert_eq!(foo, foo2); // update foo2.bar = 43; - foo2.save(&conn).unwrap(); - let foo3 = FooFullPrefixHashMap::get(&conn, id).unwrap(); + foo2.save(&conn).await.unwrap(); + let foo3 = FooFullPrefixHashMap::get(&conn, id).await.unwrap(); assert_eq!(foo2, foo3); } -testall!(basic_hashmap_full_prefix); #[model] #[derive(PartialEq, Eq, Debug, Clone)] @@ -165,7 +170,9 @@ impl FooBTreeMap { } } } -fn basic_btreemap(conn: Connection) { + +#[butane_test] +async fn basic_btreemap(conn: ConnectionAsync) { // create let id = 4; let mut foo = FooBTreeMap::new(id); @@ -173,19 +180,18 @@ fn basic_btreemap(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 = FooBTreeMap::get(&conn, id).unwrap(); + let mut foo2 = FooBTreeMap::get(&conn, id).await.unwrap(); assert_eq!(foo, foo2); // update foo2.bar = 43; - foo2.save(&conn).unwrap(); - let foo3 = FooBTreeMap::get(&conn, id).unwrap(); + foo2.save(&conn).await.unwrap(); + 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,7 +215,9 @@ impl FooHHO { } } } -fn hashmap_with_object_values(conn: Connection) { + +#[butane_test] +async fn hashmap_with_object_values(conn: ConnectionAsync) { // create let id = 4; let mut foo = FooHHO::new(id); @@ -217,19 +225,18 @@ 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); #[derive(PartialEq, Eq, Debug, Clone, FieldType, Serialize, Deserialize)] struct InlineFoo { @@ -255,20 +262,20 @@ impl OuterFoo { } } -fn inline_json(conn: Connection) { +#[butane_test] +async fn inline_json(conn: ConnectionAsync) { // 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 4bbe562a..ddd5e02a 100644 --- a/butane/tests/many.rs +++ b/butane/tests/many.rs @@ -1,12 +1,9 @@ -use butane::db::Connection; -use butane::prelude::*; 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,143 +44,146 @@ struct AutoItem { val: String, } -fn load_sorted_from_many(conn: Connection) { +#[butane_test] +async fn load_sorted_from_many(conn: ConnectionAsync) { 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(); - let post2 = Post::get(&conn, post.id).unwrap(); + let post2 = Post::get(&conn, post.id).await.unwrap(); let mut tag_iter = post2 .tags .load_ordered(&conn, OrderDirection::Ascending) + .await .unwrap(); assert_eq!(tag_iter.next().unwrap().tag, "cat"); assert_eq!(tag_iter.next().unwrap().tag, "european"); assert_eq!(tag_iter.next().unwrap().tag, "fast"); - let post3 = Post::get(&conn, post.id).unwrap(); + let post3 = Post::get(&conn, post.id).await.unwrap(); let mut tag_iter = post3 .tags .load_ordered(&conn, OrderDirection::Descending) + .await .unwrap(); assert_eq!(tag_iter.next().unwrap().tag, "fast"); assert_eq!(tag_iter.next().unwrap().tag, "european"); assert_eq!(tag_iter.next().unwrap().tag, "cat"); } -testall!(load_sorted_from_many); -fn remove_one_from_many(conn: Connection) { +#[butane_test] +async fn remove_one_from_many(conn: ConnectionAsync) { 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) { +#[butane_test] +async fn remove_multiple_from_many(conn: ConnectionAsync) { 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 delete_all_from_many(conn: Connection) { +#[butane_test] +async fn delete_all_from_many(conn: ConnectionAsync) { 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.save(&conn).unwrap(); + post.save(&conn).await.unwrap(); post.tags.add(&tag_striped).unwrap(); - post.tags.delete(&conn).unwrap(); + post.tags.delete(&conn).await.unwrap(); - let post2 = Post::get(&conn, post.id).unwrap(); - assert_eq!(post2.tags.load(&conn).unwrap().count(), 0); + let post2 = Post::get(&conn, post.id).await.unwrap(); + assert_eq!(post2.tags.load(&conn).await.unwrap().count(), 0); } -testall!(delete_all_from_many); -fn can_add_to_many_before_save(conn: Connection) { +#[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); 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) { +#[butane_test] +async fn cant_add_unsaved_to_many(_conn: ConnectionAsync) { let unsaved_item = AutoItem { id: AutoPk::uninitialized(), val: "shiny".to_string(), @@ -195,16 +195,15 @@ fn cant_add_unsaved_to_many(_conn: Connection) { .expect_err("unexpectedly not error"); assert!(matches!(err, butane::Error::ValueNotSaved)); } -testall!(cant_add_unsaved_to_many); -fn can_add_to_many_with_custom_table_name(conn: Connection) { +#[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")).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 4e21c58d..31ea8132 100644 --- a/butane/tests/migration-tests.rs +++ b/butane/tests/migration-tests.rs @@ -1,8 +1,8 @@ +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}; -use butane_core::db::{BackendConnection, Connection}; 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")] @@ -229,6 +229,7 @@ fn migration_add_field_with_default_pg() { #[cfg(feature = "pg")] #[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 diff --git a/butane/tests/nullable.rs b/butane/tests/nullable.rs index b3ebac2b..b3c243d4 100644 --- a/butane/tests/nullable.rs +++ b/butane/tests/nullable.rs @@ -1,7 +1,7 @@ -use butane::db::Connection; -use butane::prelude::*; +use butane::db::ConnectionAsync; use butane::{model, query}; use butane_test_helper::*; +use butane_test_macros::butane_test; #[model] #[derive(PartialEq, Eq, Debug)] @@ -15,56 +15,56 @@ impl WithNullable { } } -fn basic_optional(conn: Connection) { +#[butane_test] +async fn basic_optional(conn: ConnectionAsync) { 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) { +#[butane_test] +async fn query_optional_with_some(conn: ConnectionAsync) { 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)); assert_eq!(objs[1].foo, Some(44)); } -testall!(query_optional_with_some); -fn query_optional_with_none(conn: Connection) { +#[butane_test] +async fn query_optional_with_none(conn: ConnectionAsync) { 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); } -testall!(query_optional_with_none); diff --git a/butane/tests/query.rs b/butane/tests/query.rs index 0267e576..5981f518 100644 --- a/butane/tests/query.rs +++ b/butane/tests/query.rs @@ -1,8 +1,8 @@ -use butane::db::Connection; -use butane::prelude::*; +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,73 +10,79 @@ 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(); +#[butane_test] +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); posts.sort_by(|p1, p2| p1.id.partial_cmp(&p2.id).unwrap()); assert_eq!(posts[0].title, "The Tiger"); assert_eq!(posts[1].title, "Sir Charles"); assert_eq!(posts[2].title, "Mount Doom"); } -testall!(equality); -fn equality_separate_dataresult(conn: Connection) { - blog::setup_blog(&conn); - let mut posts = query!(PostMetadata, published == true).load(&conn).unwrap(); +#[butane_test] +async fn equality_separate_dataresult(conn: ConnectionAsync) { + 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"); assert_eq!(posts[1].title, "Sir Charles"); assert_eq!(posts[2].title, "Mount Doom"); } -testall!(equality_separate_dataresult); -fn ordered(conn: Connection) { - blog::setup_blog(&conn); +#[butane_test] +async fn ordered(conn: ConnectionAsync) { + 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"); assert_eq!(posts[1].title, "Sir Charles"); assert_eq!(posts[2].title, "The Tiger"); } -testall!(ordered); -fn comparison(conn: Connection) { - blog::setup_blog(&conn); - let mut posts = query!(Post, likes < 5).load(&conn).unwrap(); +#[butane_test] +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); posts.sort_by(|p1, p2| p1.id.partial_cmp(&p2.id).unwrap()); assert_eq!(posts[0].title, "The Tiger"); assert_eq!(posts[1].title, "Mt. Everest"); } -testall!(comparison); -fn like(conn: Connection) { - blog::setup_blog(&conn); - let mut posts = query!(Post, title.like("M%")).load(&conn).unwrap(); +#[butane_test] +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); posts.sort_by(|p1, p2| p1.id.partial_cmp(&p2.id).unwrap()); assert_eq!(posts[0].title, "Mount Doom"); assert_eq!(posts[1].title, "Mt. Everest"); } -testall!(like); -fn combination(conn: Connection) { - blog::setup_blog(&conn); +#[butane_test] +async fn combination(conn: ConnectionAsync) { + 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); +#[butane_test] +async fn combination_allof(conn: ConnectionAsync) { + blog::setup_blog(&conn).await; let posts = Post::query() .filter(BoolExpr::AllOf(vec![ filter!(Post, published == true), @@ -84,43 +90,46 @@ 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); +#[butane_test] +async fn not_found(conn: ConnectionAsync) { + 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); +#[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); -fn fkey_match(conn: Connection) { - blog::setup_blog(&conn); - 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(); +#[butane_test] +async fn fkey_match(conn: ConnectionAsync) { + blog::setup_blog(&conn).await; + 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; - 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); @@ -131,58 +140,62 @@ fn fkey_match(conn: Connection) { assert_eq!(posts, posts3); assert_eq!(posts, posts4); } -testall!(fkey_match); -fn many_load(conn: Connection) { - blog::setup_blog(&conn); - let post: Post = find!(Post, title == "The Tiger", &conn).unwrap(); - let tags = post.tags.load(&conn).unwrap(); +#[butane_test] +async fn many_load(conn: ConnectionAsync) { + blog::setup_blog(&conn).await; + 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); -fn many_serialize(conn: Connection) { - blog::setup_blog(&conn); - let post: Post = find!(Post, title == "The Tiger", &conn).unwrap(); +#[butane_test] +async fn many_serialize(conn: ConnectionAsync) { + blog::setup_blog(&conn).await; + 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).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"); assert_eq!(tags[1].tag, "danger"); } -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(); +#[butane_test] +async fn many_objects_with_tag(conn: ConnectionAsync) { + 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"); assert_eq!(posts[2].title, "Mt. Everest"); } -testall!(many_objects_with_tag); -fn many_objects_with_tag_explicit(conn: Connection) { - blog::setup_blog(&conn); +#[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")) .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"); assert_eq!(posts[2].title, "Mt. Everest"); } -testall!(many_objects_with_tag_explicit); +#[butane_test] #[cfg(feature = "datetime")] -fn by_timestamp(conn: Connection) { - blog::setup_blog(&conn); - let mut post = find!(Post, title == "Sir Charles", &conn).unwrap(); +async fn by_timestamp(conn: ConnectionAsync) { + blog::setup_blog(&conn).await; + 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) @@ -190,16 +203,16 @@ 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(); + 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() .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. @@ -214,36 +227,37 @@ 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"); } -#[cfg(feature = "datetime")] -testall!(by_timestamp); -fn limit(conn: Connection) { - blog::setup_blog(&conn); +#[butane_test] +async fn limit(conn: ConnectionAsync) { + 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"); assert_eq!(posts[1].title, "Mt. Everest"); } -testall!(limit); -fn offset(conn: Connection) { - blog::setup_blog(&conn); +#[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 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"); assert_eq!(posts[1].title, "The Tiger"); } -testall!(offset); 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 683b7699..85463bcf 100644 --- a/butane/tests/unmigrate.rs +++ b/butane/tests/unmigrate.rs @@ -1,20 +1,41 @@ //! 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::{Connection, ConnectionAsync}; use butane::migrations::{Migration, Migrations}; use butane_test_helper::*; +use butane_test_macros::*; -fn unmigrate(mut connection: Connection) { - let mem_migrations = create_current_migrations(&connection); +#[butane_test(async)] +async fn unmigrate_async(mut connection: ConnectionAsync) { + let mem_migrations = create_current_migrations(connection.backend()); - let migrations = mem_migrations.unapplied_migrations(&connection).unwrap(); + connection + .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(conn).unwrap(); + + let migrations = mem_migrations.unapplied_migrations(conn).unwrap(); + assert_eq!(migrations.len(), 1); + Ok(()) + }) + .await + .unwrap(); +} + +#[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 connection).unwrap(); + migration.downgrade(&mut conn).unwrap(); - let migrations = mem_migrations.unapplied_migrations(&connection).unwrap(); + let migrations = mem_migrations.unapplied_migrations(&conn).unwrap(); assert_eq!(migrations.len(), 1); } -testall!(unmigrate); diff --git a/butane/tests/uuid.rs b/butane/tests/uuid.rs index 5ad48205..5914afb0 100644 --- a/butane/tests/uuid.rs +++ b/butane/tests/uuid.rs @@ -1,7 +1,7 @@ -use butane::db::Connection; +use butane::db::ConnectionAsync; use butane::model; -use butane::prelude::*; use butane_test_helper::*; +use butane_test_macros::butane_test; use uuid_for_test::Uuid; #[model] @@ -16,22 +16,22 @@ impl FooUU { } } -fn basic_uuid(conn: Connection) { +#[butane_test] +async fn basic_uuid(conn: ConnectionAsync) { //create let id = Uuid::new_v4(); #[allow(clippy::disallowed_names)] 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 960c1864..b21d1cd0 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] default = ["pg", "sqlite"] diff --git a/butane_cli/src/lib.rs b/butane_cli/src/lib.rs index 61014215..f621d1a6 100644 --- a/butane_cli/src/lib.rs +++ b/butane_cli/src/lib.rs @@ -13,13 +13,15 @@ use std::{ path::{Path, PathBuf}, }; +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::{ 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; @@ -522,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()?; @@ -532,7 +534,7 @@ pub fn load_latest_migration_backends(base_dir: &Path) -> Result> = vec![]; + let mut backends: Vec> = vec![]; for backend_name in backend_names { backends.push( @@ -541,7 +543,7 @@ pub fn load_latest_migration_backends(base_dir: &Path) -> Result>::from_vec(backends).unwrap()); + return Ok(NonEmpty::>::from_vec(backends).unwrap()); } } Err(anyhow::anyhow!("There are no exiting migrations.")) @@ -549,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() { diff --git a/butane_core/Cargo.toml b/butane_core/Cargo.toml index 8aaebd05..7975d799 100644 --- a/butane_core/Cargo.toml +++ b/butane_core/Cargo.toml @@ -10,32 +10,40 @@ repository.workspace = true [features] -datetime = ["chrono", "postgres?/with-chrono-0_4"] +async-adapter = [] +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"] log = ["dep:log", "rusqlite?/trace"] -pg = ["bytes", "postgres"] -sqlite = ["rusqlite"] +pg = ["bytes", "tokio-postgres"] +sqlite = ["rusqlite", "async-adapter"] sqlite-bundled = ["rusqlite/bundled"] tls = ["native-tls", "postgres-native-tls"] [dependencies] +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.3" fallible-streaming-iterator = "0.1" 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 } nonempty.workspace = true once_cell = { workspace = true } pin-project = "1" -postgres = { optional = true, workspace = true } +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 } quote = { workspace = true } @@ -54,9 +62,11 @@ 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 +tokio = { workspace = true, features = ["macros"] } [[test]] name = "uuid" diff --git a/butane_core/src/codegen/dbobj.rs b/butane_core/src/codegen/dbobj.rs index 19c9c6b4..5416b7fe 100644 --- a/butane_core/src/codegen/dbobj.rs +++ b/butane_core/src/codegen/dbobj.rs @@ -37,26 +37,10 @@ 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)?; - ) - }) - .collect(); + let many_save_async = impl_many_save(ast_struct, config, true); + let many_save_sync = impl_many_save(ast_struct, config, false); - let conn_arg_name = if many_save.is_empty() { + let conn_arg_name = if many_save_sync.is_empty() { syn::Ident::new("_conn", Span::call_site()) } else { syn::Ident::new("conn", Span::call_site()) @@ -99,8 +83,18 @@ pub fn impl_dbobject(ast_struct: &ItemStruct, config: &Config) -> TokenStream2 { fn pk_mut(&mut self) -> &mut impl butane::PrimaryKeyType { &mut self.#pkident } - fn save_many_to_many(&mut self, #conn_arg_name: &impl butane::db::ConnectionMethods) -> butane::Result<()> { - #many_save + 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<()> { + #many_save_sync Ok(()) } #non_auto_values_fn @@ -215,6 +209,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() @@ -436,3 +431,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::ManyOpsAsync::save(&mut self.#ident, conn).await?;) + } else { + quote!(butane::ManyOpsSync::save(&mut self.#ident, conn)?;) + }; + + // Save needs to ensure_initialized + quote!( + self.#ident.ensure_init( + #many_table_lit, + butane::ToSql::to_sql(butane::DataObject::pk(self)), + #pksqltype, + ); + #save_with_conn + ) + }) + .collect(); +} diff --git a/butane_core/src/custom.rs b/butane_core/src/custom.rs index 1b577aca..70cd6dd6 100644 --- a/butane_core/src/custom.rs +++ b/butane_core/src/custom.rs @@ -11,11 +11,14 @@ use std::fmt; use serde::{Deserialize, Serialize}; +#[cfg(feature = "pg")] +use tokio_postgres as postgres; + /// For use with [SqlType::Custom](crate::SqlType) #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] 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 +140,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/adapter.rs b/butane_core/src/db/adapter.rs new file mode 100644 index 00000000..6108c603 --- /dev/null +++ b/butane_core/src/db/adapter.rs @@ -0,0 +1,417 @@ +//! Adapter between sync and async connections +//! Allows implementing an async trait in terms of synchronous +//! operations without blocking the task. It accomplishes this by +//! running the blocking operations on a dedicated thread and communicating +//! between threads. + +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/ + // https://docs.rs/crossbeam/0.8.2/crossbeam/fn.scope.html + // TODO ensure soundness and document why + 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/ + // https://docs.rs/crossbeam/0.8.2/crossbeam/fn.scope.html + // TODO ensure soundness and document why + 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); + 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 {} + +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: Debug + 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 {} + +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, + 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, + { + // TODO 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] +impl ConnectionMethodsAsync for AsyncAdapter +where + T: 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] +impl BackendConnectionAsync for AsyncAdapter +where + T: BackendConnection, +{ + async fn transaction<'c>(&'c mut self) -> Result> { + let transaction_ptr: SyncSendPtrMut = self + .invoke_mut(|conn| { + 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(TransactionAsync::new(Box::new(transaction_adapter))) + } + + fn backend(&self) -> Box { + // todo clean up unwrap + 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() + } +} + +impl AsyncAdapter +where + T: BackendConnection + 'static, +{ + pub fn into_connection(self) -> ConnectionAsync { + ConnectionAsync { + conn: Box::new(self), + } + } +} + +#[async_trait] +impl BackendTransactionAsync<'c> for AsyncAdapter +where + T: 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 ConnectionMethodsAsync { + self + } +} + +/// 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 + B: Backend + Clone + 'static, +{ + // 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/connmethods.rs b/butane_core/src/db/connmethods.rs index 27a4140e..934e021f 100644 --- a/butane_core/src/db/connmethods.rs +++ b/butane_core/src/db/connmethods.rs @@ -3,6 +3,8 @@ use std::ops::{Deref, DerefMut}; +use async_trait::async_trait; + use crate::query::{BoolExpr, Expr, Order}; use crate::{Result, SqlType, SqlVal, SqlValRef}; @@ -10,49 +12,61 @@ 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. -pub trait ConnectionMethods { - fn execute(&self, sql: &str) -> Result<()>; - fn query<'a, 'b, 'c: 'a>( +#[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, table: &str, - columns: &'b [Column], + columns: &[Column], expr: Option, limit: Option, offset: Option, sort: Option<&[Order]>, - ) -> Result>; - fn insert_returning_pk( + ) -> 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 - 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( + /// 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<()>; - fn update( + async fn update( &self, table: &str, pkcol: Column, - pk: SqlValRef, + pk: SqlValRef<'_>, 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 @@ -141,6 +155,17 @@ impl VecRows { VecRows { rows, idx: 0 } } } + +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, @@ -165,3 +190,45 @@ impl<'a> BackendRows for Box { self.deref().current() } } + +#[derive(Debug)] +pub(crate) struct VecRow { + values: Vec, +} + +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/dummy.rs b/butane_core/src/db/dummy.rs new file mode 100644 index 00000000..10a5905c --- /dev/null +++ b/butane_core/src/db/dummy.rs @@ -0,0 +1,135 @@ +//! 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; + +use super::*; +use crate::migrations::adb; +use crate::query::{BoolExpr, Order}; +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 { + "dummy" + } + fn create_migration_sql(&self, current: &adb::ADB, ops: Vec) -> Result { + Err(Error::PoisonedConnection) + } + fn connect(&self, conn_str: &str) -> Result { + Err(Error::PoisonedConnection) + } + async fn connect_async(&self, conn_str: &str) -> Result { + Err(Error::PoisonedConnection) + } +} + +/// 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 { + pub fn new() -> Self { + Self {} + } +} + +#[maybe_async_cfg::maybe( + idents(ConnectionMethods(sync = "ConnectionMethods", async = "ConnectionMethodsAsync")), + keep_self, + sync(), + async() +)] +#[async_trait] +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( + BackendConnection(sync = "BackendConnection"), + Transaction(sync = "Transaction") + ), + keep_self, + sync(), + async() +)] +#[async_trait] +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/macros.rs b/butane_core/src/db/macros.rs index 6a1f1897..a2c26ca4 100644 --- a/butane_core/src/db/macros.rs +++ b/butane_core/src/db/macros.rs @@ -1,23 +1,34 @@ #[macro_export] macro_rules! connection_method_wrapper { ($ty:path) => { + #[maybe_async_cfg::maybe( + idents( + Connection(sync = "Connection"), + ConnectionMethods(sync = "ConnectionMethods"), + Transaction(sync = "Transaction") + ), + sync(keep_self), + async() + )] + #[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 } - 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 } - fn insert_returning_pk( + async fn insert_returning_pk( &self, table: &str, columns: &[Column], @@ -26,8 +37,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], @@ -35,8 +47,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], @@ -45,23 +58,32 @@ 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, - pk: SqlValRef, + pk: SqlValRef<'_>, columns: &[Column], values: &[SqlValRef<'_>], ) -> Result<()> { self.wrapped_connection_methods()? .update(table, pkcol, pk, columns, values) + .await + } + async fn delete(&self, table: &str, pkcol: &'static str, pk: SqlVal) -> Result<()> { + self.wrapped_connection_methods()? + .delete(table, pkcol, pk) + .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 c1b35b73..356d9a8d 100644 --- a/butane_core/src/db/mod.rs +++ b/butane_core/src/db/mod.rs @@ -21,35 +21,78 @@ use std::io::Write; use std::ops::{Deref, DerefMut}; use std::path::Path; +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}; +mod adapter; +pub(crate) mod dummy; +use dummy::DummyConnection; +mod sync_adapter; +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; #[cfg(feature = "pg")] pub mod pg; + #[cfg(feature = "sqlite")] pub mod sqlite; #[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; +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())] + impl AsyncRequiresSend for T {} + + #[maybe_async_cfg::maybe(async())] + 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. -pub trait BackendConnection: ConnectionMethods + Debug + Send + 'static { +#[maybe_async_cfg::maybe( + idents( + AsyncRequiresSend, + ConnectionMethods(sync = "ConnectionMethods", async = "ConnectionMethodsAsync"), + Transaction(sync = "Transaction", async = "TransactionAsync"), + ), + sync(self = "BackendConnection"), + async(self = "BackendConnectionAsync") +)] +#[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 and aborted. - 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 for this connection. fn backend(&self) -> Box; /// Retrieve the backend name for this connection. @@ -59,25 +102,200 @@ pub trait BackendConnection: ConnectionMethods + Debug + Send + 'static { fn is_closed(&self) -> bool; } +#[maybe_async_cfg::maybe( + idents( + BackendConnection(sync = "BackendConnection"), + Connection(sync = "Connection"), + Transaction(sync = "Transaction") + ), + keep_self, + sync(), + async() +)] +#[async_trait] +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(sync = "BackendConnection"), + ConnectionMethods(sync = "ConnectionMethods") + ), + keep_self, + sync(), + async() +)] +#[async_trait] +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 = "BackendConnection")), + sync(keep_self), + async(self = "ConnectionAsync") +)] #[derive(Debug)] pub struct Connection { conn: Box, } + +#[maybe_async_cfg::maybe( + idents(BackendConnection(sync = "BackendConnection")), + sync(keep_self), + async() +)] impl Connection { - pub fn execute(&mut self, sql: impl AsRef) -> Result<()> { - self.conn.execute(sql.as_ref()) + pub fn new(conn: Box) -> Self { + Self { conn } } - // For use with connection_method_wrapper macro + pub async fn execute(&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()) } + + /// 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()) + } + + /// 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 + /// 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), + } + } +} + +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()) + } } + +#[maybe_async_cfg::maybe( + idents( + BackendConnection(sync = "BackendConnection"), + Connection(sync = "Connection"), + Transaction(sync = "Transaction") + ), + sync(keep_self), + async() +)] +#[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() @@ -91,6 +309,207 @@ impl BackendConnection for Connection { } connection_method_wrapper!(Connection); +#[maybe_async_cfg::maybe( + idents(ConnectionMethods(sync = "ConnectionMethods"), AsyncRequiresSend), + sync(keep_self), + async() +)] +#[async_trait] +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. + /// 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 = "BackendTransaction")), + sync(self = "Transaction"), + async() +)] +#[derive(Debug)] +pub struct Transaction<'c> { + pub(super) trans: Box + 'c>, +} + +#[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()) + } +} + +connection_method_wrapper!(Transaction<'_>); + +#[maybe_async_cfg::maybe( + idents( + BackendTransaction(sync = "BackendTransaction"), + ConnectionMethods(sync = "ConnectionMethods") + ), + sync(keep_self), + async() +)] +#[async_trait] +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( + BackendTransaction(sync = "BackendTransaction"), + ConnectionMethods(sync = "ConnectionMethods") + ), + keep_self, + sync(), + async() +)] +#[async_trait] +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(sync = "BackendTransaction"), + ConnectionMethods(sync = "ConnectionMethods") + ), + keep_self, + sync(), + async() +)] +#[async_trait] +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]. +#[async_trait] +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; +} + +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`. @@ -135,13 +554,7 @@ fn conn_complete_if_dir(path: &Path) -> Cow { } } -/// Database backend. A boxed implementation can be returned by name via [`get_backend`]. -pub trait Backend { - fn name(&self) -> &'static str; - fn create_migration_sql(&self, current: &adb::ADB, ops: Vec) -> Result; - fn connect(&self, conn_str: &str) -> Result; -} - +#[async_trait] impl Backend for Box { fn name(&self) -> &'static str { self.deref().name() @@ -152,6 +565,9 @@ impl Backend for Box { fn connect(&self, conn_str: &str) -> Result { self.deref().connect(conn_str) } + async fn connect_async(&self, conn_str: &str) -> Result { + self.deref().connect_async(conn_str).await + } } /// Find a backend by name. @@ -165,55 +581,21 @@ 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 { get_backend(&spec.backend_name) .ok_or_else(|| Error::UnknownBackend(spec.backend_name.clone()))? .connect(&spec.conn_str) } -trait BackendTransaction<'c>: ConnectionMethods + 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 - fn commit(&mut self) -> Result<()>; - /// Roll back the transaction. Same comment about consuming self as above. - fn rollback(&mut self) -> Result<()>; - - // Workaround for https://github.com/rust-lang/rfcs/issues/2765 - fn connection_methods(&self) -> &dyn ConnectionMethods; -} - -/// Database transaction. +/// Connect to a database async. /// -/// 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 fn commit(mut self) -> Result<()> { - self.trans.deref_mut().commit() - } - /// Roll back the transaction. Equivalent to dropping it. - pub fn rollback(mut self) -> Result<()> { - self.trans.deref_mut().rollback() - } - // 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()) - } +/// 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()))? + .connect_async(&spec.conn_str) + .await } - -connection_method_wrapper!(Transaction<'_>); diff --git a/butane_core/src/db/pg.rs b/butane_core/src/db/pg.rs index 64b54022..ee4db80e 100644 --- a/butane_core/src/db/pg.rs +++ b/butane_core/src/db/pg.rs @@ -1,41 +1,41 @@ //! Postgresql database backend use std::borrow::Cow; -use std::cell::RefCell; use std::fmt::{Debug, Write}; +use async_trait::async_trait; use bytes::BufMut; #[cfg(feature = "datetime")] use chrono::NaiveDateTime; -use postgres::fallible_iterator::FallibleIterator; -use postgres::GenericClient; +use futures_util::stream::StreamExt; +use tokio_postgres as postgres; +use tokio_postgres::GenericClient; use super::connmethods::VecRows; use super::helper; use crate::custom::{SqlTypeCustom, SqlValRefCustom}; use crate::db::{ - Backend, BackendConnection, BackendRow, BackendTransaction, Column, Connection, - ConnectionMethods, RawQueryResult, 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::{debug, query}; -use crate::{query::BoolExpr, Error, Result, SqlType, SqlVal, SqlValRef}; +use crate::query::{BoolExpr, Expr}; +use crate::{debug, query, warn, Error, Result, SqlType, SqlVal, SqlValRef}; /// The name of the postgres backend. pub const BACKEND_NAME: &str = "pg"; /// Postgres [`Backend`] implementation. -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct PgBackend; impl PgBackend { pub fn new() -> PgBackend { PgBackend {} } } -impl PgBackend { - fn connect(&self, params: &str) -> Result { - PgConnection::open(params) - } -} + +#[async_trait] impl Backend for PgBackend { fn name(&self) -> &'static str { BACKEND_NAME @@ -52,8 +52,14 @@ impl Backend for PgBackend { } fn connect(&self, path: &str) -> Result { - Ok(Connection { - conn: Box::new(self.connect(path)?), + debug!("Postgres connecting via sync adapter"); + let conn = SyncAdapter::new(self.clone())?.connect(path)?; + Ok(conn) + } + + async fn connect_async(&self, path: &str) -> Result { + Ok(ConnectionAsync { + conn: Box::new(PgConnection::open(path).await?), }) } } @@ -62,17 +68,19 @@ impl Backend for PgBackend { pub struct PgConnection { #[cfg(feature = "debug")] params: Box, - conn: RefCell, + client: postgres::Client, } + impl PgConnection { - fn open(params: &str) -> Result { - Ok(PgConnection { + async fn open(params: &str) -> Result { + let client = Self::connect(params).await?; + Ok(Self { #[cfg(feature = "debug")] params: params.into(), - conn: RefCell::new(Self::connect(params)?), + client, }) } - fn connect(params: &str) -> Result { + async fn connect(params: &str) -> Result { cfg_if::cfg_if! { if #[cfg(feature = "tls")] { let connector = native_tls::TlsConnector::new()?; @@ -81,18 +89,27 @@ impl PgConnection { let connector = postgres::NoTls; } } - Ok(postgres::Client::connect(params, connector)?) + let (client, conn) = postgres::connect(params, connector).await?; + 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) } } 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)) } @@ -103,7 +120,7 @@ impl BackendConnection for PgConnection { BACKEND_NAME } fn is_closed(&self) -> bool { - self.conn.borrow().is_closed() + self.client.is_closed() } } impl Debug for PgConnection { @@ -129,32 +146,35 @@ 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>; +trait PgConnectionLike { + type Client: postgres::GenericClient + Send; + 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())?; + // 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(()) } - 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(); @@ -185,24 +205,22 @@ where } let types: Vec = values.iter().map(pgtype_for_val).collect(); - let stmt = self - .cell()? - .try_borrow_mut()? - .prepare_typed(&sqlquery, types.as_ref())?; - // 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 future = self.client()?.prepare_typed(&sqlquery, types.as_ref()); + let stmt = future.await?; + let mut rowvec = Vec::::new(); + let future = self + .client()? + .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?; + 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], @@ -222,16 +240,24 @@ 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 future = self + .client()? + .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))); + 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, @@ -240,12 +266,11 @@ 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())?; + let future = self.client()?.execute(sql.as_str(), params.as_slice()); + future.await?; Ok(()) } - fn insert_or_replace( + async fn insert_or_replace( &self, table: &str, columns: &[Column], @@ -255,16 +280,15 @@ 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()? - .try_borrow_mut()? - .execute(sql.as_str(), params.as_slice())?; + let future = self.client()?.execute(sql.as_str(), params.as_slice()); + future.await?; Ok(()) } - fn update( + async fn update( &self, table: &str, pkcol: Column, - pk: SqlValRef, + pk: SqlValRef<'_>, columns: &[Column], values: &[SqlValRef<'_>], ) -> Result<()> { @@ -284,12 +308,16 @@ where if cfg!(feature = "log") { debug!("update sql {}", sql); } - self.cell()? - .try_borrow_mut()? - .execute(sql.as_str(), params.as_slice())?; + let future = self.client()?.execute(sql.as_str(), params.as_slice()); + 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(()) } - 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!( @@ -305,36 +333,34 @@ where &mut sql, ); 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())?; + let future = self.client()?.execute(sql.as_str(), params.as_slice()); + let cnt = future.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])?; + let future = self + .client()? + .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()) } } struct PgTransaction<'c> { - trans: Option>>, + trans: Option>, } impl<'c> PgTransaction<'c> { fn new(trans: postgres::Transaction<'c>) -> Self { - PgTransaction { - trans: Some(RefCell::new(trans)), - } + PgTransaction { 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 { @@ -352,22 +378,24 @@ impl<'c> Debug for 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?), } } - 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.rollback().await?), } } // Workaround for https://github.com/rust-lang/rfcs/issues/2765 diff --git a/butane_core/src/db/r2.rs b/butane_core/src/db/r2.rs index 281d75f0..c7d27bfe 100644 --- a/butane_core/src/db/r2.rs +++ b/butane_core/src/db/r2.rs @@ -1,12 +1,13 @@ //! R2D2 support for Butane. +use std::ops::Deref; + 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::{query::BoolExpr, query::Order, Result, SqlVal, SqlValRef}; /// R2D2 support for Butane. Implements [`r2d2::ManageConnection`]. #[derive(Clone, Debug)] @@ -36,4 +37,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_core/src/db/sqlite.rs b/butane_core/src/db/sqlite.rs index ac571b2d..76668e2a 100644 --- a/butane_core/src/db/sqlite.rs +++ b/butane_core/src/db/sqlite.rs @@ -7,22 +7,22 @@ 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; -use crate::connection_method_wrapper; -use crate::db::{ - Backend, BackendConnection, BackendRow, BackendRows, BackendTransaction, Column, Connection, - ConnectionMethods, RawQueryResult, Transaction, +use super::{helper, Backend, BackendRow, Column, RawQueryResult}; +use super::{ + BackendConnection, BackendTransaction, Connection, ConnectionAsync, ConnectionMethods, + Transaction, }; -use crate::debug; -use crate::migrations::adb::{AColumn, ARef, ATable, Operation, TypeIdentifier, ADB}; -use crate::query; +use crate::db::connmethods::BackendRows; +use crate::migrations::adb::ARef; +use crate::migrations::adb::{AColumn, ATable, Operation, TypeIdentifier, ADB}; use crate::query::{BoolExpr, Order}; -use crate::{Error, Result, SqlType, SqlVal, SqlValRef}; +use crate::{debug, query, Error, Result, SqlType, SqlVal, SqlValRef}; #[cfg(feature = "datetime")] const SQLITE_DT_FORMAT: &str = "%Y-%m-%d %H:%M:%S"; @@ -49,7 +49,7 @@ fn log_callback(error_code: std::ffi::c_int, message: &str) { } /// SQLite [`Backend`] implementation. -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct SQLiteBackend; impl SQLiteBackend { pub fn new() -> SQLiteBackend { @@ -63,6 +63,8 @@ impl SQLiteBackend { Ok(connection) } } + +#[async_trait] impl Backend for SQLiteBackend { fn name(&self) -> &'static str { BACKEND_NAME @@ -87,6 +89,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. @@ -115,7 +120,68 @@ impl SQLiteConnection { 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, 'c>( + &'c self, + table: &str, + columns: &[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( + &self, + table: &str, + pkcol: Column, + pk: SqlValRef<'_>, + 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> { @@ -143,15 +209,15 @@ impl ConnectionMethods for rusqlite::Connection { Ok(()) } - fn query<'a, 'b, 'c: 'a>( + fn query<'c>( &'c self, table: &str, - columns: &'b [Column], + columns: &[Column], expr: Option, limit: Option, offset: Option, order: Option<&[Order]>, - ) -> Result> { + ) -> Result> { let mut sqlquery = String::new(); helper::sql_select(columns, table, &mut sqlquery); let mut values: Vec = Vec::new(); @@ -327,7 +393,68 @@ impl<'c> SqliteTransaction<'c> { Error::Internal("transaction has already been consumed".to_string()) } } -connection_method_wrapper!(SqliteTransaction<'_>); +impl ConnectionMethods for SqliteTransaction<'_> { + fn execute(&self, sql: &str) -> Result<()> { + ConnectionMethods::execute(self.wrapped_connection_methods()?, sql) + } + fn query<'c>( + &'c self, + table: &str, + columns: &[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( + &self, + table: &str, + pkcol: Column, + pk: SqlValRef<'_>, + 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<'c> BackendTransaction<'c> for SqliteTransaction<'c> { fn commit(&mut self) -> Result<()> { match self.trans.take() { diff --git a/butane_core/src/db/sync_adapter.rs b/butane_core/src/db/sync_adapter.rs new file mode 100644 index 00000000..5f6acfdc --- /dev/null +++ b/butane_core/src/db/sync_adapter.rs @@ -0,0 +1,217 @@ +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, + Transaction, TransactionAsync, +}; +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, + _runtime: Option>, + inner: T, +} + +impl SyncAdapter { + 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) => { + 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(Arc::new(runtime)), + inner, + }) + } + } + } + + /// 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(), + _runtime: self._runtime.as_ref().cloned(), + 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::ConnectionMethods for SyncAdapter +where + T: crate::db::ConnectionMethodsAsync, +{ + 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 BackendConnection for SyncAdapter +where + T: BackendConnectionAsync, +{ + 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().cloned(); + let transaction: TransactionAsync = + self.runtime_handle.block_on(self.inner.transaction())?; + let transaction_adapter = SyncAdapter { + runtime_handle, + _runtime: runtime, + inner: transaction.trans, + }; + Ok(Transaction::new(Box::new(transaction_adapter))) + } + fn backend(&self) -> Box { + self.inner.backend() + } + fn backend_name(&self) -> &'static str { + self.inner.backend_name() + } + fn is_closed(&self) -> bool { + self.inner.is_closed() + } +} + +impl SyncAdapter +where + T: BackendConnectionAsync + 'static, +{ + pub fn into_connection(self) -> Connection { + Connection::new(Box::new(self)) + } +} + +impl<'c, T> BackendTransaction<'c> for SyncAdapter +where + T: BackendTransactionAsync<'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 ConnectionMethods { + self + } +} + +#[async_trait] +impl Backend for SyncAdapter +where + T: 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_async(conn_str))?; + let conn = 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/fkey.rs b/butane_core/src/fkey.rs index f8411b93..e5e638e0 100644 --- a/butane_core/src/fkey.rs +++ b/butane_core/src/fkey.rs @@ -1,21 +1,25 @@ //! Implementation of foreign key relationships between models. #![deny(missing_docs)] use std::borrow::Cow; +use std::fmt::Debug; +use std::sync::OnceLock; #[cfg(feature = "fake")] use fake::{Dummy, Faker}; -use once_cell::unsync::OnceCell; use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use crate::db::ConnectionMethods; +use crate::util::{get_or_init_once_lock, get_or_init_once_lock_async}; 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 [`ForeignKeyOpsSync`] and [`ForeignKeyOpsAsync`] for operations requiring a live database connection. +/// /// # Examples /// ```ignore /// #[model] @@ -34,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 @@ -63,21 +68,10 @@ 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(), - valpk: OnceCell::new(), + val: OnceLock::new(), + valpk: OnceLock::new(), } } @@ -93,6 +87,52 @@ impl ForeignKey { } } +/// [`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"),), + sync(), + async() +)] +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<&'a T> + where + T: 'a; +} + +impl ForeignKeyOpsAsync for ForeignKey { + async fn load<'a>(&'a self, conn: &impl ConnectionMethodsAsync) -> Result<&'a T> + where + T: 'a, + { + use crate::DataObjectOpsAsync; + 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()) + } +} + +impl ForeignKeyOpsSync for ForeignKey { + fn load<'a>(&'a self, conn: &impl ConnectionMethods) -> Result<&'a T> + where + T: 'a, + { + use crate::DataObjectOpsSync; + 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) + }) + .map(|v| v.as_ref()) + } +} + impl From for ForeignKey { fn from(obj: T) -> Self { let ret = Self::new_raw(); @@ -146,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 7f073a0f..c4bbeab8 100644 --- a/butane_core/src/lib.rs +++ b/butane_core/src/lib.rs @@ -1,9 +1,9 @@ //! Library providing functionality used by butane macros and tools. - #![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}; use serde::{Deserialize, Serialize}; @@ -22,9 +22,11 @@ pub mod sqlval; pub mod uuid; mod autopk; +mod util; + pub use autopk::AutoPk; use custom::SqlTypeCustom; -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}; @@ -62,6 +64,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 + #[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]; @@ -71,7 +74,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. - fn save_many_to_many(&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 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. @@ -83,7 +93,8 @@ pub mod internal { /// /// Rather than implementing this type manually, use the /// `#[model]` attribute. -pub trait DataObject: DataResult + internal::DataObjectInternal { +#[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; /// Link to a generated struct providing query helpers for each field. @@ -98,39 +109,62 @@ pub trait DataObject: DataResult + internal::DataObjectInternal { /// 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 = "ConnectionMethods"), + save_many_to_many(snake), + QueryOps, + ), + sync(), + async() +)] +pub trait DataObjectOps { /// Find this object in the database based on primary key. /// Returns `Error::NoSuchObject` if the primary key does not exist. - fn get(conn: &impl ConnectionMethods, id: impl ToSql) -> Result + async fn get(conn: &impl ConnectionMethods, id: impl ToSql) -> Result where - Self: Sized, + Self: DataObject + 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. - fn try_get(conn: &impl ConnectionMethods, id: impl ToSql) -> Result> + async fn try_get(conn: &impl ConnectionMethods, id: impl ToSql) -> Result> where - Self: Sized, + Self: DataObject + Sized, { + use crate::query::QueryOps; Ok(::query() .filter(query::BoolExpr::Eq( - Self::PKCOL, - query::Expr::Val(id.to_sql()), + T::PKCOL, + query::Expr::Val(id.borrow().to_sql()), )) .limit(1) - .load(conn)? + .load(conn) + .await? .into_iter() .nth(0)) } + /// Save the object to the database. - 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 { // Our only field is an AutoPk if !self.pk().is_valid() { - let pk = conn.insert_returning_pk(Self::TABLE, &[], &pkcol, &[])?; + let pk = conn + .insert_returning_pk(Self::TABLE, &[], &pkcol, &[]) + .await?; self.pk_mut().initialize(pk)?; } } else if Self::AUTO_PK { @@ -149,15 +183,18 @@ pub trait DataObject: DataResult + internal::DataObjectInternal { self.pk().to_sql_ref(), Self::NON_AUTO_COLUMNS, &self.non_auto_values(false), - )?; + ) + .await?; } else { // invalid pk, do an insert - let pk = conn.insert_returning_pk( - Self::TABLE, - Self::NON_AUTO_COLUMNS, - &pkcol, - &self.non_auto_values(true), - )?; + let pk = conn + .insert_returning_pk( + Self::TABLE, + Self::NON_AUTO_COLUMNS, + &pkcol, + &self.non_auto_values(true), + ) + .await?; self.pk_mut().initialize(pk)?; }; } else { @@ -167,20 +204,27 @@ pub trait DataObject: DataResult + internal::DataObjectInternal { Self::COLUMNS, &pkcol, &self.non_auto_values(true), - )?; + ) + .await?; } - self.save_many_to_many(conn)?; + Self::save_many_to_many(self, conn).await?; Ok(()) } /// Delete the object from the database. - 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<()> + where + Self: DataObject, + { + conn.delete(T::TABLE, T::PKCOL, self.pk().to_sql()).await } } +impl DataObjectOpsSync for T where T: DataObject {} +impl DataObjectOpsAsync for T where T: DataObject {} + /// Butane errors. #[allow(missing_docs)] #[derive(Debug, ThisError)] @@ -229,6 +273,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}")] @@ -241,7 +287,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), @@ -252,6 +298,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")] @@ -270,6 +322,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`]. diff --git a/butane_core/src/many.rs b/butane_core/src/many.rs index e0e09c4c..8f06eabf 100644 --- a/butane_core/src/many.rs +++ b/butane_core/src/many.rs @@ -1,18 +1,20 @@ //! Implementation of many-to-many relationships between models. #![deny(missing_docs)] use std::borrow::Cow; +use std::sync::OnceLock; #[cfg(feature = "fake")] use fake::{Dummy, Faker}; -use once_cell::unsync::OnceCell; use serde::{Deserialize, Serialize}; -use crate::db::{Column, ConnectionMethods}; +use crate::db::{Column, ConnectionMethods, ConnectionMethodsAsync}; use crate::query::{BoolExpr, Expr, OrderDirection, Query}; -use crate::{DataObject, Error, FieldType, PrimaryKeyType, Result, SqlType, SqlVal, ToSql}; +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() -> OnceCell> { - OnceCell::default() +fn default_oc() -> OnceLock> { + // Same as impl Default for once_cell::unsync::OnceCell + OnceLock::new() } /// Used to implement a many-to-many relationship between models. @@ -21,6 +23,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 [`ManyOpsSync`] and [`ManyOpsAsync`] for operations requiring a live database connection. // #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Many @@ -36,7 +40,7 @@ where removed_values: Vec, #[serde(skip)] #[serde(default = "default_oc")] - all_values: OnceCell>, + all_values: OnceLock>, } impl Many where @@ -55,7 +59,7 @@ where owner_type: SqlType::Int, new_values: Vec::new(), removed_values: Vec::new(), - all_values: OnceCell::new(), + all_values: OnceLock::new(), } } @@ -67,7 +71,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 @@ -80,7 +84,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(()) } @@ -88,7 +92,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()) } @@ -100,8 +104,126 @@ where .map(|v| v.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()))), + })) + } + + /// 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, async = "ConnectionMethodsAsync"), QueryOps), + 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>( + many: &'a Many, + conn: &impl ConnectionMethods, + query: Query, +) -> Result> +where + T: DataObject + 'a, +{ + 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() { + 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. +#[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 ConnectionMethods, + query: Query, +) -> Result> +where + T: DataObject + 'a, +{ + get_or_init_once_lock(&many.all_values, || load_query_uncached(many, conn, query)) + .await + .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 = "ConnectionMethods"),), + sync(), + async() +)] +pub trait ManyOps { /// Used by macro-generated code. You do not need to call this directly. - pub fn save(&mut self, conn: &impl 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 = "ConnectionMethods"), + ManyOpsInternal, + ManyOps, + load_query(sync = "load_query_sync", async = "load_query_async"), + ), + keep_self, + sync(), + async() +)] +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() { conn.insert_only( @@ -111,109 +233,74 @@ 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(()) } - /// Delete all references from the database, and any unsaved additions. - pub 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, BoolExpr::Eq("owner", Expr::Val(owner.clone())), - )?; + ) + .await?; 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(()) } - /// Loads the values referred to by this many relationship from the - /// database if necessary and returns a reference to them. - pub 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())?.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. - fn load_query( - &self, - conn: &impl ConnectionMethods, - query: Query, - ) -> Result> { - let vals: Result<&Vec> = self.all_values.get_or_try_init(|| { - let mut vals = query.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)?, - ); - } - Ok(vals) - }); - 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 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))? - .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/migrations/fsmigrations.rs b/butane_core/src/migrations/fsmigrations.rs index 0990bf9d..d8e00d6e 100644 --- a/butane_core/src/migrations/fsmigrations.rs +++ b/butane_core/src/migrations/fsmigrations.rs @@ -3,7 +3,6 @@ use std::collections::BTreeMap; use std::fs::{File, OpenOptions}; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; -use std::rc::Rc; use fs2::FileExt; use serde::{Deserialize, Serialize}; @@ -55,7 +54,7 @@ impl MigrationsState { /// A migration stored in the filesystem. #[derive(Clone, Debug)] pub struct FsMigration { - fs: Rc, + fs: std::sync::Arc, root: PathBuf, } @@ -245,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(), }; @@ -255,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(), )?; @@ -346,14 +343,14 @@ impl Eq for FsMigration {} /// A collection of migrations stored in the filesystem. #[derive(Clone, Debug)] pub struct FsMigrations { - fs: Rc, + fs: std::sync::Arc, root: PathBuf, current: FsMigration, } impl FsMigrations { /// Create a new instance. 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"), diff --git a/butane_core/src/migrations/migration.rs b/butane_core/src/migrations/migration.rs index db2dc903..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::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::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::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::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 4cf024b8..3efc4d8e 100644 --- a/butane_core/src/migrations/mod.rs +++ b/butane_core/src/migrations/mod.rs @@ -5,11 +5,14 @@ use std::path::Path; +use async_trait::async_trait; use fallible_iterator::FallibleIterator; use nonempty::NonEmpty; -use crate::db::{BackendConnection, BackendRows}; -use crate::db::{Column, ConnectionMethods}; +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}; @@ -27,7 +30,8 @@ mod memmigrations; pub use memmigrations::{MemMigration, MemMigrations}; /// A collection of migrations. -pub trait Migrations { +#[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; /// Gets the migration with the given name, if it exists @@ -120,6 +124,19 @@ pub trait Migrations { Ok(()) } + /// Migrate connection forward. + async fn migrate_async(&self, conn: &mut ConnectionAsync) -> Result<()> + where + Self: Send + 'static, + { + let m2 = self.clone(); + conn.with_sync(move |conn| { + m2.migrate(conn)?; + Ok(()) + }) + .await + } + /// Remove all applied migrations. fn unmigrate(&self, connection: &mut impl BackendConnection) -> Result<()> { let mut migration = match self.last_applied_migration(connection)? { @@ -137,6 +154,19 @@ pub trait Migrations { } Ok(()) } + + /// Remove all applied migrations. + async fn unmigrate_async(&self, conn: &mut ConnectionAsync) -> Result<()> + where + Self: Send + 'static, + { + let m2 = self.clone(); + conn.with_sync(move |conn| { + m2.unmigrate(conn)?; + Ok(()) + }) + .await + } } /// Extension of [`Migrations`] to modify the series of migrations. @@ -187,7 +217,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 { @@ -200,7 +230,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, @@ -307,6 +337,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)]; @@ -320,10 +352,12 @@ impl DataResult for ButaneMigration { name: FromSql::from_sql_ref(row.get(0, SqlType::Text).unwrap())?, }) } + 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 @@ -333,9 +367,6 @@ impl DataObject for ButaneMigration { fn pk(&self) -> &String { &self.name } - fn delete(&self, conn: &impl ConnectionMethods) -> Result<()> { - conn.delete(Self::TABLE, Self::PKCOL, self.pk().to_sql()) - } } impl crate::internal::DataObjectInternal for ButaneMigration { @@ -350,7 +381,10 @@ impl crate::internal::DataObjectInternal for ButaneMigration { } values } - fn save_many_to_many(&mut self, _conn: &impl 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 ConnectionMethods) -> Result<()> { Ok(()) // no-op } } diff --git a/butane_core/src/query/mod.rs b/butane_core/src/query/mod.rs index dfb3e7f4..10f7d670 100644 --- a/butane_core/src/query/mod.rs +++ b/butane_core/src/query/mod.rs @@ -9,7 +9,7 @@ use std::marker::PhantomData; use fallible_iterator::FallibleIterator; -use crate::db::{BackendRows, ConnectionMethods, QueryResult}; +use crate::db::{BackendRows, ConnectionMethods, ConnectionMethodsAsync, QueryResult}; use crate::{DataResult, Result, SqlVal}; mod fieldexpr; @@ -117,7 +117,8 @@ impl Column { } /// Representation of a database query. -#[derive(Clone, Debug)] +/// See [`QueryOpsSync`] and [`QueryOpsAsync`] for operations requiring a live database connection. +#[derive(Debug)] pub struct Query { table: TblName, filter: Option, @@ -181,8 +182,40 @@ impl Query { pub fn order_desc(self, column: &'static str) -> Query { self.order(column, OrderDirection::Descending) } +} - fn fetch( +// 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, + offset: self.offset, + sort: self.sort.clone(), + phantom: PhantomData, + } + } +} + +/// 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, @@ -200,21 +233,54 @@ impl Query { self.offset, sort, ) + .await } +} +/// [`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"), QueryOpsInternal), + sync(), + async() +)] +pub trait QueryOps { /// 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) - } + async fn load_first(self, conn: &impl ConnectionMethods) -> Result>; /// Executes the query against `conn`. - pub fn load(self, conn: &impl ConnectionMethods) -> Result> { - let limit = self.limit.to_owned(); - self.fetch(conn, limit)?.mapped(T::from_row).collect() - } + async fn load(self, conn: &impl ConnectionMethods) -> Result>; /// Executes the query against `conn` and deletes all matching objects. - pub fn delete(self, conn: &impl ConnectionMethods) -> Result { + async fn delete(self, conn: &impl ConnectionMethods) -> Result; +} + +#[maybe_async_cfg::maybe( + idents( + ConnectionMethods(sync = "ConnectionMethods"), + QueryOps, + QueryOpsInternal + ), + keep_self, + sync(), + async() +)] +impl QueryOps for Query { + async fn load_first(self, conn: &impl ConnectionMethods) -> Result> { + 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(); + QueryOpsInternal::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/butane_core/src/sqlval.rs b/butane_core/src/sqlval.rs index 36dfece7..a6ff07aa 100644 --- a/butane_core/src/sqlval.rs +++ b/butane_core/src/sqlval.rs @@ -289,7 +289,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 { /// Test if this object's pk is valid. The only case in which this /// returns false is if the pk is an AutoPk and it's not yet valid. /// diff --git a/butane_core/src/util.rs b/butane_core/src/util.rs new file mode 100644 index 00000000..cf7b891f --- /dev/null +++ b/butane_core/src/util.rs @@ -0,0 +1,36 @@ +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); + } + 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"), + } +} diff --git a/butane_core/tests/connection.rs b/butane_core/tests/connection.rs index ac527043..f6c78657 100644 --- a/butane_core/tests/connection.rs +++ b/butane_core/tests/connection.rs @@ -1,10 +1,11 @@ -use butane_core::db::{connect, BackendConnection, Connection, ConnectionSpec}; +use butane_core::db::{connect_async, ConnectionAsync, ConnectionSpec}; use butane_test_helper::*; +use butane_test_macros::butane_test; -fn connection_not_closed(conn: Connection) { +#[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() { @@ -21,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_async(&spec).await; assert!(matches!(result, Err(butane_core::Error::Postgres(_)))); match result { Err(butane_core::Error::Postgres(e)) => { @@ -38,8 +39,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!( @@ -47,7 +48,7 @@ fn unreachable_pg_connection() { "host=does_not_exist user=does_not_exist".to_string() ); - let result = connect(&spec); + let result = connect_async(&spec).await; assert!(matches!(result, Err(butane_core::Error::Postgres(_)))); match result { Err(butane_core::Error::Postgres(e)) => { @@ -61,16 +62,17 @@ fn unreachable_pg_connection() { } } -fn debug_connection(conn: Connection) { +#[butane_test(nomigrate)] +async fn debug_connection(conn: ConnectionAsync) { let backend_name = conn.backend_name(); + 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); #[test] fn wont_load_connection_spec_from_missing_path() { diff --git a/butane_core/tests/migration.rs b/butane_core/tests/migration.rs index 7402cfd5..ffa886d1 100644 --- a/butane_core/tests/migration.rs +++ b/butane_core/tests/migration.rs @@ -1,7 +1,8 @@ -use butane_core::db::{BackendConnection, Connection, ConnectionMethods}; +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,7 +212,8 @@ 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. -fn add_table_fkey_delete_column(conn: Connection) { +#[butane_test(nomigrate)] +async fn add_table_fkey_delete_column(conn: ConnectionAsync) { let known_int_type = DeferredSqlType::KnownId(TypeIdentifier::Ty(SqlType::Int)); let old = ADB::default(); @@ -280,9 +282,9 @@ fn add_table_fkey_delete_column(conn: Connection) { let backend = conn.backend(); let sql = backend.create_migration_sql(&new, ops).unwrap(); - conn.execute(&sql).unwrap(); - conn.execute("SELECT * from a").unwrap(); - conn.execute("SELECT * from b").unwrap(); + conn.execute(&sql).await.unwrap(); + conn.execute("SELECT * from a").await.unwrap(); + conn.execute("SELECT * from b").await.unwrap(); // "ALTER TABLE b DROP COLUMN fkey;" fails due to sqlite not being // able to remove the attached constraint, however the RemoveColumn @@ -291,14 +293,14 @@ fn add_table_fkey_delete_column(conn: Connection) { let sql = backend .create_migration_sql(&new, vec![remove_column_op]) .unwrap(); - conn.execute(&sql).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. -fn add_table_fkey_back_reference(conn: Connection) { +#[butane_test(nomigrate)] +async fn add_table_fkey_back_reference(conn: ConnectionAsync) { let known_int_type = DeferredSqlType::KnownId(TypeIdentifier::Ty(SqlType::Int)); let old = ADB::default(); @@ -380,15 +382,15 @@ fn add_table_fkey_back_reference(conn: Connection) { ); } - conn.execute(&sql).unwrap(); - conn.execute("SELECT * from a").unwrap(); - conn.execute("SELECT * from b").unwrap(); + conn.execute(&sql).await.unwrap(); + 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. -fn add_table_fkey_multiple(conn: Connection) { +#[butane_test(nomigrate)] +async fn add_table_fkey_multiple(conn: ConnectionAsync) { let known_int_type = DeferredSqlType::KnownId(TypeIdentifier::Ty(SqlType::Int)); let old = ADB::default(); @@ -491,11 +493,10 @@ fn add_table_fkey_multiple(conn: Connection) { let backend = conn.backend(); let sql = backend.create_migration_sql(&new, ops).unwrap(); - conn.execute(&sql).unwrap(); - conn.execute("SELECT * from a").unwrap(); - conn.execute("SELECT * from b").unwrap(); + conn.execute(&sql).await.unwrap(); + 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 8f642176..6d72731e 100644 --- a/butane_core/tests/transactions.rs +++ b/butane_core/tests/transactions.rs @@ -1,30 +1,32 @@ -use butane_core::db::{BackendConnection, Connection}; +use butane_core::db::ConnectionAsync; use butane_test_helper::*; +use butane_test_macros::butane_test; -fn commit_empty_transaction(mut conn: Connection) { +#[butane_test(nomigrate)] +async fn commit_empty_transaction(mut conn: ConnectionAsync) { 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(); +#[butane_test(nomigrate)] +async fn rollback_empty_transaction(mut conn: ConnectionAsync) { + 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) { +#[butane_test(nomigrate)] +async fn debug_transaction_before_consuming(mut conn: ConnectionAsync) { let backend_name = conn.backend_name(); - let tr = conn.transaction().unwrap(); + let tr = conn.transaction().await.unwrap(); if backend_name == "pg" { assert!(format!("{:?}", tr).contains("{ trans: true }")); @@ -32,6 +34,5 @@ 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 bfb42907..3a4a3b32 100644 --- a/butane_test_helper/Cargo.toml +++ b/butane_test_helper/Cargo.toml @@ -14,11 +14,13 @@ 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 nonempty.workspace = true once_cell = { workspace = true } -postgres = { features = ["with-geo-types-0_7"], workspace = true } +tokio-postgres = { features = ["with-geo-types-0_7"], workspace = true } rand.workspace = true tempfile.workspace = true uuid = { features = ["v4"], workspace = true } diff --git a/butane_test_helper/src/lib.rs b/butane_test_helper/src/lib.rs index bec19266..c9552a53 100644 --- a/butane_test_helper/src/lib.rs +++ b/butane_test_helper/src/lib.rs @@ -2,6 +2,7 @@ //! Macros depend on [`butane_core`], `env_logger` and [`log`]. #![deny(missing_docs)] +use std::future::Future; use std::io::{BufRead, BufReader, Read, Write}; use std::ops::Deref; use std::path::PathBuf; @@ -10,22 +11,121 @@ use std::sync::Mutex; use block_id::{Alphabet, BlockId}; use butane_core::db::{ - connect, get_backend, pg, sqlite, Backend, BackendConnection, Connection, 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 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(); - let data = pg_setup(); + let data = pg_setup_sync(); (backend.connect(&pg_connstr(&data)).unwrap(), data) } /// Create a postgres [`ConnectionSpec`]. -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, @@ -63,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 { @@ -146,7 +251,34 @@ 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() -> PgSetupData { +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 conn = connect(&ConnectionSpec::new("pg", &connstr)).unwrap(); + log::debug!("closed is {}", BackendConnection::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"); // By default we set up a temporary, local postgres server just // for this test. This can be overridden by the environment @@ -163,8 +295,15 @@ pub 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)).unwrap(); + let conn = connect_async(&ConnectionSpec::new("pg", &connstr)) + .await + .unwrap(); + log::debug!( + "[async]closed is {}", + BackendConnectionAsync::is_closed(&conn) + ); conn.execute(format!("CREATE DATABASE {new_dbname};")) + .await .unwrap(); let connstr = format!("{connstr} dbname={new_dbname}"); @@ -182,9 +321,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); @@ -212,9 +349,16 @@ pub fn create_current_migrations(connection: &Connection) -> MemMigrations { mem_migrations } +/// Populate the database schema. +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 Connection) { - let mem_migrations = create_current_migrations(conn); + let mem_migrations = create_current_migrations(conn.backend()); log::info!("created current migration"); mem_migrations.migrate(conn).unwrap(); } @@ -230,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 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! { - #[test] - pub 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>](); - log::info!("connecting to {}..", &$connstr); - let mut conn = backend.connect(&$connstr).expect("Could not connect backend"); - if $migrate { - butane_test_helper::setup_db(&mut conn); - } - log::info!("running test on {}..", &$connstr); - $fname(conn); - butane_test_helper::[<$backend _teardown>]($dataname); + #[tokio::test] + pub async fn [<$fname _ $backend>]() { + 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") + }; } } }; @@ -262,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..04adb367 --- /dev/null +++ b/butane_test_macros/Cargo.toml @@ -0,0 +1,17 @@ +[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..8b622d48 --- /dev/null +++ b/butane_test_macros/src/lib.rs @@ -0,0 +1,239 @@ +//! 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::ForeignKeyOpsSync; + use butane_core::many::ManyOpsSync; + use butane_core::query::QueryOpsSync; + use butane_core::DataObjectOpsSync; + )) + .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::ForeignKeyOpsAsync; + use butane_core::many::ManyOpsAsync; + use butane_core::query::QueryOpsAsync; + use butane_core::DataObjectOpsAsync; + )) + .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/example/Cargo.toml b/example/Cargo.toml index c8721498..8eb4ed98 100644 --- a/example/Cargo.toml +++ b/example/Cargo.toml @@ -11,7 +11,8 @@ build = "build.rs" sqlite-bundled = ["butane/sqlite-bundled"] [dependencies] -butane = { features = ["sqlite"], workspace = true } +butane = { features = ["pg", "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 947d6754..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::{Connection, ConnectionSpec}; -use butane::prelude::*; -use butane::{find, model, query, AutoPk, Error, ForeignKey, Many}; +use butane::db::{ConnectionAsync, ConnectionSpec}; +use butane::prelude_async::*; +use butane::{find_async, model, query, AutoPk, Error, ForeignKey, Many}; type Result = std::result::Result; @@ -46,52 +46,59 @@ 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(), }; - 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 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)?; + 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_async(&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 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/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 02a8d493..596f824d 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 cc879a16..c3aff322 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::*; 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 8a087de3..3e6bd5f7 100644 --- a/examples/getting_started/tests/unmigrate.rs +++ b/examples/getting_started/tests/unmigrate.rs @@ -1,7 +1,8 @@ use butane::db::{BackendConnection, Connection}; use butane::migrations::Migrations; -use butane::DataObject; +use butane::DataObjectOpsSync; use butane_test_helper::*; +use butane_test_macros::butane_test; use getting_started::models::{Blog, Post, Tag}; @@ -34,6 +35,7 @@ fn insert_data(connection: &Connection) { post.save(connection).unwrap(); } +#[butane_test(sync, nomigrate)] fn migrate_and_unmigrate(mut connection: Connection) { // Migrate forward. let base_dir = std::path::PathBuf::from(".butane"); @@ -46,4 +48,3 @@ fn migrate_and_unmigrate(mut connection: Connection) { // Undo migrations. migrations.unmigrate(&mut connection).unwrap(); } -testall_no_migrate!(migrate_and_unmigrate); 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..536c949e --- /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..1eb956aa --- /dev/null +++ b/examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/pg_down.sql @@ -0,0 +1,3 @@ +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 new file mode 100644 index 00000000..773142e0 --- /dev/null +++ b/examples/getting_started_async/.butane/migrations/20240115_023841384_dbconstraints/pg_up.sql @@ -0,0 +1,3 @@ +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/.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..de59c2a8 --- /dev/null +++ b/examples/getting_started_async/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "getting_started_async" +version = "0.1.0" +authors = ["James Oakley "] +license.workspace = true +edition.workspace = true +publish = false + +[[bin]] +name = "show_posts_async" +path = "src/bin/show_posts.rs" +doc = false + +[[bin]] +name = "write_post_async" +path = "src/bin/write_post.rs" +doc = false + +[[bin]] +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] +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 +butane_test_macros.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..db3d3284 --- /dev/null +++ b/examples/getting_started_async/src/bin/delete_post.rs @@ -0,0 +1,19 @@ +use std::env::args; + +use butane::prelude_async::*; +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..f69f1ca2 --- /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_async::*; +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..3f86f09f --- /dev/null +++ b/examples/getting_started_async/src/bin/show_posts.rs @@ -0,0 +1,20 @@ +use butane::prelude_async::*; +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..b6cd1073 --- /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": "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": "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" + } + } + }, + "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..4b004c50 --- /dev/null +++ b/examples/getting_started_async/src/lib.rs @@ -0,0 +1,41 @@ +//! Common helpers for the getting_started example CLI. + +#![deny(missing_docs)] + +pub mod butane_migrations; +pub mod models; + +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() -> ConnectionAsync { + let mut connection = + butane::db::connect_async(&ConnectionSpec::load(".butane/connection.json").unwrap()) + .await + .unwrap(); + let migrations = butane_migrations::get_migrations().unwrap(); + migrations.migrate_async(&mut connection).await.unwrap(); + connection +} + +/// Create a [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: &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: &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 new file mode 100644 index 00000000..29611951 --- /dev/null +++ b/examples/getting_started_async/src/models.rs @@ -0,0 +1,75 @@ +//! Models for the getting_started example. + +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/unmigrate.rs b/examples/getting_started_async/tests/unmigrate.rs new file mode 100644 index 00000000..6dab144c --- /dev/null +++ b/examples/getting_started_async/tests/unmigrate.rs @@ -0,0 +1,50 @@ +use butane::db::{BackendConnectionAsync, ConnectionAsync}; +use butane::migrations::Migrations; +use butane::DataObjectOpsAsync; +use butane_test_helper::*; +use butane_test_macros::butane_test; + +use getting_started_async::models::{Blog, Post, 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: &ConnectionAsync) { + 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(); +} + +#[butane_test(async, nomigrate)] +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_async(&mut connection).await.unwrap(); + + insert_data(&connection).await; + + // Undo migrations. + migrations.unmigrate_async(&mut connection).await.unwrap(); +} diff --git a/examples/newtype/Cargo.toml b/examples/newtype/Cargo.toml index 7801e764..865ea478 100644 --- a/examples/newtype/Cargo.toml +++ b/examples/newtype/Cargo.toml @@ -26,10 +26,12 @@ 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 paste.workspace = true +tokio = { workspace = true, features = ["macros"] } [package.metadata.release] release = false diff --git a/examples/newtype/src/lib.rs b/examples/newtype/src/lib.rs index fdbf5c4f..6536f811 100644 --- a/examples/newtype/src/lib.rs +++ b/examples/newtype/src/lib.rs @@ -5,35 +5,37 @@ pub mod butane_migrations; pub mod models; -use butane::db::{Connection, ConnectionSpec}; +use butane::db::{ConnectionAsync, ConnectionSpec}; use butane::migrations::Migrations; -use butane::prelude::*; +use butane::prelude_async::*; use models::{Blog, Post}; /// Load a [Connection]. -pub fn establish_connection() -> Connection { +pub async fn establish_connection() -> ConnectionAsync { let mut connection = - butane::db::connect(&ConnectionSpec::load(".butane/connection.json").unwrap()).unwrap(); + butane::db::connect_async(&ConnectionSpec::load(".butane/connection.json").unwrap()) + .await + .unwrap(); let migrations = butane_migrations::get_migrations().unwrap(); - migrations.migrate(&mut connection).unwrap(); + migrations.migrate_async(&mut connection).await.unwrap(); connection } /// Create a [Blog]. -pub 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).unwrap(); + blog.save(conn).await.unwrap(); blog } /// Create a [Post]. -pub 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).unwrap(); + new_post.save(conn).await.unwrap(); new_post } /// Fetch the first existing [Blog] if one exists. -pub fn existing_blog(conn: &Connection) -> Option { - Blog::query().load_first(conn).unwrap() +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 6a9da5c7..bc8ab827 100644 --- a/examples/newtype/tests/unmigrate.rs +++ b/examples/newtype/tests/unmigrate.rs @@ -1,17 +1,26 @@ -use butane::db::{BackendConnection, Connection}; +use butane::db::{BackendConnectionAsync, Connection, ConnectionAsync}; use butane::migrations::Migrations; -use butane::DataObject; use butane_test_helper::*; +use butane_test_macros::butane_test; use newtype::models::{Blog, Post, Tags}; -fn insert_data(connection: &Connection) { +#[maybe_async_cfg::maybe( + sync(), + async(), + idents( + Connection(sync = "Connection", async = "ConnectionAsync"), + DataObjectOps(sync = "DataObjectOpsSync", async = "DataObjectOpsAsync") + ) +)] +async fn insert_data(connection: &Connection) { + use butane::DataObjectOps; if connection.backend_name() == "sqlite" { // https://github.com/Electron100/butane/issues/226 return; } let mut cats_blog = Blog::new("Cats").unwrap(); - cats_blog.save(connection).unwrap(); + cats_blog.save(connection).await.unwrap(); let mut post = Post::new( &cats_blog, @@ -24,19 +33,33 @@ fn insert_data(connection: &Connection) { "asia".to_string(), "danger".to_string(), ])); - post.save(connection).unwrap(); + post.save(connection).await.unwrap(); } -fn migrate_and_unmigrate(mut connection: Connection) { +#[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_async(&connection).await; + + // Undo migrations. + migrations.unmigrate_async(&mut connection).await.unwrap(); +} + +#[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(&connection); + insert_data_sync(&connection); // Undo migrations. migrations.unmigrate(&mut connection).unwrap(); } -testall_no_migrate!(migrate_and_unmigrate);