Repository: Brendonovich/prisma-client-rust (language: rust)

Issue:
Interactive Transactions
support transaction


Discussion / hints:
I do plan on supporting transactions, ~~but they're not a priority right now~~ ~~they're gonna be fairly important for Spacedrive so I'll get them out in 0.6.0~~
Batching (#31) will be important for Spacedrive, so they will be implemented. Interactive transactions are still a preview feature so I'm not going to do them yet.

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index c525ab1e..eee0198c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2170,9 +2170,11 @@ dependencies = [
 name = "integration-tests"
 version = "0.1.0"
 dependencies = [
+ "async-trait",
  "prisma-client-rust",
  "rspc",
  "serde",
+ "thiserror",
  "tokio",
 ]
 
@@ -3515,9 +3517,11 @@ dependencies = [
  "diagnostics",
  "dml",
  "dmmf",
+ "futures",
  "include_dir",
  "indexmap",
  "migration-core",
+ "paste",
  "prisma-models",
  "psl",
  "query-connector",
diff --git a/Cargo.toml b/Cargo.toml
index 30ed79fe..6a660efd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -30,10 +30,7 @@ mocking = ["tokio"]
 mysql = ["query-core/mysql", "migration-core/mysql"]
 sqlite = ["query-core/sqlite", "migration-core/sqlite"]
 mssql = ["query-core/mssql", "migration-core/mssql"]
-postgresql = [
-  "query-core/postgresql",
-  "migration-core/postgresql",
-]
+postgresql = ["query-core/postgresql", "migration-core/postgresql"]
 mongodb = ["query-core/mongodb", "migration-core/mongodb"]
 
 [dependencies]
@@ -47,6 +44,8 @@ indexmap = "1.8.2"
 serde-value = "0.7.0"
 base64 = "0.13.0"
 tokio = { version = "1.21.0", optional = true }
+futures = "0.3"
+paste = "1.0.11"
 
 query-core = { git = "https://github.com/Brendonovich/prisma-engines", rev = "43fcfd1b5c0be59e414fb03cf7b7712e1661b6d0" }
 query-connector = { git = "https://github.com/Brendonovich/prisma-engines", rev = "43fcfd1b5c0be59e414fb03cf7b7712e1661b6d0" }
@@ -71,3 +70,4 @@ rspc = { version = "0.1.2", optional = true, features = [
   "uuid",
 ] }
 convert_case = "0.6.0"
+
diff --git a/cli/src/generator/client.rs b/cli/src/generator/client.rs
index 8bdf5d06..ad11c897 100644
--- a/cli/src/generator/client.rs
+++ b/cli/src/generator/client.rs
@@ -127,13 +127,31 @@ pub fn generate(args: &GenerateArgs) -> TokenStream {
                 )
             }
 
-            pub async fn _batch<'a, T: #pcr::BatchContainer<'a, Marker>, Marker>(&'a self, queries: T) -> #pcr::Result<<T as #pcr::BatchContainer<'a, Marker>>::ReturnType> {
+            pub async fn _batch<'batch, T: #pcr::BatchContainer<'batch, Marker>, Marker>(&self, queries: T) -> #pcr::Result<<T as #pcr::BatchContainer<'batch, Marker>>::ReturnType> {
                 #pcr::batch(queries, &self.0).await
             }
 
+            pub fn _transaction(&self) -> #pcr::TransactionBuilder<Self> {
+                #pcr::TransactionBuilder::_new(self, &self.0)
+            }
+
             #migrate_fns
 
             #(#model_actions)*
         }
+
+        impl #pcr::PrismaClient for PrismaClient {
+            fn internals(&self) -> &#pcr::PrismaClientInternals {
+                &self.0
+            }
+
+            fn internals_mut(&mut self) -> &mut #pcr::PrismaClientInternals {
+                &mut self.0
+            }
+
+            fn with_tx_id(&self, tx_id: Option<#pcr::query_core::TxId>) -> Self {
+                Self(self.0.with_tx_id(tx_id))
+            }
+        }
     }
 }
diff --git a/cli/src/generator/internal_enums.rs b/cli/src/generator/internal_enums.rs
index cddbe274..e67c92d5 100644
--- a/cli/src/generator/internal_enums.rs
+++ b/cli/src/generator/internal_enums.rs
@@ -1,6 +1,6 @@
-use prisma_client_rust_sdk::{Case, Casing, GenerateArgs};
+use prisma_client_rust_sdk::prelude::*;
 use proc_macro2::TokenStream;
-use quote::{format_ident, quote};
+use quote::quote;
 
 pub fn generate(args: &GenerateArgs) -> TokenStream {
     let internal_enums = args
@@ -10,13 +10,13 @@ pub fn generate(args: &GenerateArgs) -> TokenStream {
         .unwrap()
         .iter()
         .map(|e| {
-            let name = format_ident!("{}", e.name.to_case(Case::Pascal));
+            let name = pascal_ident(&e.name);
 
             let variants = e
                 .values
                 .iter()
                 .map(|v| {
-                    let variant_name = format_ident!("{}", v.to_case(Case::Pascal));
+                    let variant_name = pascal_ident(v);
 
                     quote! {
                         #[serde(rename=#v)]
@@ -29,12 +29,16 @@ pub fn generate(args: &GenerateArgs) -> TokenStream {
                 .values
                 .iter()
                 .map(|name| {
-                    let variant_name = format_ident!("{}", name.to_case(Case::Pascal));
+                    let variant_name = pascal_ident(name);
 
                     quote!(Self::#variant_name => #name.to_string())
                 })
                 .collect::<Vec<_>>();
 
+            let isolation_level_impl = (&e.name == "TransactionIsolationLevel").then(|| quote! {
+                impl ::prisma_client_rust::TransactionIsolationLevel for TransactionIsolationLevel {}
+            });
+
             quote! {
                 #[derive(Debug, Clone, Copy, ::serde::Serialize, ::serde::Deserialize, PartialEq, Eq)]
                 pub enum #name {
@@ -48,10 +52,13 @@ pub fn generate(args: &GenerateArgs) -> TokenStream {
                         }
                     }
                 }
+
+                #isolation_level_impl
             }
         });
 
     quote! {
-       #(#internal_enums)*
+        #(#internal_enums)*
+
     }
 }
diff --git a/cli/src/generator/models/actions.rs b/cli/src/generator/models/actions.rs
index 65b5120a..f6d387a2 100644
--- a/cli/src/generator/models/actions.rs
+++ b/cli/src/generator/models/actions.rs
@@ -3,19 +3,6 @@ use prisma_client_rust_sdk::GenerateArgs;
 
 use super::required_fields;
 
-pub fn scalar_selections_fn(model: &dml::Model) -> TokenStream {
-    let scalar_fields = model.scalar_fields().map(|f| &f.name);
-
-    quote! {
-        fn scalar_selections() -> Vec<::prisma_client_rust::Selection> {
-            [#(#scalar_fields),*]
-                .into_iter()
-                .map(::prisma_client_rust::sel)
-                .collect()
-        }
-    }
-}
-
 pub fn create_args_params_pushes(model: &dml::Model) -> Vec<TokenStream> {
     let required_fields = required_fields(model);
 
@@ -108,8 +95,6 @@ pub fn upsert_fn(model: &dml::Model) -> TokenStream {
 pub fn struct_definition(model: &dml::Model, args: &GenerateArgs) -> TokenStream {
     let pcr = quote!(::prisma_client_rust);
 
-    let model_name_str = &model.name;
-
     let create_fn = create_fn(model);
     let upsert_fn = upsert_fn(model);
 
@@ -119,27 +104,12 @@ pub fn struct_definition(model: &dml::Model, args: &GenerateArgs) -> TokenStream
         .contains(&datamodel_connector::ConnectorCapability::CreateMany))
     .then(|| create_many_fn(model));
 
-    let scalar_selections_fn = scalar_selections_fn(model);
-
     quote! {
         #[derive(Clone)]
         pub struct Actions<'a> {
             pub client: &'a #pcr::PrismaClientInternals,
         }
 
-        impl #pcr::ModelActions for Actions<'_> {
-            type Data = Data;
-            type Where = WhereParam;
-            type Set = SetParam;
-            type With = WithParam;
-            type OrderBy = OrderByParam;
-            type Cursor = UniqueWhereParam;
-
-            const MODEL: &'static str = #model_name_str;
-
-            #scalar_selections_fn
-        }
-
         impl<'a> Actions<'a> {
             pub fn find_unique(self, _where: UniqueWhereParam) -> FindUnique<'a> {
                 FindUnique::new(
diff --git a/cli/src/generator/models/include_select.rs b/cli/src/generator/models/include_select.rs
index 3fec369e..c89e5a6e 100644
--- a/cli/src/generator/models/include_select.rs
+++ b/cli/src/generator/models/include_select.rs
@@ -290,7 +290,7 @@ fn model_macro<'a>(
     let specta_impl = cfg!(feature = "rspc").then(|| {
         let base_field_types = base_fields.clone().filter_map(|f| {
             let field_name_str = f.name();
-            let field_type = f.type_tokens(quote!());
+            let field_type = f.type_tokens(quote!(crate::#module_path::));
 
             f.as_scalar_field().map(|_| 
                 quote!(#specta::ObjectField {
@@ -399,7 +399,7 @@ fn model_macro<'a>(
     let selection = {
         let scalar_selections = matches!(variant, Variant::Include).then(||
             quote! {
-                <$crate::#module_path::#model_name_snake::Actions as ::prisma_client_rust::ModelActions>::scalar_selections()
+                <$crate::#module_path::#model_name_snake::Types as ::prisma_client_rust::ModelActions>::scalar_selections()
             }
         );
 
@@ -542,7 +542,7 @@ fn field_module_enum(field: &dml::Field, pcr: &TokenStream, variant: Variant) ->
             let relation_model_name_snake = snake_ident(&relation_field.relation_info.referenced_model);
 
             let initial_nested_selections = match variant {
-                Variant::Include => quote!(<#relation_model_name_snake::Actions as #pcr::ModelActions>::scalar_selections()),
+                Variant::Include => quote!(<#relation_model_name_snake::Types as #pcr::ModelActions>::scalar_selections()),
                 Variant::Select => quote!(vec![])
             };
 
@@ -577,7 +577,7 @@ fn field_module_enum(field: &dml::Field, pcr: &TokenStream, variant: Variant) ->
                                 ),
                                 Self::Fetch(args) => (
                                     args.to_graphql().0,
-                                    <#relation_model_name_snake::Actions as #pcr::ModelActions>::scalar_selections()
+                                    <#relation_model_name_snake::Types as #pcr::ModelActions>::scalar_selections()
                                 )
                             };
 
@@ -618,7 +618,7 @@ fn field_module_enum(field: &dml::Field, pcr: &TokenStream, variant: Variant) ->
                                     nested_selections
                                 },
                                 Self::Fetch => {
-                                    <#relation_model_name_snake::Actions as #pcr::ModelActions>::scalar_selections()
+                                    <#relation_model_name_snake::Types as #pcr::ModelActions>::scalar_selections()
                                 }
                             };
 
diff --git a/cli/src/generator/models/mod.rs b/cli/src/generator/models/mod.rs
index 21ca1751..c8939dd2 100644
--- a/cli/src/generator/models/mod.rs
+++ b/cli/src/generator/models/mod.rs
@@ -8,6 +8,7 @@ mod create;
 mod field;
 mod where_params;
 mod include_select;
+mod types;
 
 use include_select::*;
 use prisma_client_rust_sdk::prelude::*;
@@ -258,6 +259,7 @@ pub fn generate(args: &GenerateArgs, module_path: TokenStream) -> Vec<TokenStrea
         let include_macro = include::model_macro(model, &module_path);
         let include_params_enum = include::model_module_enum(&model, &pcr);
         let actions_struct = actions::struct_definition(&model, args);
+        let types_struct = types::struct_definition(&model);
 
         quote! {
             pub mod #model_name_snake {
@@ -286,22 +288,24 @@ pub fn generate(args: &GenerateArgs, module_path: TokenStream) -> Vec<TokenStrea
 
                 #where_params_enums
 
+                #types_struct
+
                 // 'static since the actions struct is only used for types
 
-                pub type UniqueArgs = ::prisma_client_rust::UniqueArgs<Actions<'static>>;
-                pub type ManyArgs = ::prisma_client_rust::ManyArgs<Actions<'static>>;
+                pub type UniqueArgs = ::prisma_client_rust::UniqueArgs<Types>;
+                pub type ManyArgs = ::prisma_client_rust::ManyArgs<Types>;
                 
-                pub type Count<'a> = ::prisma_client_rust::Count<'a, Actions<'a>>;
-                pub type Create<'a> = ::prisma_client_rust::Create<'a, Actions<'a>>;
-                pub type CreateMany<'a> = ::prisma_client_rust::CreateMany<'a, Actions<'a>>;
-                pub type FindUnique<'a> = ::prisma_client_rust::FindUnique<'a, Actions<'a>>;
-                pub type FindMany<'a> = ::prisma_client_rust::FindMany<'a, Actions<'a>>;
-                pub type FindFirst<'a> = ::prisma_client_rust::FindFirst<'a, Actions<'a>>;
-                pub type Update<'a> = ::prisma_client_rust::Update<'a, Actions<'a>>;
-                pub type UpdateMany<'a> = ::prisma_client_rust::UpdateMany<'a, Actions<'a>>;
-                pub type Upsert<'a> = ::prisma_client_rust::Upsert<'a, Actions<'a>>;
-                pub type Delete<'a> = ::prisma_client_rust::Delete<'a, Actions<'a>>;
-                pub type DeleteMany<'a> = ::prisma_client_rust::DeleteMany<'a, Actions<'a>>;
+                pub type Count<'a> = ::prisma_client_rust::Count<'a, Types>;
+                pub type Create<'a> = ::prisma_client_rust::Create<'a, Types>;
+                pub type CreateMany<'a> = ::prisma_client_rust::CreateMany<'a, Types>;
+                pub type FindUnique<'a> = ::prisma_client_rust::FindUnique<'a, Types>;
+                pub type FindMany<'a> = ::prisma_client_rust::FindMany<'a, Types>;
+                pub type FindFirst<'a> = ::prisma_client_rust::FindFirst<'a, Types>;
+                pub type Update<'a> = ::prisma_client_rust::Update<'a, Types>;
+                pub type UpdateMany<'a> = ::prisma_client_rust::UpdateMany<'a, Types>;
+                pub type Upsert<'a> = ::prisma_client_rust::Upsert<'a, Types>;
+                pub type Delete<'a> = ::prisma_client_rust::Delete<'a, Types>;
+                pub type DeleteMany<'a> = ::prisma_client_rust::DeleteMany<'a, Types>;
               
                 #actions_struct
             }
diff --git a/cli/src/generator/models/types.rs b/cli/src/generator/models/types.rs
new file mode 100644
index 00000000..91e90e69
--- /dev/null
+++ b/cli/src/generator/models/types.rs
@@ -0,0 +1,39 @@
+use crate::generator::prelude::*;
+
+pub fn scalar_selections_fn(model: &dml::Model) -> TokenStream {
+    let scalar_fields = model.scalar_fields().map(|f| &f.name);
+
+    quote! {
+        fn scalar_selections() -> Vec<::prisma_client_rust::Selection> {
+            [#(#scalar_fields),*]
+                .into_iter()
+                .map(::prisma_client_rust::sel)
+                .collect()
+        }
+    }
+}
+
+pub fn struct_definition(model: &dml::Model) -> TokenStream {
+    let pcr = quote!(::prisma_client_rust);
+
+    let model_name_str = &model.name;
+    let scalar_selections_fn = scalar_selections_fn(model);
+
+    quote! {
+        #[derive(Clone)]
+        pub struct Types;
+
+        impl #pcr::ModelActions for Types {
+            type Data = Data;
+            type Where = WhereParam;
+            type Set = SetParam;
+            type With = WithParam;
+            type OrderBy = OrderByParam;
+            type Cursor = UniqueWhereParam;
+
+            const MODEL: &'static str = #model_name_str;
+
+            #scalar_selections_fn
+        }
+    }
+}
diff --git a/cli/src/generator/models/with_params.rs b/cli/src/generator/models/with_params.rs
index 3c3b625d..a2e28770 100644
--- a/cli/src/generator/models/with_params.rs
+++ b/cli/src/generator/models/with_params.rs
@@ -33,7 +33,7 @@ fn into_selection_arm(field: &dml::RelationField) -> TokenStream {
     let body = match field.arity {
         dml::FieldArity::List => quote! {
             let (arguments, mut nested_selections) = args.to_graphql();
-            nested_selections.extend(<super::#relation_model_name_snake::Actions as #pcr::ModelActions>::scalar_selections());
+            nested_selections.extend(<super::#relation_model_name_snake::Types as #pcr::ModelActions>::scalar_selections());
 
             #pcr::Selection::new(
                 #field_name_str,
@@ -43,7 +43,7 @@ fn into_selection_arm(field: &dml::RelationField) -> TokenStream {
             )
         },
         _ => quote! {
-            let mut selections = <super::#relation_model_name_snake::Actions as #pcr::ModelActions>::scalar_selections();
+            let mut selections = <super::#relation_model_name_snake::Types as #pcr::ModelActions>::scalar_selections();
             selections.extend(args.with_params.into_iter().map(Into::<#pcr::Selection>::into));
 
             #pcr::Selection::new(
diff --git a/docs/src/config.ts b/docs/src/config.ts
index 9608b46b..19a7d084 100644
--- a/docs/src/config.ts
+++ b/docs/src/config.ts
@@ -1,21 +1,21 @@
 export const SITE = {
-  title: 'Prisma Client Rust',
-  description: 'Type-safe database access for Rust',
-  defaultLanguage: 'en_US',
+	title: 'Prisma Client Rust',
+	description: 'Type-safe database access for Rust',
+	defaultLanguage: 'en_US',
 };
 
 export const OPEN_GRAPH = {
-  image: {
-    src: 'https://github.com/withastro/astro/blob/main/assets/social/banner.jpg?raw=true',
-    alt:
-      'astro logo on a starry expanse of space,' +
-      ' with a purple saturn-like planet floating in the right foreground',
-  },
-  twitter: 'astrodotbuild',
+	image: {
+		src: 'https://github.com/withastro/astro/blob/main/assets/social/banner.jpg?raw=true',
+		alt:
+			'astro logo on a starry expanse of space,' +
+			' with a purple saturn-like planet floating in the right foreground',
+	},
+	twitter: 'astrodotbuild',
 };
 
 export const KNOWN_LANGUAGES = {
-  English: 'en',
+	English: 'en',
 };
 
 export const GITHUB_EDIT_URL = `https://github.com/brendonovich/prisma-client-rust/blob/main/docs/`;
@@ -30,44 +30,45 @@ export const DISCORD_INVITE_URL = `https://discord.gg/5M6fpszrry`;
 // }
 
 function section(name: string, data: any[]) {
-  let fsName = name.toLowerCase().replace(" ", "-");
+	let fsName = name.toLowerCase().replace(" ", "-");
 
-  return [
-    { text: name, header: true },
-    ...data.map(d => ({ ...d, link: `${fsName}/${d.link}` }))
-  ]
+	return [
+		{ text: name, header: true },
+		...data.map(d => ({ ...d, link: `${fsName}/${d.link}` }))
+	]
 }
 
 export const SIDEBAR = {
-  en: [
-    { text: 'Introduction', link: 'introduction' },
-    { text: '0.6.0 Migration', link: '0.6.0-migration' },
-    ...section("Getting Started", [
-      { text: 'Installation', link: 'installation' },
-      { text: 'Setup', link: 'setup' },
-      { text: 'Structure', link: 'structure' },
-    ]),
-    ...section("Reading Data", [
-      { text: "Find Queries", link: 'find' },
-      { text: "Fetching Relations", link: 'fetch' },
-      { text: "Pagination", link: 'pagination' },
-      { text: "Ordering", link: 'order-by' },
-      { text: "Counting Records", link: 'count' },
-      { text: "Select & Include", link: 'select-include' }
-    ]),
-    ...section("Writing Data", [
-      { text: "Create Queries", link: 'create' },
-      { text: "Update Queries", link: 'update' },
-      { text: "Delete Queries", link: 'delete' },
-      { text: "Upserting", link: 'upsert' },
-    ]),
-    ...section("Extra", [
-      { text: "Raw Queries", link: 'raw' },
-      { text: "Batching Queries", link: 'batching' },
-      { text: "Mocking Queries", link: 'mocking' },
-      { text: "Error Handling", link: "error-handling" },
-      { text: "Migrations", link: 'migrations' },
-      { text: "rspc Integration", link: 'rspc' }
-    ])
-  ],
+	en: [
+		{ text: 'Introduction', link: 'introduction' },
+		{ text: '0.6.0 Migration', link: '0.6.0-migration' },
+		...section("Getting Started", [
+			{ text: 'Installation', link: 'installation' },
+			{ text: 'Setup', link: 'setup' },
+			{ text: 'Structure', link: 'structure' },
+		]),
+		...section("Reading Data", [
+			{ text: "Find Queries", link: 'find' },
+			{ text: "Fetching Relations", link: 'fetch' },
+			{ text: "Pagination", link: 'pagination' },
+			{ text: "Ordering", link: 'order-by' },
+			{ text: "Counting Records", link: 'count' },
+			{ text: "Select & Include", link: 'select-include' }
+		]),
+		...section("Writing Data", [
+			{ text: "Create Queries", link: 'create' },
+			{ text: "Update Queries", link: 'update' },
+			{ text: "Delete Queries", link: 'delete' },
+			{ text: "Upserting", link: 'upsert' },
+		]),
+		...section("Extra", [
+			{ text: "Raw Queries", link: 'raw' },
+			{ text: "Batching Queries", link: 'batching' },
+			{ text: "Transactions", link: 'transactions' },
+			{ text: "Mocking Queries", link: 'mocking' },
+			{ text: "Error Handling", link: "error-handling" },
+			{ text: "Migrations", link: 'migrations' },
+			{ text: "rspc Integration", link: 'rspc' }
+		])
+	],
 };
diff --git a/docs/src/pages/extra/batching.md b/docs/src/pages/extra/batching.md
index af026677..c73bcd3e 100644
--- a/docs/src/pages/extra/batching.md
+++ b/docs/src/pages/extra/batching.md
@@ -6,13 +6,22 @@ layout: ../../layouts/MainLayout.astro
 `PrismaClient::_batch` allows you to sequentially execute multiple queries in a single transaction.
 If one of the queries fails, all changes will be rolled back.
 
-When providing queries to `_batch` there is no need to call `exec()`,
-but all queries must be put inside a valid container type.
+Data provided to `_batch` falls under two categories:
 
-#### Tuple Container
+- Containers: Root level collections that contain all items in the batch.
+Can be either a tuple or a type implementing `IntoIter`.
+
+- Items: Either a query or a collection (`Vec` or tuple) of nested queries.
+
+## Containers
+
+Even if your batch doesn't include nested items,
+you still need to put queries inside some sort of container.
+
+### Tuple
 
 Using a tuple allows for multiple types of queries to be used at once.
-The return type of `_batch` will be a tuple of the results of each query in the input tuple.
+The return type of `_batch` will be a tuple of the results of each item.
 
 ```rust
 use prisma::user;
@@ -28,10 +37,11 @@ let (user_one, user_two, user_count): (user::Data, user::Data, i64) = client
 assert_eq!(user_count, 2);
 ```
 
-#### Iterator Container
+### Iterator
 
-Using an iterator such as `Vec` allows for a dynamic number of a single type of query to be batched.
-The return type will be a `Vec` of the result of the input query type.
+Using a type that implements `IntoIter` such as `Vec` allows for
+a dynamic number of items to be batched together.
+The return type will be a `Vec` of the result of the item.
 
 ```rust
 use prisma::user;
@@ -47,7 +57,9 @@ let users: Vec<user::Data> = client
 assert_eq!(users.len(), 3);
 ```
 
-Since `_batch` accepts any iterator, queries can be constructed from external data without collecting into a `Vec`.
+`IntoIter` includes regular iterators,
+so you can pass them straight into `_batch` and
+`collect` will be called internally.
 
 ```rust
 use prisma::user;
@@ -67,3 +79,55 @@ let users: Vec<user::Data> = client
 
 assert_eq!(users.len(), 5);
 ```
+
+## Items
+
+Items can be either individual queries or a collection.
+
+### Tuple
+
+The same logic applies to a tuple item as a tuple container,
+with the item's result being a 1-1 mapping of each query to its result type.
+
+```rust
+let data: Vec<(user::Data, post::Data)> = client
+		._batch(vec![
+				(client.user().create(..), client.post().create(..)),
+				(client.user().create(..), client.post().create(..)),
+		])
+		.await?;
+```
+
+### Vec
+
+Unlike containers, only `Vec` can be used for dynamic collections of queries.
+Apart from that, the behaviour is the same.
+
+```rust
+let data: (Vec<user::Data>, Vec<post::Data>) = client
+		._batch((
+				vec![client.user().create(..), client.user().create(..)],
+				vec![client.post().create(..), client.post().create(..)],
+		))
+		.await?;
+```
+
+### Nesting
+
+Any combination and nesting of items can be put inside a container,
+allowing for heavily nested return types.
+
+This example isn't really practical, but it's possible.
+
+```rust
+let data: Vec<(
+		Vec<(user::Data, post::data)>,
+		(Vec<user::Data>, Vec<post::Data>),
+)> = client._batch(vec![(
+		vec![
+				(client.user().create(..), client.post.create(..)),
+				(client.user().create(..), client.post.create(..)),
+		],
+		(vec![client.user().create(..)], vec![client.post.create(..)]),
+)]);
+```
diff --git a/docs/src/pages/extra/mocking.md b/docs/src/pages/extra/mocking.md
index 0f571cec..bbd11a12 100644
--- a/docs/src/pages/extra/mocking.md
+++ b/docs/src/pages/extra/mocking.md
@@ -3,6 +3,8 @@ title: Mocking Queries
 layout: ../../layouts/MainLayout.astro
 ---
 
+_Available since v0.6.4_
+
 When writing tests for function which use the Prisma client,
 it can be difficult to have a real database running to test with.
 To combat this, you can enable the `mocking` feature on `prisma-client-rust` and `prisma-client-rust-cli`
@@ -22,11 +24,12 @@ Say you have a function `get_post_title`:
 
 ```rust
 use prisma::{PrismaClient, post};
+use prisma_client_rust::queries;
 
 async fn get_post_title(
     client: &PrismaClient,
     post_id: String,
-) -> prisma_client_rust::QueryResult<Option<String>> {
+) -> queries::Result<Option<String>> {
     let post = client.post().find_unique(post::id::equals(post_id)).await?;
 
     post.map(|post| post.title)
@@ -42,31 +45,31 @@ The mock client will use data from the mock store to resolve queries.
 ```rust
 #[cfg(test)]
 mod test {
-    use super::*;
+		use super::*;
 
-    #[tokio::test]
-    async fn gets_title() -> prisma_client_rust::QueryResult<()> {
-        let (client, mock) = PrismaClient::_mock().await;
+		#[tokio::test]
+		async fn gets_title() -> queries::Result<()> {
+				let (client, mock) = PrismaClient::_mock().await;
 
-        let id = "123".to_string();
-        let expected_title = "Test".to_string();
+				let id = "123".to_string();
+				let expected_title = "Test".to_string();
 
-        mock.expect(
-            // First argument is query without calling 'exec'
-            client.post().find_unique(post::id::equals(post_id)),
-            // Second argument is expected return type.
-            // This will fail to compile if it does not match
-            // the return type of the query
-            post::Data {
-                id: id.clone(),
-                title: expected_title.to_string(),
-            },
-        )
-        .await;
+				mock.expect(
+						// First argument is query without calling 'exec'
+						client.post().find_unique(post::id::equals(post_id)),
+						// Second argument is expected return type.
+						// This will fail to compile if it does not match
+						// the return type of the query
+						post::Data {
+								id: id.clone(),
+								title: expected_title.to_string(),
+						},
+				)
+				.await;
 
-       let title = get_post_title(&client, id).await?;
+			 let title = get_post_title(&client, id).await?;
 
-       assert_eq!(title, Some(expected_title));
-    }
+			 assert_eq!(title, Some(expected_title));
+		}
 }
 ```
diff --git a/docs/src/pages/extra/transactions.md b/docs/src/pages/extra/transactions.md
new file mode 100644
index 00000000..f1f962c0
--- /dev/null
+++ b/docs/src/pages/extra/transactions.md
@@ -0,0 +1,209 @@
+---
+title: Transactions
+layout: ../../layouts/MainLayout.astro
+---
+
+_Available since v0.6.4_
+
+While batching can cover most use cases where queries need to succeed or fail together,
+it doesn't allow you to run code that executes between each query.
+Instead, you can use `PrismaClient::_transaction`,
+which provides both closure-based and manual methods of executing individual queries and arbitrary code inside a transaction.
+
+Both methods provide the ability to commit and roll back a transaction,
+and produce a dedicated instance of `PrismaClient` that must be used while executing the transaction.
+
+## Transaction Closures
+
+Running your transaction in a closure is the approach used by the official
+[Prisma client](https://www.prisma.io/docs/concepts/components/prisma-client/transactions#interactive-transactions).
+It can be nice as all of your transaction's code can be kept in one place,
+but it has the downside that closures can be tricky to work with.
+
+To perform a transaction this way,
+just call `client._transaction().run(..)`
+and provide a closure returning an `async move` block to `run()`.
+The closure should accept one argument (the dedicated `PrismaClient` instance),
+and return a `Result`.
+
+If the closure returns `Ok`,
+the transaction will attempt to commit itself,
+and if it returns `Err` it will attempt to roll back.
+
+```rust
+let (user, post) = client
+		._transaction()
+		.run(|client| async move {
+				let user = client
+						.user()
+						.create("brendan".to_string(), vec![])
+						.exec()
+						.await?;
+
+				client
+						.post()
+						.create(
+								"test".to_string(),
+								true,
+								vec![post::author::connect(
+										user::id::equals(user.id.clone())
+								)],
+						)
+						.exec()
+						.await
+						// if query succeeds, return user + post from transaction
+						.map(|post| (user, post))
+		})
+		.await?;
+```
+
+### Error Types
+
+Transaction closures must return a `Result`,
+but the `Err` generic can be almost anything,
+and the `Ok` generic is not restricted at all.
+
+To allow using `?` inside transaction closures,
+error types must implement `From<prisma_client_rust::QueryError>`
+(this includes `QueryError` itself if you don't need a custom error type).
+This can be done either with a manual implementation:
+
+```rust
+use prisma_client_rust::QueryError;
+
+enum CustomError {
+		QueryError(QueryError)
+}
+
+impl From<QueryError> for CustomError {
+		fn from(e: QueryError) { ... }
+}
+```
+
+or via a library like [`thiserror`](https://docs.rs/thiserror/latest/thiserror/) with its `#[from]` attribute:
+
+```rust
+#[derive(thiserror::Error)]
+
+enum CustomError {
+		#[error("Database error occurred")]
+		QueryError(prisma_client_rust::QueryError),
+		...
+}
+```
+
+### Specifying The Error Type
+
+1. Use the generic parameter directly. This works,
+but requires `_` for the rest of the `run`'s generic parameters,
+which probably isn't desirable.
+
+```rust
+cilent
+		._transaction()
+		.run::<CustomError, _, _, _>(..)
+		.await?;
+```
+2. Type casting. If your closure returns `Ok`,
+you can cast it to a `Result` with the appropriate error type.
+
+```rust
+client
+		._transaction()
+		.run(|client| async move {
+				let user = client
+						.user()
+						.create("brendan".to_string(), vec![])
+						.exec()
+						.await?;
+
+				Ok(user) as Result<_, CustomError>;
+		})
+		.await?
+```
+
+3. Returning a query's `Result` -
+this is probably the nicest looking solution.
+If you are using a custom error type,
+use `map_err` after `await` to transform the `QueryError` into your custom error type.
+
+```rust
+client
+		._transaction()
+		.run(|client| async move {
+				client
+						.user()
+						.create("brendan".to_string(), vec![])
+						.exec()
+						// No `?` so that `Result` with error type is returned
+						.await
+		})
+		.await?
+```
+
+
+## Manual Transactions
+
+If you'd prefer to manually control when the transaction commits and rolls back,
+use `client._transaction().begin()` to not only get a dedicated `PrismaClient`,
+but also a `TransactionManger` instance that you can `commit` and `rollback` with:
+
+```rust
+let (client, tx) = client
+		._transaction()
+		.begin()
+		.await?;
+```
+
+The above example names the client instance `client`,
+meaning that it would shadow the original client it was created from,
+making it inaccessible.
+You could give the client instance a name like `tx_client`,
+or put all transaction logic inside a block so that the original `client` variable
+isn't shadowed in the rest of your code.
+
+`commit` and `rollback` consume the client created by `begin` as their only argument.
+This is done because those functions need to do things with the client,
+and as an extra precaution against the transaction-specific client being used once the transaction is complete.
+
+```rust
+tx.commit(client).await?;
+// or 
+tx.rollback(client).await?;
+```
+
+
+### Error Handling
+
+Care must be taken when handling errors using this method.
+Simply using `?` could result in your code returning before `commit` or `rollback` is ran.
+An easy way to avoid this is to put your transaction logic in a function where it is safe to use `?`,
+and then `commit` or `rollback` based on the result of the function.
+```rust
+let (client, tx) = client
+		._transaction()
+		.begin()
+		.await?;
+
+async fn do_stuff(client: &PrismaClient) -> ... {
+		let user = client
+				.user()
+				.create("brendan".to_string(), vec![])
+				.exec()
+				.await?; // Early return won't escape transaction
+
+		...
+}
+
+// This is very similar to the closure method's internals
+let result = match do_stuff(client).await {
+		Ok(v) => {
+				tx.commit(client).await?;
+				Ok(v)
+		},
+		Err(e) => {
+				tx.rollback(client).await?;
+				Err(e)
+		}
+};
+```
diff --git a/docs/src/styles/index.css b/docs/src/styles/index.css
index 30fae705..7224dd6a 100644
--- a/docs/src/styles/index.css
+++ b/docs/src/styles/index.css
@@ -201,10 +201,11 @@ code {
 
 pre.astro-code > code {
 	all: unset;
+		tab-size: 2;
 }
 
 pre > code {
-	font-size: 1em;
+		font-size: 1em;
 }
 
 table,
diff --git a/examples/rspc/Cargo.toml b/examples/rspc/Cargo.toml
index d2d85ca4..694e6ab5 100644
--- a/examples/rspc/Cargo.toml
+++ b/examples/rspc/Cargo.toml
@@ -5,7 +5,7 @@ edition = "2021"
 default-run = "rspc-example"
 
 [dependencies]
-prisma-client-rust = { features = ["rspc", "migrations", "sqlite"], default-features = false, path = "../../" }
+prisma-client-rust = { features = ["rspc", "migrations", "sqlite", "mocking"], default-features = false, path = "../../" }
 serde = { version = "1.0", features = ["derive"] }
 rspc = { version = "0.1.2" }
 tokio = "1.20.1"
diff --git a/src/client.rs b/src/client.rs
index 77b928f7..896f4727 100644
--- a/src/client.rs
+++ b/src/client.rs
@@ -1,7 +1,7 @@
 use crate::queries::ModelActions;
 use crate::{ActionNotifier, ModelQuery};
 use diagnostics::Diagnostics;
-use query_core::{schema_builder, CoreError, Operation};
+use query_core::{schema_builder, BatchDocumentTransaction, CoreError, Operation, TxId};
 use schema::QuerySchema;
 use serde::de::{DeserializeOwned, IntoDeserializer};
 use std::sync::Arc;
@@ -11,11 +11,23 @@ use crate::{prisma_value, ModelMutationCallbackData, ModelOperation, QueryError,
 
 pub type Executor = Box<dyn query_core::QueryExecutor + Send + Sync + 'static>;
 
+pub trait PrismaClient {
+    fn internals(&self) -> &PrismaClientInternals;
+    fn internals_mut(&mut self) -> &mut PrismaClientInternals;
+    fn with_tx_id(&self, tx_id: Option<TxId>) -> Self;
+}
+
+pub struct ExecutorConnector {
+    pub executor: Executor,
+    pub query_schema: Arc<QuerySchema>,
+    pub url: String,
+}
+
+#[derive(Clone)]
 pub(crate) enum ExecutionEngine {
     Real {
-        executor: Executor,
-        query_schema: Arc<QuerySchema>,
-        url: String,
+        connector: Arc<ExecutorConnector>,
+        tx_id: Option<TxId>,
     },
     #[cfg(feature = "mocking")]
     Mock(crate::MockStore),
@@ -24,13 +36,10 @@ pub(crate) enum ExecutionEngine {
 impl ExecutionEngine {
     async fn execute(&self, op: Operation) -> Result<serde_value::Value> {
         match self {
-            Self::Real {
-                executor,
-                query_schema,
-                ..
-            } => {
-                let response = executor
-                    .execute(None, op, query_schema.clone(), None)
+            Self::Real { connector, tx_id } => {
+                let response = connector
+                    .executor
+                    .execute(tx_id.clone(), op, connector.query_schema.clone(), None)
                     .await
                     .map_err(|e| QueryError::Execute(e.into()))?;
 
@@ -48,13 +57,16 @@ impl ExecutionEngine {
         ops: Vec<Operation>,
     ) -> Result<Vec<Result<serde_value::Value>>> {
         match self {
-            Self::Real {
-                executor,
-                query_schema,
-                ..
-            } => {
-                let response = executor
-                    .execute_all(None, ops, None, query_schema.clone(), None)
+            Self::Real { connector, .. } => {
+                let response = connector
+                    .executor
+                    .execute_all(
+                        None,
+                        ops,
+                        Some(BatchDocumentTransaction::new(None)),
+                        connector.query_schema.clone(),
+                        None,
+                    )
                     .await
                     .map_err(|e| QueryError::Execute(e.into()))?;
 
@@ -82,13 +94,24 @@ impl ExecutionEngine {
             }
         }
     }
+
+    fn with_tx_id(&self, tx_id: Option<TxId>) -> Self {
+        match self {
+            Self::Real { connector, .. } => Self::Real {
+                connector: connector.clone(),
+                tx_id,
+            },
+            _ => self.clone(),
+        }
+    }
 }
 
 /// The data held by the generated PrismaClient
 /// Do not use this in your own code!
+#[derive(Clone)]
 pub struct PrismaClientInternals {
     pub(crate) engine: ExecutionEngine,
-    pub action_notifier: crate::ActionNotifier,
+    pub action_notifier: Arc<crate::ActionNotifier>,
 }
 
 impl PrismaClientInternals {
@@ -167,11 +190,14 @@ impl PrismaClientInternals {
 
         Ok(Self {
             engine: ExecutionEngine::Real {
-                executor,
-                query_schema,
-                url,
+                connector: Arc::new(ExecutorConnector {
+                    executor,
+                    query_schema,
+                    url,
+                }),
+                tx_id: None,
             },
-            action_notifier,
+            action_notifier: Arc::new(action_notifier),
         })
     }
 
@@ -182,7 +208,7 @@ impl PrismaClientInternals {
         (
             Self {
                 engine: ExecutionEngine::Mock(mock_store.clone()),
-                action_notifier,
+                action_notifier: Arc::new(action_notifier),
             },
             mock_store,
         )
@@ -192,7 +218,14 @@ impl PrismaClientInternals {
         match &self.engine {
             #[cfg(feature = "mocking")]
             ExecutionEngine::Mock(_) => "mock",
-            ExecutionEngine::Real { url, .. } => url,
+            ExecutionEngine::Real { connector, .. } => &connector.url,
+        }
+    }
+
+    pub fn with_tx_id(&self, tx_id: Option<TxId>) -> Self {
+        Self {
+            engine: self.engine.with_tx_id(tx_id),
+            action_notifier: self.action_notifier.clone(),
         }
     }
 }
diff --git a/src/lib.rs b/src/lib.rs
index d9ce62fd..5bbd775c 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -10,6 +10,7 @@ pub mod queries;
 pub mod raw;
 pub mod serde;
 mod traits;
+mod transaction;
 
 use std::collections::HashMap;
 
@@ -37,6 +38,7 @@ pub use operator::Operator;
 pub use queries::*;
 pub use raw::*;
 pub use traits::*;
+pub use transaction::*;
 
 #[cfg(feature = "rspc")]
 pub use rspc;
diff --git a/src/mock.rs b/src/mock.rs
index db7a32a3..28121d64 100644
--- a/src/mock.rs
+++ b/src/mock.rs
@@ -6,7 +6,7 @@ use serde::Serialize;
 use serde_value::Value;
 use tokio::sync::Mutex;
 
-use crate::Query;
+use crate::{Query, QueryConvert};
 
 #[derive(Default, Clone)]
 pub struct MockStore {
@@ -31,7 +31,7 @@ impl MockStore {
 
     pub async fn expect<'a, Q: Query<'a>>(&self, query: Q, expected: Q::RawType)
     where
-        <Q as Query<'a>>::RawType: Serialize,
+        <Q as QueryConvert>::RawType: Serialize,
     {
         self.add_op(query.graphql().0, serde_value::to_value(expected).unwrap())
             .await;
diff --git a/src/queries/batch.rs b/src/queries/batch.rs
index dc28cd10..91df3199 100644
--- a/src/queries/batch.rs
+++ b/src/queries/batch.rs
@@ -1,12 +1,17 @@
-use std::collections::VecDeque;
+use std::{collections::VecDeque, num::NonZeroUsize};
 
 use query_core::Operation;
 
-use crate::{PrismaClientInternals, Query};
+use crate::{PrismaClientInternals, Query, QueryConvert};
+
+pub enum VecMeta {
+    Empty,
+    NotEmpty(NonZeroUsize, Box<BatchItemDataMeta>),
+}
 
 pub enum BatchItemDataMeta {
     Query,
-    Vec(usize, Box<Self>),
+    Vec(VecMeta),
     Tuple(Vec<Self>),
 }
 
@@ -20,7 +25,11 @@ impl BatchItemData {
     fn meta(&self) -> BatchItemDataMeta {
         match self {
             Self::Query(_) => BatchItemDataMeta::Query,
-            Self::Vec(v) => BatchItemDataMeta::Vec(v.len(), Box::new(v[0].meta())),
+            Self::Vec(v) => BatchItemDataMeta::Vec({
+                NonZeroUsize::new(v.len())
+                    .map(|size| VecMeta::NotEmpty(size, Box::new(v[0].meta())))
+                    .unwrap_or(VecMeta::Empty)
+            }),
             Self::Tuple(v) => BatchItemDataMeta::Tuple(v.iter().map(BatchItemData::meta).collect()),
         }
     }
@@ -35,7 +44,7 @@ impl BatchItemData {
 }
 
 pub enum BatchDataMeta {
-    Iterator(usize, BatchItemDataMeta),
+    Iterator(VecMeta),
     Tuple(Vec<BatchItemDataMeta>),
 }
 
@@ -47,7 +56,11 @@ pub enum BatchData {
 impl BatchData {
     fn meta(&self) -> BatchDataMeta {
         match self {
-            Self::Iterator(v) => BatchDataMeta::Iterator(v.len(), v[0].meta()),
+            Self::Iterator(v) => BatchDataMeta::Iterator(
+                NonZeroUsize::new(v.len())
+                    .map(|size| VecMeta::NotEmpty(size, Box::new(v[0].meta())))
+                    .unwrap_or(VecMeta::Empty),
+            ),
             Self::Tuple(v) => BatchDataMeta::Tuple(v.iter().map(BatchItemData::meta).collect()),
         }
     }
@@ -66,10 +79,10 @@ impl BatchData {
     }
 }
 
-pub async fn batch<'a, T: BatchContainer<'a, Marker>, Marker>(
+pub async fn batch<'batch, 'b, T: BatchContainer<'batch, Marker>, Marker>(
     container: T,
-    client: &'a PrismaClientInternals,
-) -> super::Result<<T as BatchContainer<Marker>>::ReturnType> {
+    client: &'b PrismaClientInternals,
+) -> super::Result<<T as BatchContainer<'batch, Marker>>::ReturnType> {
     let data = container.data();
     let meta = data.meta();
 
@@ -85,20 +98,24 @@ pub async fn batch<'a, T: BatchContainer<'a, Marker>, Marker>(
     Ok(T::resolve(meta, values))
 }
 
-pub trait BatchItem<'a> {
+pub trait BatchItemParent {
     type ReturnValue;
+}
 
+pub trait BatchItem<'a>: BatchItemParent {
     fn data(self) -> BatchItemData;
 
     fn resolve(
         meta: &BatchItemDataMeta,
         values: &mut VecDeque<serde_value::Value>,
-    ) -> Self::ReturnValue;
+    ) -> <Self as BatchItemParent>::ReturnValue;
 }
 
-impl<'a, Q: Query<'a>> BatchItem<'a> for Q {
+impl<Q: QueryConvert> BatchItemParent for Q {
     type ReturnValue = Q::ReturnValue;
+}
 
+impl<'a, 'b, Q: Query<'a>> BatchItem<'b> for Q {
     fn data(self) -> BatchItemData {
         BatchItemData::Query(self.graphql().0)
     }
@@ -106,7 +123,7 @@ impl<'a, Q: Query<'a>> BatchItem<'a> for Q {
     fn resolve(
         _: &BatchItemDataMeta,
         values: &mut VecDeque<serde_value::Value>,
-    ) -> Self::ReturnValue {
+    ) -> <Self as BatchItemParent>::ReturnValue {
         Q::convert(
             values
                 .pop_front()
@@ -117,9 +134,11 @@ impl<'a, Q: Query<'a>> BatchItem<'a> for Q {
     }
 }
 
-impl<'a, I: BatchItem<'a>> BatchItem<'a> for Vec<I> {
-    type ReturnValue = Vec<I::ReturnValue>;
+impl<'batch, I: BatchItemParent> BatchItemParent for Vec<I> {
+    type ReturnValue = Vec<<I as BatchItemParent>::ReturnValue>;
+}
 
+impl<'batch, 'query, I: Query<'query>> BatchItem<'batch> for Vec<I> {
     fn data(self) -> BatchItemData {
         BatchItemData::Vec(self.into_iter().map(BatchItem::data).collect())
     }
@@ -127,18 +146,21 @@ impl<'a, I: BatchItem<'a>> BatchItem<'a> for Vec<I> {
     fn resolve(
         meta: &BatchItemDataMeta,
         values: &mut VecDeque<serde_value::Value>,
-    ) -> Self::ReturnValue {
+    ) -> <Self as BatchItemParent>::ReturnValue {
         match meta {
-            BatchItemDataMeta::Vec(count, meta) => (0..*count)
-                .map(|_| I::resolve(meta.as_ref(), values))
-                .collect(),
+            BatchItemDataMeta::Vec(meta) => match meta {
+                VecMeta::Empty => vec![],
+                VecMeta::NotEmpty(size, meta) => (0..size.get())
+                    .map(|_| <I as BatchItem>::resolve(meta.as_ref(), values))
+                    .collect(),
+            },
             _ => unreachable!(),
         }
     }
 }
 
 /// A container that can hold queries to batch into a transaction
-pub trait BatchContainer<'a, Marker> {
+pub trait BatchContainer<'batch, Marker> {
     type ReturnType;
 
     fn data(self) -> BatchData;
@@ -146,8 +168,10 @@ pub trait BatchContainer<'a, Marker> {
     fn resolve(meta: BatchDataMeta, values: VecDeque<serde_value::Value>) -> Self::ReturnType;
 }
 
-impl<'a, T: BatchItem<'a>, I: IntoIterator<Item = T>> BatchContainer<'a, ()> for I {
-    type ReturnType = Vec<T::ReturnValue>;
+impl<'batch, 't: 'batch, T: BatchItem<'t>, I: IntoIterator<Item = T>> BatchContainer<'batch, ()>
+    for I
+{
+    type ReturnType = Vec<<T as BatchItemParent>::ReturnValue>;
 
     fn data(self) -> BatchData {
         BatchData::Iterator(self.into_iter().map(BatchItem::data).collect())
@@ -155,9 +179,12 @@ impl<'a, T: BatchItem<'a>, I: IntoIterator<Item = T>> BatchContainer<'a, ()> for
 
     fn resolve(meta: BatchDataMeta, mut values: VecDeque<serde_value::Value>) -> Self::ReturnType {
         match meta {
-            BatchDataMeta::Iterator(count, meta) => {
-                (0..count).map(|_| T::resolve(&meta, &mut values)).collect()
-            }
+            BatchDataMeta::Iterator(meta) => match meta {
+                VecMeta::Empty => vec![],
+                VecMeta::NotEmpty(size, meta) => (0..size.get())
+                    .map(|_| T::resolve(&meta, &mut values))
+                    .collect(),
+            },
             _ => unreachable!(),
         }
     }
@@ -166,80 +193,76 @@ impl<'a, T: BatchItem<'a>, I: IntoIterator<Item = T>> BatchContainer<'a, ()> for
 pub enum TupleMarker {}
 
 macro_rules! impl_tuple {
-    ($($generic:ident),+) => {
-        #[allow(warnings)]
-        impl<'a, $($generic: BatchItem<'a>),+> BatchContainer<'a, TupleMarker> for ($($generic),+) {
-            type ReturnType = ($($generic::ReturnValue),+);
-
-            fn data(self) -> BatchData {
-                let ($($generic),+) = self;
-
-                BatchData::Tuple(
-                    vec![$(BatchItem::data($generic)),+]
-                )
-            }
+    ($generic_1: ident, $($generic:ident),+) => {
+        impl_tuple!(impl $generic_1 $(,$generic)+);
+        impl_tuple!($($generic),+);
+    };
+    ($generic:ident) => {};
+    (impl $generic:ident) => {};
+    (impl $($generic:ident),+) => {
+        paste::paste! {
+            #[allow(warnings)]
+            impl<'batch, $( [< "'" $generic >]: 'batch),+, $($generic: BatchItem<[< "'" $generic >]>),+> BatchContainer<'batch, TupleMarker> for ($($generic),+) {
+                type ReturnType = ($(<$generic as BatchItemParent>::ReturnValue),+);
+
+                fn data(self) -> BatchData {
+                    let ($($generic),+) = self;
+
+                    BatchData::Tuple(
+                        vec![$(BatchItem::data($generic)),+]
+                    )
+                }
 
-            fn resolve(meta: BatchDataMeta, mut values: VecDeque<serde_value::Value>) -> Self::ReturnType {
-                match meta {
-                    BatchDataMeta::Tuple(metas) => {
-                        let mut metas_iter = metas.iter();
+                fn resolve(meta: BatchDataMeta, mut values: VecDeque<serde_value::Value>) -> Self::ReturnType {
+                    match meta {
+                        BatchDataMeta::Tuple(metas) => {
+                            let mut metas_iter = metas.iter();
 
-                        (($(<$generic as BatchItem>::resolve(metas_iter.next().unwrap(), &mut values)),+))
-                    },
-                    _ => unreachable!()
+                            (($(<$generic as BatchItem>::resolve(metas_iter.next().unwrap(), &mut values)),+))
+                        },
+                        _ => unreachable!()
+                    }
                 }
             }
-        }
 
-        #[allow(warnings)]
-        impl<'a, $($generic: BatchItem<'a>),+> BatchItem<'a> for ($($generic),+) {
-            type ReturnValue = ($($generic::ReturnValue),+);
+            #[allow(warnings)]
+            impl<$($generic: BatchItemParent),+> BatchItemParent for ($($generic),+) {
+                type ReturnValue = ($(<$generic as BatchItemParent>::ReturnValue),+);
+            }
 
-            fn data(self) -> BatchItemData {
-                let ($($generic),+) = self;
+            #[allow(warnings)]
+            impl<'batch, $( [< "'" $generic >]: 'batch),+, $($generic: BatchItem<[< "'" $generic >]>),+> BatchItem<'batch> for ($($generic),+) {
+                fn data(self) -> BatchItemData {
+                    let ($($generic),+) = self;
 
-                BatchItemData::Tuple(
-                    vec![$(BatchItem::data($generic)),+]
-                )
-            }
+                    BatchItemData::Tuple(
+                        vec![$(BatchItem::data($generic)),+]
+                    )
+                }
 
-            fn resolve(
-                meta: &BatchItemDataMeta,
-                values: &mut VecDeque<serde_value::Value>,
-            ) -> Self::ReturnValue {
-                match meta {
-                    BatchItemDataMeta::Tuple(meta) => {
-                        let mut meta = meta.iter();
-
-                        ($($generic::resolve(meta.next().unwrap(), values)),+)
-                    },
-                    _ => unreachable!(),
+                fn resolve(
+                    meta: &BatchItemDataMeta,
+                    values: &mut VecDeque<serde_value::Value>,
+                ) -> <Self as BatchItemParent>::ReturnValue {
+                    match meta {
+                        BatchItemDataMeta::Tuple(meta) => {
+                            let mut meta = meta.iter();
+
+                            ($($generic::resolve(meta.next().unwrap(), values)),+)
+                        },
+                        _ => unreachable!(),
+                    }
                 }
             }
         }
-    }
+    };
 }
 
-impl_tuple!(T1, T2);
-impl_tuple!(T1, T2, T3);
-impl_tuple!(T1, T2, T3, T4);
-impl_tuple!(T1, T2, T3, T4, T5);
-impl_tuple!(T1, T2, T3, T4, T5, T6);
-impl_tuple!(T1, T2, T3, T4, T5, T6, T7);
-impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8);
-impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9);
-impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10);
-impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11);
-impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12);
-impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13);
-impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14);
-impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15);
-impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16);
 impl_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17);
 
 /// TODO: remove this in 0.7.0
 #[allow(warnings)]
-impl<'a, Q: Query<'a>> BatchContainer<'a, TupleMarker> for Q {
+impl<'batch, 'query, Q: Query<'query>> BatchContainer<'batch, TupleMarker> for Q {
     type ReturnType = Q::ReturnValue;
 
     fn data(self) -> BatchData {
diff --git a/src/queries/count.rs b/src/queries/count.rs
index 735d6912..2f59112d 100644
--- a/src/queries/count.rs
+++ b/src/queries/count.rs
@@ -4,7 +4,8 @@ use serde::Deserialize;
 
 use crate::{
     merge_fields, ModelActions, ModelOperation, ModelQuery, ModelReadOperation, OrderByQuery,
-    PaginatedQuery, PrismaClientInternals, Query, SerializedWhereInput, WhereInput, WhereQuery,
+    PaginatedQuery, PrismaClientInternals, Query, QueryConvert, SerializedWhereInput, WhereInput,
+    WhereQuery,
 };
 
 pub struct Count<'a, Actions: ModelActions> {
@@ -63,10 +64,16 @@ pub struct CountResult {
     _all: i64,
 }
 
-impl<'a, Actions: ModelActions> Query<'a> for Count<'a, Actions> {
+impl<'a, Actions: ModelActions> QueryConvert for Count<'a, Actions> {
     type RawType = CountAggregateResult;
     type ReturnValue = i64;
 
+    fn convert(raw: Self::RawType) -> Self::ReturnValue {
+        raw._count._all
+    }
+}
+
+impl<'a, Actions: ModelActions> Query<'a> for Count<'a, Actions> {
     fn graphql(self) -> (Operation, &'a PrismaClientInternals) {
         (
             Operation::Read(Self::base_selection(
@@ -128,10 +135,6 @@ impl<'a, Actions: ModelActions> Query<'a> for Count<'a, Actions> {
             self.client,
         )
     }
-
-    fn convert(raw: Self::RawType) -> Self::ReturnValue {
-        raw._count._all
-    }
 }
 
 impl<'a, Actions: ModelActions> ModelQuery<'a> for Count<'a, Actions> {
diff --git a/src/queries/create.rs b/src/queries/create.rs
index 9b57bc4a..8abc1ad8 100644
--- a/src/queries/create.rs
+++ b/src/queries/create.rs
@@ -3,7 +3,8 @@ use query_core::{Operation, Selection};
 
 use crate::{
     merge_fields, Include, IncludeType, ModelActions, ModelOperation, ModelQuery,
-    ModelWriteOperation, PrismaClientInternals, Query, Select, SelectType, SetQuery, WithQuery,
+    ModelWriteOperation, PrismaClientInternals, Query, QueryConvert, Select, SelectType, SetQuery,
+    WithQuery,
 };
 
 pub struct Create<'a, Actions: ModelActions> {
@@ -68,10 +69,16 @@ impl<'a, Actions: ModelActions> Create<'a, Actions> {
     }
 }
 
-impl<'a, Actions: ModelActions> Query<'a> for Create<'a, Actions> {
+impl<'a, Actions: ModelActions> QueryConvert for Create<'a, Actions> {
     type RawType = Actions::Data;
     type ReturnValue = Self::RawType;
 
+    fn convert(raw: Self::RawType) -> Self::ReturnValue {
+        raw
+    }
+}
+
+impl<'a, Actions: ModelActions> Query<'a> for Create<'a, Actions> {
     fn graphql(self) -> (Operation, &'a PrismaClientInternals) {
         let mut scalar_selections = Actions::scalar_selections();
 
@@ -82,10 +89,6 @@ impl<'a, Actions: ModelActions> Query<'a> for Create<'a, Actions> {
             self.client,
         )
     }
-
-    fn convert(raw: Self::RawType) -> Self::ReturnValue {
-        raw
-    }
 }
 
 impl<'a, Actions: ModelActions> ModelQuery<'a> for Create<'a, Actions> {
diff --git a/src/queries/create_many.rs b/src/queries/create_many.rs
index 1bd06c3f..18573afc 100644
--- a/src/queries/create_many.rs
+++ b/src/queries/create_many.rs
@@ -3,7 +3,7 @@ use query_core::{Operation, Selection};
 
 use crate::{
     merge_fields, BatchResult, ModelActions, ModelOperation, ModelQuery, ModelWriteOperation,
-    PrismaClientInternals, Query,
+    PrismaClientInternals, Query, QueryConvert,
 };
 
 pub struct CreateMany<'a, Actions: ModelActions> {
@@ -63,10 +63,16 @@ impl<'a, Actions: ModelActions> CreateMany<'a, Actions> {
     }
 }
 
-impl<'a, Actions: ModelActions> Query<'a> for CreateMany<'a, Actions> {
+impl<'a, Actions: ModelActions> QueryConvert for CreateMany<'a, Actions> {
     type RawType = BatchResult;
     type ReturnValue = i64;
 
+    fn convert(raw: Self::RawType) -> Self::ReturnValue {
+        raw.count
+    }
+}
+
+impl<'a, Actions: ModelActions> Query<'a> for CreateMany<'a, Actions> {
     fn graphql(self) -> (Operation, &'a PrismaClientInternals) {
         (
             Operation::Write(Self::to_selection(
@@ -77,10 +83,6 @@ impl<'a, Actions: ModelActions> Query<'a> for CreateMany<'a, Actions> {
             self.client,
         )
     }
-
-    fn convert(raw: Self::RawType) -> Self::ReturnValue {
-        raw.count
-    }
 }
 
 impl<'a, Actions: ModelActions> ModelQuery<'a> for CreateMany<'a, Actions> {
diff --git a/src/queries/delete.rs b/src/queries/delete.rs
index f7295026..fce85b11 100644
--- a/src/queries/delete.rs
+++ b/src/queries/delete.rs
@@ -3,7 +3,7 @@ use query_core::{Operation, Selection};
 
 use crate::{
     Include, IncludeType, ModelActions, ModelOperation, ModelQuery, ModelWriteOperation,
-    PrismaClientInternals, Query, Select, SelectType, WhereInput, WithQuery,
+    PrismaClientInternals, Query, QueryConvert, Select, SelectType, WhereInput, WithQuery,
 };
 
 pub struct Delete<'a, Actions: ModelActions> {
@@ -68,10 +68,16 @@ impl<'a, Actions: ModelActions> Delete<'a, Actions> {
     }
 }
 
-impl<'a, Actions: ModelActions> Query<'a> for Delete<'a, Actions> {
+impl<'a, Actions: ModelActions> QueryConvert for Delete<'a, Actions> {
     type RawType = Actions::Data;
     type ReturnValue = Actions::Data;
 
+    fn convert(raw: Self::RawType) -> Self::ReturnValue {
+        raw
+    }
+}
+
+impl<'a, Actions: ModelActions> Query<'a> for Delete<'a, Actions> {
     fn graphql(self) -> (Operation, &'a PrismaClientInternals) {
         let mut scalar_selections = Actions::scalar_selections();
 
@@ -82,10 +88,6 @@ impl<'a, Actions: ModelActions> Query<'a> for Delete<'a, Actions> {
             self.client,
         )
     }
-
-    fn convert(raw: Self::RawType) -> Self::ReturnValue {
-        raw
-    }
 }
 
 impl<'a, Actions: ModelActions> ModelQuery<'a> for Delete<'a, Actions> {
diff --git a/src/queries/delete_many.rs b/src/queries/delete_many.rs
index ab7cf9ec..e4864dd7 100644
--- a/src/queries/delete_many.rs
+++ b/src/queries/delete_many.rs
@@ -2,7 +2,7 @@ use query_core::Operation;
 
 use crate::{
     merge_fields, BatchResult, ModelActions, ModelOperation, ModelQuery, ModelWriteOperation,
-    PrismaClientInternals, Query, WhereInput, WhereQuery,
+    PrismaClientInternals, Query, QueryConvert, WhereInput, WhereQuery,
 };
 use prisma_models::PrismaValue;
 
@@ -28,10 +28,16 @@ impl<'a, Actions: ModelActions> DeleteMany<'a, Actions> {
     }
 }
 
-impl<'a, Actions: ModelActions> Query<'a> for DeleteMany<'a, Actions> {
+impl<'a, Actions: ModelActions> QueryConvert for DeleteMany<'a, Actions> {
     type RawType = BatchResult;
     type ReturnValue = i64;
 
+    fn convert(raw: Self::RawType) -> Self::ReturnValue {
+        Self::convert(raw)
+    }
+}
+
+impl<'a, Actions: ModelActions> Query<'a> for DeleteMany<'a, Actions> {
     fn graphql(self) -> (Operation, &'a PrismaClientInternals) {
         (
             Operation::Write(Self::base_selection(
@@ -55,10 +61,6 @@ impl<'a, Actions: ModelActions> Query<'a> for DeleteMany<'a, Actions> {
             self.client,
         )
     }
-
-    fn convert(raw: Self::RawType) -> Self::ReturnValue {
-        Self::convert(raw)
-    }
 }
 
 impl<'a, Actions: ModelActions> ModelQuery<'a> for DeleteMany<'a, Actions> {
diff --git a/src/queries/execute_raw.rs b/src/queries/execute_raw.rs
index cf3bff6e..dfec9e36 100644
--- a/src/queries/execute_raw.rs
+++ b/src/queries/execute_raw.rs
@@ -2,7 +2,7 @@ use prisma_models::PrismaValue;
 use query_core::{Operation, Selection};
 use serde_json::Value;
 
-use crate::{raw::Raw, PrismaClientInternals, Query};
+use crate::{raw::Raw, PrismaClientInternals, Query, QueryConvert};
 
 pub struct ExecuteRaw<'a> {
     client: &'a PrismaClientInternals,
@@ -26,10 +26,16 @@ impl<'a> ExecuteRaw<'a> {
     }
 }
 
-impl<'a> Query<'a> for ExecuteRaw<'a> {
+impl<'a> QueryConvert for ExecuteRaw<'a> {
     type RawType = i64;
     type ReturnValue = Self::RawType;
 
+    fn convert(raw: Self::RawType) -> Self::ReturnValue {
+        raw
+    }
+}
+
+impl<'a> Query<'a> for ExecuteRaw<'a> {
     fn graphql(self) -> (Operation, &'a PrismaClientInternals) {
         (
             Operation::Write(Selection::new(
@@ -47,8 +53,4 @@ impl<'a> Query<'a> for ExecuteRaw<'a> {
             self.client,
         )
     }
-
-    fn convert(raw: Self::RawType) -> Self::ReturnValue {
-        raw
-    }
 }
diff --git a/src/queries/find_first.rs b/src/queries/find_first.rs
index 63ef28c2..c09cd0e5 100644
--- a/src/queries/find_first.rs
+++ b/src/queries/find_first.rs
@@ -3,8 +3,8 @@ use query_core::{Operation, QueryValue, Selection};
 
 use crate::{
     merge_fields, Include, IncludeType, ModelActions, ModelOperation, ModelQuery,
-    ModelReadOperation, OrderByQuery, PaginatedQuery, PrismaClientInternals, Query, Select,
-    SelectType, WhereInput, WhereQuery, WithQuery,
+    ModelReadOperation, OrderByQuery, PaginatedQuery, PrismaClientInternals, Query, QueryConvert,
+    Select, SelectType, WhereInput, WhereQuery, WithQuery,
 };
 
 use super::SerializedWhereInput;
@@ -155,10 +155,16 @@ impl<'a, Actions: ModelActions> FindFirst<'a, Actions> {
     }
 }
 
-impl<'a, Actions: ModelActions> Query<'a> for FindFirst<'a, Actions> {
+impl<'a, Actions: ModelActions> QueryConvert for FindFirst<'a, Actions> {
     type RawType = Option<Actions::Data>;
     type ReturnValue = Self::RawType;
 
+    fn convert(raw: Self::RawType) -> Self::ReturnValue {
+        raw
+    }
+}
+
+impl<'a, Actions: ModelActions> Query<'a> for FindFirst<'a, Actions> {
     fn graphql(self) -> (Operation, &'a PrismaClientInternals) {
         let mut scalar_selections = Actions::scalar_selections();
 
@@ -176,10 +182,6 @@ impl<'a, Actions: ModelActions> Query<'a> for FindFirst<'a, Actions> {
             self.client,
         )
     }
-
-    fn convert(raw: Self::RawType) -> Self::ReturnValue {
-        raw
-    }
 }
 
 impl<'a, Actions: ModelActions> ModelQuery<'a> for FindFirst<'a, Actions> {
diff --git a/src/queries/find_many.rs b/src/queries/find_many.rs
index b3af7584..9467f316 100644
--- a/src/queries/find_many.rs
+++ b/src/queries/find_many.rs
@@ -3,8 +3,8 @@ use query_core::{Operation, QueryValue, Selection};
 
 use crate::{
     merge_fields, Include, IncludeType, ModelActions, ModelOperation, ModelQuery,
-    ModelReadOperation, OrderByQuery, PaginatedQuery, PrismaClientInternals, Query, Select,
-    SelectType, WhereInput, WhereQuery, WithQuery,
+    ModelReadOperation, OrderByQuery, PaginatedQuery, PrismaClientInternals, Query, QueryConvert,
+    Select, SelectType, WhereInput, WhereQuery, WithQuery,
 };
 
 use super::SerializedWhereInput;
@@ -155,10 +155,16 @@ impl<'a, Actions: ModelActions> FindMany<'a, Actions> {
     }
 }
 
-impl<'a, Actions: ModelActions> Query<'a> for FindMany<'a, Actions> {
+impl<'a, Actions: ModelActions> QueryConvert for FindMany<'a, Actions> {
     type RawType = Vec<Actions::Data>;
     type ReturnValue = Self::RawType;
 
+    fn convert(raw: Self::RawType) -> Self::ReturnValue {
+        raw
+    }
+}
+
+impl<'a, Actions: ModelActions> Query<'a> for FindMany<'a, Actions> {
     fn graphql(self) -> (Operation, &'a PrismaClientInternals) {
         let mut scalar_selections = Actions::scalar_selections();
 
@@ -176,10 +182,6 @@ impl<'a, Actions: ModelActions> Query<'a> for FindMany<'a, Actions> {
             self.client,
         )
     }
-
-    fn convert(raw: Self::RawType) -> Self::ReturnValue {
-        raw
-    }
 }
 
 impl<'a, Actions: ModelActions> ModelQuery<'a> for FindMany<'a, Actions> {
diff --git a/src/queries/find_unique.rs b/src/queries/find_unique.rs
index 5eea2192..92c1a9ad 100644
--- a/src/queries/find_unique.rs
+++ b/src/queries/find_unique.rs
@@ -5,7 +5,7 @@ use query_core::{Operation, Selection};
 
 use crate::{
     Include, IncludeType, ModelActions, ModelOperation, ModelQuery, ModelReadOperation,
-    PrismaClientInternals, Query, Select, SelectType, WhereInput, WithQuery,
+    PrismaClientInternals, Query, QueryConvert, Select, SelectType, WhereInput, WithQuery,
 };
 
 pub struct FindUnique<'a, Actions: ModelActions> {
@@ -71,10 +71,16 @@ impl<'a, Actions: ModelActions> FindUnique<'a, Actions> {
     }
 }
 
-impl<'a, Actions: ModelActions> Query<'a> for FindUnique<'a, Actions> {
+impl<'a, Actions: ModelActions> QueryConvert for FindUnique<'a, Actions> {
     type RawType = Option<Actions::Data>;
     type ReturnValue = Self::RawType;
 
+    fn convert(raw: Self::RawType) -> Self::ReturnValue {
+        raw
+    }
+}
+
+impl<'a, Actions: ModelActions> Query<'a> for FindUnique<'a, Actions> {
     fn graphql(self) -> (Operation, &'a PrismaClientInternals) {
         let mut scalar_selections = Actions::scalar_selections();
 
@@ -85,10 +91,6 @@ impl<'a, Actions: ModelActions> Query<'a> for FindUnique<'a, Actions> {
             self.client,
         )
     }
-
-    fn convert(raw: Self::RawType) -> Self::ReturnValue {
-        raw
-    }
 }
 
 impl<'a, Actions: ModelActions> ModelQuery<'a> for FindUnique<'a, Actions> {
diff --git a/src/queries/include.rs b/src/queries/include.rs
index c2476b3c..f0c1a5e1 100644
--- a/src/queries/include.rs
+++ b/src/queries/include.rs
@@ -1,7 +1,7 @@
 use query_core::{Operation, Selection};
 use std::marker::PhantomData;
 
-use crate::{PrismaClientInternals, Query};
+use crate::{PrismaClientInternals, Query, QueryConvert};
 
 use super::query;
 
@@ -33,15 +33,17 @@ impl<'a, Data: query::Data> Include<'a, Data> {
     }
 }
 
-impl<'a, Data: query::Data> Query<'a> for Include<'a, Data> {
+impl<'a, Data: query::Data> QueryConvert for Include<'a, Data> {
     type RawType = Data;
     type ReturnValue = Self::RawType;
 
-    fn graphql(self) -> (Operation, &'a PrismaClientInternals) {
-        (self.operation, self.client)
-    }
-
     fn convert(raw: Self::RawType) -> Self::ReturnValue {
         raw
     }
 }
+
+impl<'a, Data: query::Data> Query<'a> for Include<'a, Data> {
+    fn graphql(self) -> (Operation, &'a PrismaClientInternals) {
+        (self.operation, self.client)
+    }
+}
diff --git a/src/queries/mod.rs b/src/queries/mod.rs
index eac84922..c8fb53c8 100644
--- a/src/queries/mod.rs
+++ b/src/queries/mod.rs
@@ -36,7 +36,9 @@ pub use update::*;
 pub use update_many::*;
 pub use upsert::*;
 
+use futures::FutureExt;
 pub use query_core::{schema::QuerySchemaRef, Operation, Selection};
+use std::future::Future;
 
 pub enum SerializedWhereValue {
     Object(Vec<(String, prisma_models::PrismaValue)>),
@@ -98,11 +100,9 @@ impl Into<(String, prisma_models::PrismaValue)> for SerializedWhereInput {
     }
 }
 
-pub(crate) async fn exec<'a, Q: Query<'a>>(
+pub fn exec<'a, Q: Query<'a> + 'a>(
     query: Q,
-) -> error::Result<<Q as Query<'a>>::ReturnValue> {
+) -> impl Future<Output = Result<<Q as QueryConvert>::ReturnValue>> + 'a {
     let (op, client) = query.graphql();
-    let res = client.execute(op).await;
-
-    res.map(Q::convert)
+    client.execute(op).map(|res| res.map(Q::convert))
 }
diff --git a/src/queries/query.rs b/src/queries/query.rs
index cc985c9d..93b191f2 100644
--- a/src/queries/query.rs
+++ b/src/queries/query.rs
@@ -4,17 +4,19 @@ use serde::de::DeserializeOwned;
 
 use crate::{PrismaClientInternals, WhereInput};
 
-pub trait Query<'a> {
-    type RawType: DeserializeOwned;
+pub trait QueryConvert {
+    type RawType: Data;
     type ReturnValue: 'static;
 
-    fn graphql(self) -> (Operation, &'a PrismaClientInternals);
-
     /// Function for converting between raw database data and the type expected by the user.
     /// Necessary for things like raw queries
     fn convert(raw: Self::RawType) -> Self::ReturnValue;
 }
 
+pub trait Query<'a>: QueryConvert {
+    fn graphql(self) -> (Operation, &'a PrismaClientInternals);
+}
+
 pub trait ModelActions {
     type Data: Data;
     type Where: WhereInput;
diff --git a/src/queries/query_raw.rs b/src/queries/query_raw.rs
index 0d5a4bfe..a0afb8fc 100644
--- a/src/queries/query_raw.rs
+++ b/src/queries/query_raw.rs
@@ -7,7 +7,7 @@ use serde_json::Value;
 
 use crate::{
     raw::{Raw, RawOperationData, RawPrismaValue},
-    PrismaClientInternals, Query,
+    PrismaClientInternals, Query, QueryConvert,
 };
 
 pub struct QueryRaw<'a, Data>
@@ -60,13 +60,22 @@ where
     }
 }
 
-impl<'a, Data> Query<'a> for QueryRaw<'a, Data>
+impl<'a, Data> QueryConvert for QueryRaw<'a, Data>
 where
     Data: DeserializeOwned + 'static,
 {
     type RawType = RawOperationData;
     type ReturnValue = Vec<Data>;
 
+    fn convert(raw: Self::RawType) -> Self::ReturnValue {
+        Self::convert(raw).unwrap()
+    }
+}
+
+impl<'a, Data> Query<'a> for QueryRaw<'a, Data>
+where
+    Data: DeserializeOwned + 'static,
+{
     fn graphql(self) -> (Operation, &'a PrismaClientInternals) {
         (
             Operation::Write(Selection::new(
@@ -84,8 +93,4 @@ where
             self.client,
         )
     }
-
-    fn convert(raw: Self::RawType) -> Self::ReturnValue {
-        Self::convert(raw).unwrap()
-    }
 }
diff --git a/src/queries/select.rs b/src/queries/select.rs
index 0fca8a15..e5573d4c 100644
--- a/src/queries/select.rs
+++ b/src/queries/select.rs
@@ -3,7 +3,7 @@ use std::marker::PhantomData;
 use query_core::{Operation, Selection};
 use serde::de::DeserializeOwned;
 
-use crate::{PrismaClientInternals, Query};
+use crate::{PrismaClientInternals, Query, QueryConvert};
 
 pub trait SelectType {
     // TODO: ModelActions
@@ -33,15 +33,17 @@ impl<'a, Data: DeserializeOwned> Select<'a, Data> {
     }
 }
 
-impl<'a, Data: DeserializeOwned + 'static> Query<'a> for Select<'a, Data> {
+impl<'a, Data: DeserializeOwned + 'static> QueryConvert for Select<'a, Data> {
     type RawType = Data;
     type ReturnValue = Self::RawType;
 
-    fn graphql(self) -> (Operation, &'a PrismaClientInternals) {
-        (self.operation, self.client)
-    }
-
     fn convert(raw: Self::RawType) -> Self::ReturnValue {
         raw
     }
 }
+
+impl<'a, Data: DeserializeOwned + 'static> Query<'a> for Select<'a, Data> {
+    fn graphql(self) -> (Operation, &'a PrismaClientInternals) {
+        (self.operation, self.client)
+    }
+}
diff --git a/src/queries/update.rs b/src/queries/update.rs
index df6dcda2..b02d11fd 100644
--- a/src/queries/update.rs
+++ b/src/queries/update.rs
@@ -3,8 +3,8 @@ use query_core::{Operation, Selection};
 
 use crate::{
     merge_fields, Include, IncludeType, ModelActions, ModelOperation, ModelQuery,
-    ModelWriteOperation, PrismaClientInternals, Query, Select, SelectType, SetQuery, WhereInput,
-    WithQuery,
+    ModelWriteOperation, PrismaClientInternals, Query, QueryConvert, Select, SelectType, SetQuery,
+    WhereInput, WithQuery,
 };
 
 pub struct Update<'a, Actions: ModelActions> {
@@ -90,10 +90,16 @@ impl<'a, Actions: ModelActions> Update<'a, Actions> {
     }
 }
 
-impl<'a, Actions: ModelActions> Query<'a> for Update<'a, Actions> {
+impl<'a, Actions: ModelActions> QueryConvert for Update<'a, Actions> {
     type RawType = Actions::Data;
     type ReturnValue = Self::RawType;
 
+    fn convert(raw: Self::RawType) -> Self::ReturnValue {
+        raw
+    }
+}
+
+impl<'a, Actions: ModelActions> Query<'a> for Update<'a, Actions> {
     fn graphql(self) -> (Operation, &'a PrismaClientInternals) {
         let mut scalar_selections = Actions::scalar_selections();
 
@@ -108,10 +114,6 @@ impl<'a, Actions: ModelActions> Query<'a> for Update<'a, Actions> {
             self.client,
         )
     }
-
-    fn convert(raw: Self::RawType) -> Self::ReturnValue {
-        raw
-    }
 }
 
 impl<'a, Actions: ModelActions> ModelQuery<'a> for Update<'a, Actions> {
diff --git a/src/queries/update_many.rs b/src/queries/update_many.rs
index 4ce7ce1f..e376456d 100644
--- a/src/queries/update_many.rs
+++ b/src/queries/update_many.rs
@@ -3,7 +3,7 @@ use query_core::Operation;
 
 use crate::{
     merge_fields, BatchResult, ModelActions, ModelOperation, ModelQuery, ModelWriteOperation,
-    PrismaClientInternals, Query, SetQuery, WhereInput, WhereQuery,
+    PrismaClientInternals, Query, QueryConvert, SetQuery, WhereInput, WhereQuery,
 };
 
 pub struct UpdateMany<'a, Actions: ModelActions> {
@@ -30,10 +30,16 @@ impl<'a, Actions: ModelActions> UpdateMany<'a, Actions> {
     }
 }
 
-impl<'a, Actions: ModelActions> Query<'a> for UpdateMany<'a, Actions> {
+impl<'a, Actions: ModelActions> QueryConvert for UpdateMany<'a, Actions> {
     type RawType = BatchResult;
     type ReturnValue = i64;
 
+    fn convert(raw: Self::RawType) -> Self::ReturnValue {
+        raw.count
+    }
+}
+
+impl<'a, Actions: ModelActions> Query<'a> for UpdateMany<'a, Actions> {
     fn graphql(self) -> (Operation, &'a PrismaClientInternals) {
         (
             Operation::Write(Self::base_selection(
@@ -66,10 +72,6 @@ impl<'a, Actions: ModelActions> Query<'a> for UpdateMany<'a, Actions> {
             self.client,
         )
     }
-
-    fn convert(raw: Self::RawType) -> Self::ReturnValue {
-        raw.count
-    }
 }
 
 impl<'a, Actions: ModelActions> ModelQuery<'a> for UpdateMany<'a, Actions> {
diff --git a/src/queries/upsert.rs b/src/queries/upsert.rs
index c7fd33a6..8d2d4346 100644
--- a/src/queries/upsert.rs
+++ b/src/queries/upsert.rs
@@ -3,7 +3,7 @@ use query_core::{Operation, Selection};
 
 use crate::{
     Include, IncludeType, ModelActions, ModelOperation, ModelQuery, ModelWriteOperation,
-    PrismaClientInternals, Query, Select, SelectType, WhereInput, WithQuery,
+    PrismaClientInternals, Query, QueryConvert, Select, SelectType, WhereInput, WithQuery,
 };
 
 pub struct Upsert<'a, Actions: ModelActions> {
@@ -95,10 +95,16 @@ impl<'a, Actions: ModelActions> Upsert<'a, Actions> {
     }
 }
 
-impl<'a, Actions: ModelActions> Query<'a> for Upsert<'a, Actions> {
+impl<'a, Actions: ModelActions> QueryConvert for Upsert<'a, Actions> {
     type RawType = Actions::Data;
     type ReturnValue = Self::RawType;
 
+    fn convert(raw: Self::RawType) -> Self::ReturnValue {
+        raw
+    }
+}
+
+impl<'a, Actions: ModelActions> Query<'a> for Upsert<'a, Actions> {
     fn graphql(self) -> (Operation, &'a PrismaClientInternals) {
         let mut scalar_selections = Actions::scalar_selections();
 
@@ -114,10 +120,6 @@ impl<'a, Actions: ModelActions> Query<'a> for Upsert<'a, Actions> {
             self.client,
         )
     }
-
-    fn convert(raw: Self::RawType) -> Self::ReturnValue {
-        raw
-    }
 }
 
 impl<'a, Actions: ModelActions> ModelQuery<'a> for Upsert<'a, Actions> {
diff --git a/src/transaction.rs b/src/transaction.rs
new file mode 100644
index 00000000..5dd19bb7
--- /dev/null
+++ b/src/transaction.rs
@@ -0,0 +1,142 @@
+use std::{future::Future, marker::PhantomData};
+
+use query_core::TxId;
+
+use crate::{ExecutionEngine, PrismaClient, PrismaClientInternals, QueryError};
+
+pub struct TransactionBuilder<'a, TClient> {
+    client: &'a TClient,
+    internals: &'a PrismaClientInternals,
+    timeout: u64,
+    max_wait: u64,
+    isolation_level: Option<String>,
+}
+
+impl<'a, TClient: PrismaClient> TransactionBuilder<'a, TClient> {
+    pub fn _new(client: &'a TClient, internals: &'a PrismaClientInternals) -> Self {
+        Self {
+            client,
+            internals,
+            timeout: 5000,
+            max_wait: 2000,
+            isolation_level: None,
+        }
+    }
+
+    pub fn with_timeout(self, timeout: u64) -> Self {
+        Self { timeout, ..self }
+    }
+
+    pub fn with_max_wait(self, max_wait: u64) -> Self {
+        Self { max_wait, ..self }
+    }
+
+    pub fn with_isolation_level(self, isolation_level: impl TransactionIsolationLevel) -> Self {
+        Self {
+            isolation_level: Some(isolation_level.to_string()),
+            ..self
+        }
+    }
+
+    pub async fn run<TErr, TRet, TFut, TFn>(self, tx: TFn) -> Result<TRet, TErr>
+    where
+        TFut: Future<Output = Result<TRet, TErr>>,
+        TFn: FnOnce(TClient) -> TFut,
+        TErr: From<crate::QueryError>,
+    {
+        match &self.internals.engine {
+            ExecutionEngine::Real { connector, .. } => {
+                let new_tx_id = connector
+                    .executor
+                    .start_tx(
+                        connector.query_schema.clone(),
+                        self.max_wait,
+                        self.timeout,
+                        self.isolation_level,
+                    )
+                    .await
+                    .map_err(|e| QueryError::Execute(e.into()))?;
+
+                match tx(self.client.with_tx_id(Some(new_tx_id.clone()))).await {
+                    result @ Ok(_) => {
+                        connector
+                            .executor
+                            .commit_tx(new_tx_id)
+                            .await
+                            .map_err(|e| QueryError::Execute(e.into()))?;
+
+                        result
+                    }
+                    err @ Err(_) => {
+                        connector.executor.rollback_tx(new_tx_id).await.ok();
+
+                        err
+                    }
+                }
+            }
+            _ => tx(self.client.with_tx_id(None)).await,
+        }
+    }
+
+    pub async fn begin(self) -> super::Result<(TransactionController<TClient>, TClient)> {
+        Ok(match &self.internals.engine {
+            ExecutionEngine::Real { connector, .. } => {
+                let new_tx_id = connector
+                    .executor
+                    .start_tx(
+                        connector.query_schema.clone(),
+                        self.max_wait,
+                        self.timeout,
+                        self.isolation_level,
+                    )
+                    .await
+                    .map_err(|e| QueryError::Execute(e.into()))?;
+
+                (
+                    TransactionController::new(new_tx_id.clone()),
+                    self.client.with_tx_id(Some(new_tx_id.clone())),
+                )
+            }
+            _ => (
+                TransactionController::new("".to_string().into()),
+                self.client.with_tx_id(None),
+            ),
+        })
+    }
+}
+
+pub struct TransactionController<TClient> {
+    tx_id: TxId,
+    _client: PhantomData<TClient>,
+}
+
+impl<TClient: PrismaClient> TransactionController<TClient> {
+    fn new(tx_id: TxId) -> Self {
+        Self {
+            tx_id,
+            _client: Default::default(),
+        }
+    }
+
+    pub async fn commit(self, client: TClient) -> super::Result<()> {
+        Ok(match &client.internals().engine {
+            ExecutionEngine::Real { connector, .. } => connector
+                .executor
+                .commit_tx(self.tx_id)
+                .await
+                .map_err(|e| QueryError::Execute(e.into()))?,
+            _ => {}
+        })
+    }
+
+    pub async fn rollback(self, client: TClient) -> super::Result<()> {
+        Ok(match &client.internals().engine {
+            ExecutionEngine::Real { connector, .. } => {
+                connector.executor.rollback_tx(self.tx_id).await.ok();
+            }
+            _ => {}
+        })
+    }
+}
+
+pub trait TransactionIsolationLevel: ToString {}


Test patch:
diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml
index ee4098f4..3ae7d382 100644
--- a/integration-tests/Cargo.toml
+++ b/integration-tests/Cargo.toml
@@ -22,4 +22,6 @@ prisma-client-rust = { path = "..", features = [
   # "mutation-callbacks",
   "mocking",
 ], default-features = false }
-rspc = { version = "0.1.2" }
+async-trait = "0.1.60"
+thiserror = "1.0.37"
+rspc = { version = "0.1.2" }
\ No newline at end of file
diff --git a/integration-tests/tests/batch.rs b/integration-tests/tests/batch.rs
index 047daa0f..d44f0b9e 100644
--- a/integration-tests/tests/batch.rs
+++ b/integration-tests/tests/batch.rs
@@ -328,3 +328,48 @@ async fn update_many() -> TestResult {
 
     cleanup(client).await
 }
+
+#[tokio::test]
+async fn async_trait() -> TestResult {
+    #[async_trait::async_trait]
+    trait TestTrait {
+        async fn test(&self, client: &PrismaClient) -> prisma_client_rust::Result<()>;
+    }
+
+    struct Test;
+
+    #[async_trait::async_trait]
+    impl TestTrait for Test {
+        async fn test(&self, client: &PrismaClient) -> prisma_client_rust::Result<()> {
+            let (brendan, oscar) = client
+                ._batch((
+                    vec![client.user().create("Brendan".to_string(), vec![])],
+                    vec![client.user().create("Oscar".to_string(), vec![])],
+                ))
+                .await?;
+
+            assert_eq!(&brendan[0].name, "Brendan");
+            assert_eq!(&oscar[0].name, "Oscar");
+
+            Ok(())
+        }
+    }
+
+    let client = client().await;
+
+    Test.test(&client).await?;
+
+    cleanup(client).await
+}
+
+#[tokio::test]
+async fn allows_empty() -> TestResult {
+    let client = client().await;
+
+    client._batch(vec![] as Vec<user::Create>).await?;
+    client
+        ._batch(vec![vec![]] as Vec<Vec<user::Create>>)
+        .await?;
+
+    cleanup(client).await
+}
diff --git a/integration-tests/tests/callbacks.rs b/integration-tests/tests/callbacks.rs
index db3b20f7..1e23f461 100644
--- a/integration-tests/tests/callbacks.rs
+++ b/integration-tests/tests/callbacks.rs
@@ -1,6 +1,5 @@
-use prisma_client_rust::*;
-use std::sync::Arc;
-use std::sync::Mutex;
+use prisma_client_rust::{ModelMutationCallbackData, ModelMutationType};
+use std::sync::{Arc, Mutex};
 
 use crate::db::*;
 use crate::utils::*;
diff --git a/integration-tests/tests/lib.rs b/integration-tests/tests/lib.rs
index 2889456c..a144c14c 100644
--- a/integration-tests/tests/lib.rs
+++ b/integration-tests/tests/lib.rs
@@ -1,6 +1,7 @@
 use utils::{cleanup, TestResult};
 
 mod db;
+mod transaction;
 mod utils;
 
 #[tokio::test]
diff --git a/integration-tests/tests/transaction.rs b/integration-tests/tests/transaction.rs
new file mode 100644
index 00000000..b9539866
--- /dev/null
+++ b/integration-tests/tests/transaction.rs
@@ -0,0 +1,258 @@
+use std::time::Duration;
+
+use prisma_client_rust::QueryError;
+
+use crate::db::*;
+use crate::utils::*;
+
+// Closure
+
+#[tokio::test]
+async fn ok() -> TestResult {
+    let client = client().await;
+
+    let (user, post) = client
+        ._transaction()
+        .run(|client| async move {
+            let user = client
+                .user()
+                .create("brendan".to_string(), vec![])
+                .exec()
+                .await?;
+
+            client
+                .post()
+                .create(
+                    "test".to_string(),
+                    true,
+                    vec![post::author::connect(user::id::equals(user.id.clone()))],
+                )
+                .exec()
+                .await
+                .map(|post| (user, post))
+        })
+        .await?;
+
+    assert_eq!(&user.name, "brendan");
+    assert_eq!(&post.author_id.unwrap(), &user.id);
+
+    cleanup(client).await
+}
+
+#[tokio::test]
+async fn err() -> TestResult {
+    let client = client().await;
+
+    let result = client
+        ._transaction()
+        .run(|client| async move {
+            let user = client
+                .user()
+                .create("brendan".to_string(), vec![])
+                .exec()
+                .await?;
+
+            client
+                .post()
+                .create(
+                    "test".to_string(),
+                    true,
+                    vec![post::author::connect(user::id::equals("".to_string()))],
+                )
+                .exec()
+                .await
+                .map(|post| (user, post))
+        })
+        .await;
+
+    assert!(result.is_err());
+    assert!(client.user().find_many(vec![]).exec().await?.is_empty());
+    assert!(client.post().find_many(vec![]).exec().await?.is_empty());
+
+    cleanup(client).await
+}
+
+#[tokio::test]
+async fn custom_error() -> TestResult {
+    let client = client().await;
+
+    #[derive(Debug, thiserror::Error)]
+    enum TxError {
+        #[error("prisma error")]
+        Prisma(#[from] QueryError),
+        #[error("post not found")]
+        PostNotFound,
+    }
+
+    let result = client
+        ._transaction()
+        .run(|client| async move {
+            client
+                .post()
+                .create("test".to_string(), false, vec![])
+                .exec()
+                .await?;
+
+            client
+                .post()
+                .find_unique(post::id::equals("".to_string()))
+                .exec()
+                .await?
+                .ok_or(TxError::PostNotFound)
+        })
+        .await;
+
+    assert!(result.is_err());
+    assert!(client.post().find_many(vec![]).exec().await?.is_empty());
+
+    cleanup(client).await
+}
+
+#[tokio::test]
+async fn timeout() -> TestResult {
+    let client = client().await;
+
+    let result = client
+        ._transaction()
+        .with_timeout(2000)
+        .run(|client| async move {
+            for _ in 0..3 {
+                tokio::time::sleep(Duration::from_secs(1)).await;
+                client.user().find_many(vec![]).exec().await?;
+            }
+
+            client.user().find_many(vec![]).exec().await
+        })
+        .await;
+
+    assert!(result.is_err());
+
+    let result = client
+        ._transaction()
+        .with_timeout(8000)
+        .run(|client| async move {
+            for _ in 0..3 {
+                tokio::time::sleep(Duration::from_secs(1)).await;
+                client.user().find_many(vec![]).exec().await?;
+            }
+
+            client.user().find_many(vec![]).exec().await
+        })
+        .await;
+
+    assert!(result.is_ok());
+
+    cleanup(client).await
+}
+
+#[tokio::test]
+async fn batch() -> TestResult {
+    let client = client().await;
+
+    let users = client
+        ._transaction()
+        .run(|client| async move {
+            client
+                ._batch(vec![
+                    client.user().create("brendan".to_string(), vec![]),
+                    client.user().create("brendan".to_string(), vec![]),
+                ])
+                .await
+        })
+        .await?;
+
+    assert_eq!(&users[0].name, "brendan");
+
+    cleanup(client).await
+}
+
+// Imperative
+
+#[tokio::test]
+async fn commit() -> TestResult {
+    let client = client().await;
+
+    // separate function for nice error handling
+    async fn run(client: &PrismaClient) -> prisma_client_rust::Result<(user::Data, post::Data)> {
+        let user = client
+            .user()
+            .create("brendan".to_string(), vec![])
+            .exec()
+            .await?;
+
+        let post = client
+            .post()
+            .create(
+                "test".to_string(),
+                true,
+                vec![post::author::connect(user::id::equals(user.id.clone()))],
+            )
+            .exec()
+            .await?;
+
+        Ok((user, post))
+    }
+
+    // block to avoid shadowing `client`
+    let (user, post) = {
+        let (tx, client) = client._transaction().begin().await?;
+
+        match run(&client).await {
+            Ok(v) => tx.commit(client).await.map(|_| v),
+            Err(e) => {
+                tx.rollback(client).await?;
+                Err(e)
+            }
+        }?
+    };
+
+    assert_eq!(&user.name, "brendan");
+    assert_eq!(&post.author_id.unwrap(), &user.id);
+
+    cleanup(client).await
+}
+
+#[tokio::test]
+async fn rollback() -> TestResult {
+    let client = client().await;
+
+    // separate function for nice error handling
+    async fn run(client: &PrismaClient) -> prisma_client_rust::Result<(user::Data, post::Data)> {
+        let user = client
+            .user()
+            .create("brendan".to_string(), vec![])
+            .exec()
+            .await?;
+
+        let post = client
+            .post()
+            .create(
+                "test".to_string(),
+                true,
+                vec![post::author::connect(user::id::equals("".to_string()))],
+            )
+            .exec()
+            .await?;
+
+        Ok((user, post))
+    }
+
+    // block to avoid shadowing `client`
+    let result = {
+        let (tx, client) = client._transaction().begin().await?;
+
+        match run(&client).await {
+            Ok(v) => tx.commit(client).await.map(|_| v),
+            Err(e) => {
+                tx.rollback(client).await?;
+                Err(e)
+            }
+        }
+    };
+
+    assert!(result.is_err());
+    assert!(client.user().find_many(vec![]).exec().await?.is_empty());
+    assert!(client.post().find_many(vec![]).exec().await?.is_empty());
+
+    cleanup(client).await
+}