From baf05ae9abda00148b8420ed1c0f2c391feac9a8 Mon Sep 17 00:00:00 2001 From: Alexei Pastuchov Date: Wed, 22 Apr 2020 11:31:49 +0200 Subject: [PATCH 01/19] Add mysql feature flags Add corresponding feature flags to the wundergraph_derive, wundergraph and wundergraph_cli crate --- wundergraph/Cargo.toml | 1 + wundergraph_cli/Cargo.toml | 1 + wundergraph_derive/Cargo.toml | 1 + 3 files changed, 3 insertions(+) diff --git a/wundergraph/Cargo.toml b/wundergraph/Cargo.toml index b0f80b3..92a0fbf 100644 --- a/wundergraph/Cargo.toml +++ b/wundergraph/Cargo.toml @@ -39,6 +39,7 @@ insta = "0.12" [features] default = [] debug = ["wundergraph_derive/debug", "log"] +mysql = ["diesel/mysql", "wundergraph_derive/mysql"] sqlite = ["diesel/sqlite", "wundergraph_derive/sqlite"] postgres = ["diesel/postgres", "wundergraph_derive/postgres"] extras = ["uuid", "chrono"] diff --git a/wundergraph_cli/Cargo.toml b/wundergraph_cli/Cargo.toml index bc96a56..1ea9f14 100644 --- a/wundergraph_cli/Cargo.toml +++ b/wundergraph_cli/Cargo.toml @@ -24,5 +24,6 @@ serde_json = "1" [features] default = ["postgres"] +mysql = ["diesel/mysql"] sqlite = ["diesel/sqlite"] postgres = ["diesel/postgres"] diff --git a/wundergraph_derive/Cargo.toml b/wundergraph_derive/Cargo.toml index 08dc410..915c714 100644 --- a/wundergraph_derive/Cargo.toml +++ b/wundergraph_derive/Cargo.toml @@ -21,6 +21,7 @@ proc-macro = true [features] default = [] nightly = ["proc-macro2/nightly"] +mysql = [] postgres = [] sqlite = [] debug = [] From 702ddad7a681f60c52b26a8d9b65aad55a491908 Mon Sep 17 00:00:00 2001 From: Alexei Pastuchov Date: Wed, 22 Apr 2020 11:40:19 +0200 Subject: [PATCH 02/19] Generate an impl for LoadingHandler for diesel::mysql::Mysql --- wundergraph_derive/src/wundergraph_entity.rs | 23 ++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/wundergraph_derive/src/wundergraph_entity.rs b/wundergraph_derive/src/wundergraph_entity.rs index 759274a..0e48b92 100644 --- a/wundergraph_derive/src/wundergraph_entity.rs +++ b/wundergraph_derive/src/wundergraph_entity.rs @@ -29,6 +29,16 @@ pub fn derive(item: &syn::DeriveInput) -> Result { None }; + let mysql_loading_handler = if cfg!(feature = "mysql") { + Some(derive_loading_handler( + &model, + item, + "e!(diesel::mysql::Mysql), + )?) + } else { + None + }; + let pg_non_table_field_filter = if cfg!(feature = "postgres") { Some(derive_non_table_filter( &model, @@ -49,6 +59,17 @@ pub fn derive(item: &syn::DeriveInput) -> Result { None }; + + let mysql_non_table_field_filter = if cfg!(feature = "mysql") { + Some(derive_non_table_filter( + &model, + item, + "e!(diesel::mysql::Mysql), + )?) + } else { + None + }; + let belongs_to = crate::belonging_to::derive_belonging_to(&model, item)?; Ok(wrap_in_dummy_mod( @@ -61,8 +82,10 @@ pub fn derive(item: &syn::DeriveInput) -> Result { #pg_loading_handler #sqlite_loading_handler + #mysql_loading_handler #pg_non_table_field_filter #sqlite_non_table_field_filter + #mysql_non_table_field_filter #(#belongs_to)* }, From 778fecbf9ae491682643cc263eeb4d1bbec11bc5 Mon Sep 17 00:00:00 2001 From: Alexei Pastuchov Date: Wed, 22 Apr 2020 11:42:13 +0200 Subject: [PATCH 03/19] Generate an impl for WundergraphBelongsTo for diesel::mysql::Mysql --- wundergraph_derive/src/belonging_to.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/wundergraph_derive/src/belonging_to.rs b/wundergraph_derive/src/belonging_to.rs index 0120433..5420d94 100644 --- a/wundergraph_derive/src/belonging_to.rs +++ b/wundergraph_derive/src/belonging_to.rs @@ -70,9 +70,22 @@ pub fn derive_belonging_to( } else { None }; + let mysql = if cfg!(feature = "mysql") { + Some(derive_belongs_to( + model, + item, + parent_ty, + &key_ty, + f.sql_name(), + "e!(diesel::mysql::Mysql), + )?) + } else { + None + }; Ok(quote! { #pg #sqlite + #mysql }) }) .collect::, _>>() From 6f7615285667ed8c7d27a8ad5b5cf117ac00b177 Mon Sep 17 00:00:00 2001 From: Alexei Pastuchov Date: Wed, 22 Apr 2020 11:50:28 +0200 Subject: [PATCH 04/19] Generate an impl for BuildFilterHelper for diesel::mysql::Mysql --- wundergraph_derive/src/build_filter_helper.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/wundergraph_derive/src/build_filter_helper.rs b/wundergraph_derive/src/build_filter_helper.rs index e91653f..95cff3a 100644 --- a/wundergraph_derive/src/build_filter_helper.rs +++ b/wundergraph_derive/src/build_filter_helper.rs @@ -28,12 +28,23 @@ pub fn derive(item: &syn::DeriveInput) -> Result { None }; + let mysql = if cfg!(feature = "mysql") { + Some(derive_non_table_filter( + &model, + item, + "e!(diesel::mysql::Mysql), + )?) + } else { + None + }; + Ok(wrap_in_dummy_mod( "build_filter_helper", &model.name, "e! { #pg #sqlite + #mysql }, )) } From 61641d2b85620b3bcf7c1412bb0d04d1100d918e Mon Sep 17 00:00:00 2001 From: Alexei Pastuchov Date: Sat, 2 May 2020 08:07:38 +0200 Subject: [PATCH 05/19] Implement ApplyOffset for diesel::mysql::Mysq --- .../src/query_builder/selection/offset.rs | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/wundergraph/src/query_builder/selection/offset.rs b/wundergraph/src/query_builder/selection/offset.rs index 0fb0760..39f45d1 100644 --- a/wundergraph/src/query_builder/selection/offset.rs +++ b/wundergraph/src/query_builder/selection/offset.rs @@ -1,15 +1,16 @@ use crate::error::Result; -#[cfg(any(feature = "postgres", feature = "sqlite"))] +#[cfg(any(feature = "postgres", feature = "sqlite", feature = "mysql"))] use crate::error::WundergraphError; -#[cfg(any(feature = "postgres", feature = "sqlite"))] +#[cfg(any(feature = "postgres", feature = "sqlite", feature = "mysql"))] use crate::juniper_ext::FromLookAheadValue; use crate::query_builder::selection::{BoxedQuery, LoadingHandler}; use crate::scalar::WundergraphScalarValue; use diesel::backend::Backend; #[cfg(feature = "sqlite")] use diesel::query_dsl::methods::LimitDsl; -#[cfg(any(feature = "postgres", feature = "sqlite"))] +#[cfg(any(feature = "postgres", feature = "sqlite", feature = "mysql"))] use diesel::query_dsl::methods::OffsetDsl; + use juniper::LookAheadSelection; /// A trait abstracting over the different behaviour of limit/offset @@ -72,3 +73,25 @@ impl ApplyOffset for diesel::sqlite::Sqlite { } } } + +#[cfg(feature = "mysql")] +impl ApplyOffset for diesel::mysql::Mysql { + fn apply_offset<'a, L, Ctx>( + query: BoxedQuery<'a, L, Self, Ctx>, + select: &LookAheadSelection<'_, WundergraphScalarValue>, + ) -> Result> + where + L: LoadingHandler, + { + use juniper::LookAheadMethods; + if let Some(offset) = select.argument("offset") { + Ok(<_ as OffsetDsl>::offset( + query, + i64::from_look_ahead(offset.value()) + .ok_or(WundergraphError::CouldNotBuildFilterArgument)?, + )) + } else { + Ok(query) + } + } +} \ No newline at end of file From 0da28149026d614fde484d8c4eeee2264284694c Mon Sep 17 00:00:00 2001 From: Alexei Pastuchov Date: Sat, 2 May 2020 08:12:27 +0200 Subject: [PATCH 06/19] wundergraph_example supports mysql --- wundergraph_example/Cargo.toml | 3 +- .../mysql/2018-01-24-131925_setup/down.sql | 7 +++ .../mysql/2018-01-24-131925_setup/up.sql | 52 +++++++++++++++++++ wundergraph_example/src/bin/main.rs | 2 + wundergraph_example/src/lib.rs | 3 ++ 5 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 wundergraph_example/migrations/mysql/2018-01-24-131925_setup/down.sql create mode 100644 wundergraph_example/migrations/mysql/2018-01-24-131925_setup/up.sql diff --git a/wundergraph_example/Cargo.toml b/wundergraph_example/Cargo.toml index f21fa61..afa9e82 100644 --- a/wundergraph_example/Cargo.toml +++ b/wundergraph_example/Cargo.toml @@ -6,7 +6,7 @@ license = "MIT OR Apache-2.0" publish = false repository = "https://github.com/weiznich/wundergraph" readme = "../README.md" -keywords = ["GraphQL", "ORM", "PostgreSQL", "SQLite"] +keywords = ["GraphQL", "ORM", "PostgreSQL", "SQLite", "Mysql"] categories = ["database", "web-programming"] description = "A GraphQL ORM build on top of diesel" edition = "2018" @@ -31,3 +31,4 @@ default-features = false default = ["postgres", "wundergraph/debug"] sqlite = ["wundergraph/sqlite", "diesel/sqlite"] postgres = ["wundergraph/postgres", "diesel/postgres"] +mysql = ["wundergraph/mysql", "diesel/mysql"] diff --git a/wundergraph_example/migrations/mysql/2018-01-24-131925_setup/down.sql b/wundergraph_example/migrations/mysql/2018-01-24-131925_setup/down.sql new file mode 100644 index 0000000..c93586f --- /dev/null +++ b/wundergraph_example/migrations/mysql/2018-01-24-131925_setup/down.sql @@ -0,0 +1,7 @@ +-- This file should undo anything in `up.sql` + +DROP TABLE friends; +DROP TABLE appears_in; +DROP TABLE heros; +DROP TABLE home_worlds; +DROP TABLE species; diff --git a/wundergraph_example/migrations/mysql/2018-01-24-131925_setup/up.sql b/wundergraph_example/migrations/mysql/2018-01-24-131925_setup/up.sql new file mode 100644 index 0000000..098dbd2 --- /dev/null +++ b/wundergraph_example/migrations/mysql/2018-01-24-131925_setup/up.sql @@ -0,0 +1,52 @@ +CREATE TABLE species( + id INT AUTO_INCREMENT PRIMARY KEY, + name TEXT NOT NULL +); + +CREATE TABLE home_worlds( + id INT AUTO_INCREMENT PRIMARY KEY, + name TEXT NOT NULL +); + +CREATE TABLE heros( + id INT AUTO_INCREMENT PRIMARY KEY, + name TEXT NOT NULL, + hair_color TEXT, + species INTEGER NOT NULL REFERENCES species(id) ON DELETE CASCADE ON UPDATE RESTRICT, + home_world INTEGER REFERENCES home_worlds(id) ON DELETE CASCADE ON UPDATE RESTRICT +); + +CREATE TABLE appears_in( + hero_id INTEGER NOT NULL REFERENCES heros(id) ON DELETE CASCADE ON UPDATE RESTRICT, + episode SMALLINT NOT NULL CHECK(episode IN (1,2,3)), + PRIMARY KEY(hero_id, episode) +); + +CREATE TABLE friends( + hero_id INTEGER NOT NULL REFERENCES heros(id) ON DELETE CASCADE ON UPDATE RESTRICT, + friend_id INTEGER NOT NULL REFERENCES heros(id) ON DELETE CASCADE ON UPDATE RESTRICT, + PRIMARY KEY(hero_id, friend_id) +); + +INSERT INTO species(id, name) VALUES (1, 'Human'), (2, 'Robot'); + +INSERT INTO home_worlds(id, name) VALUES(1, 'Tatooine'), (2, 'Alderaan'); + +INSERT INTO heros(id, name, species, home_world, hair_color) + VALUES (1, 'Luke Skywalker', 1, 1, 'blond'), + (2, 'Darth Vader', 1, 1, DEFAULT), + (3, 'Han Solo', 1, Null, DEFAULT), + (4, 'Leia Organa', 1, 2, DEFAULT), + (5, 'Wilhuff Tarkin', 1, Null, DEFAULT); + +INSERT INTO appears_in(hero_id, episode) + VALUES (1, 1), (1, 2), (1, 3), + (2, 1), (2, 2), (2, 3), + (3, 1), (3, 2), (3, 3), + (4, 1), (4, 2), (4, 3), + (5, 3); + + +INSERT INTO friends(hero_id, friend_id) + VALUES (1, 3), (1, 4), (2, 5), (3, 1), + (3, 4), (4, 1), (4, 3), (5, 2); diff --git a/wundergraph_example/src/bin/main.rs b/wundergraph_example/src/bin/main.rs index be6e95b..c6afc56 100644 --- a/wundergraph_example/src/bin/main.rs +++ b/wundergraph_example/src/bin/main.rs @@ -83,6 +83,8 @@ fn run_migrations(conn: &DBConnection) { migration_path.push("pg"); } else if cfg!(feature = "sqlite") { migration_path.push("sqlite"); + } else if cfg!(feature = "mysql") { + migration_path.push("mysql"); } let pending_migrations = ::diesel_migrations::mark_migrations_in_directory(conn, &migration_path) diff --git a/wundergraph_example/src/lib.rs b/wundergraph_example/src/lib.rs index 85fb532..4926489 100644 --- a/wundergraph_example/src/lib.rs +++ b/wundergraph_example/src/lib.rs @@ -261,6 +261,9 @@ pub type DBConnection = ::diesel::PgConnection; #[cfg(feature = "sqlite")] pub type DBConnection = ::diesel::SqliteConnection; +#[cfg(feature = "mysql")] +pub type DBConnection = ::diesel::mysql::MysqlConnection; + pub type DbBackend = ::Backend; pub type Schema = From 0ebcb0dccde66c35d16a44b9f9a2b7977307486c Mon Sep 17 00:00:00 2001 From: Alexei Pastuchov Date: Mon, 4 May 2020 16:43:38 +0200 Subject: [PATCH 07/19] ApplyOffset for diesel::mysql::Mysql sets mandatory limit parameter diesel limits bei u64::MAX https://github.com/diesel-rs/diesel/blob/968e7f79cf6953eb66ddb4256c1e8d5991dc808d/diesel/src/mysql/query_builder/limit_offset.rs#L57 Since GraphQL doesn't know unsigned types, limit is restricted i64::MAX --- wundergraph/src/query_builder/selection/offset.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/wundergraph/src/query_builder/selection/offset.rs b/wundergraph/src/query_builder/selection/offset.rs index 39f45d1..bb69783 100644 --- a/wundergraph/src/query_builder/selection/offset.rs +++ b/wundergraph/src/query_builder/selection/offset.rs @@ -6,7 +6,7 @@ use crate::juniper_ext::FromLookAheadValue; use crate::query_builder::selection::{BoxedQuery, LoadingHandler}; use crate::scalar::WundergraphScalarValue; use diesel::backend::Backend; -#[cfg(feature = "sqlite")] +#[cfg(any(feature = "sqlite", feature = "mysql"))] use diesel::query_dsl::methods::LimitDsl; #[cfg(any(feature = "postgres", feature = "sqlite", feature = "mysql"))] use diesel::query_dsl::methods::OffsetDsl; @@ -85,13 +85,18 @@ impl ApplyOffset for diesel::mysql::Mysql { { use juniper::LookAheadMethods; if let Some(offset) = select.argument("offset") { - Ok(<_ as OffsetDsl>::offset( + let q = <_ as OffsetDsl>::offset( query, i64::from_look_ahead(offset.value()) .ok_or(WundergraphError::CouldNotBuildFilterArgument)?, - )) + ); + if select.argument("limit").is_some() { + Ok(q) + } else { + Ok(<_ as LimitDsl>::limit(q, std::i64::MAX)) + } } else { Ok(query) } } -} \ No newline at end of file +} From 52759f93d76c88ea94698ecf1f9ffe982b3cb24a Mon Sep 17 00:00:00 2001 From: Alexei Pastuchov Date: Tue, 5 May 2020 17:29:54 +0200 Subject: [PATCH 08/19] [UNFINISHED DRAFT] Implement HandleInsert and HandleBatchInsert for diesel::mysql::Mysql --- .../src/query_builder/mutations/insert/mod.rs | 3 + .../query_builder/mutations/insert/mysql.rs | 120 ++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 wundergraph/src/query_builder/mutations/insert/mysql.rs diff --git a/wundergraph/src/query_builder/mutations/insert/mod.rs b/wundergraph/src/query_builder/mutations/insert/mod.rs index 22bd47c..6fc2e60 100644 --- a/wundergraph/src/query_builder/mutations/insert/mod.rs +++ b/wundergraph/src/query_builder/mutations/insert/mod.rs @@ -15,6 +15,9 @@ mod pg; #[cfg(feature = "sqlite")] mod sqlite; +#[cfg(feature = "mysql")] +mod mysql; + #[doc(hidden)] pub fn handle_insert( selection: Option<&'_ [Selection<'_, WundergraphScalarValue>]>, diff --git a/wundergraph/src/query_builder/mutations/insert/mysql.rs b/wundergraph/src/query_builder/mutations/insert/mysql.rs new file mode 100644 index 0000000..0483711 --- /dev/null +++ b/wundergraph/src/query_builder/mutations/insert/mysql.rs @@ -0,0 +1,120 @@ +use super::{HandleBatchInsert, HandleInsert}; +use crate::context::WundergraphContext; +use crate::helper::UnRef; +use crate::query_builder::selection::fields::WundergraphFieldList; +use crate::query_builder::selection::filter::build_filter::BuildFilter; +use crate::query_builder::selection::order::BuildOrder; +use crate::query_builder::selection::query_modifier::QueryModifier; +use crate::query_builder::selection::select::BuildSelect; +use crate::query_builder::selection::{LoadingHandler, SqlTypeOfPlaceholder}; +use crate::scalar::WundergraphScalarValue; +use diesel::associations::HasTable; +use diesel::dsl::SqlTypeOf; +use diesel::expression::{Expression, NonAggregate, SelectableExpression}; +use diesel::insertable::CanInsertInSingleQuery; +use diesel::mysql::Mysql; +use diesel::query_builder::{BoxedSelectStatement, QueryFragment}; +use diesel::query_dsl::methods::{BoxedDsl, FilterDsl, LimitDsl}; +use diesel::sql_types::{Bigint, HasSqlType}; +use diesel::{AppearsOnTable, Connection, Insertable, RunQueryDsl, Table}; +use diesel::{no_arg_sql_function, EqAll, Identifiable, Queryable}; +use juniper::{ExecutionResult, Executor, Selection, Value}; + +// https://dev.mysql.com/doc/refman/8.0/en/getting-unique-id.html +diesel::no_arg_sql_function!(LAST_INSERT_ID, Bigint); + +impl HandleInsert for T +where + T: Table + HasTable + 'static, + T::FromClause: QueryFragment, + L: LoadingHandler + 'static, + L::Columns: BuildOrder + + BuildSelect>, + Ctx: WundergraphContext + QueryModifier, + Ctx::Connection: Connection, + L::FieldList: WundergraphFieldList, + I: Insertable, + I::Values: QueryFragment + CanInsertInSingleQuery, + T::PrimaryKey: QueryFragment + Default, + T: BoxedDsl< + 'static, + Mysql, + Output = BoxedSelectStatement<'static, SqlTypeOf<::AllColumns>, T, Mysql>, + >, + ::Backend: HasSqlType> + + HasSqlType>, + >::Ret: AppearsOnTable, + T::PrimaryKey: EqAll, + T::PrimaryKey: Expression, + &'static L: Identifiable, + <&'static L as Identifiable>::Id: UnRef<'static, UnRefed = i32>, + >::Output: + SelectableExpression + NonAggregate + QueryFragment + 'static, +{ + fn handle_insert( + selection: Option<&'_ [Selection<'_, WundergraphScalarValue>]>, + executor: &Executor<'_, Ctx, WundergraphScalarValue>, + insertable: I, + ) -> ExecutionResult { + let ctx = executor.context(); + let conn = ctx.get_connection(); + conn.transaction(|| -> ExecutionResult { + let look_ahead = executor.look_ahead(); + insertable.insert_into(T::table()).execute(conn)?; + + let last_insert_id: i64 = diesel::select(LAST_INSERT_ID).first(conn)?; + + let q = L::build_query(&[], &look_ahead)?; + let q = FilterDsl::filter(q, T::PrimaryKey::default().eq_all(last_insert_id as i32)); + let q = LimitDsl::limit(q, 1); + #[cfg(feature = "debug")] + { + log::debug!("{}", ::diesel::debug_query(&q)); + } + let items = L::load(&look_ahead, selection, executor, q)?; + + Ok(items.into_iter().next().unwrap_or(Value::Null)) + }) + } +} + +impl HandleBatchInsert for T +where + T: Table + HasTable
+ 'static, + T::FromClause: QueryFragment, + L: LoadingHandler + 'static, + L::Columns: BuildOrder + + BuildSelect>, + Ctx: WundergraphContext + QueryModifier, + Ctx::Connection: Connection, + L::FieldList: WundergraphFieldList, + Vec: Insertable, + as Insertable>::Values: QueryFragment + CanInsertInSingleQuery, + T::PrimaryKey: QueryFragment + Default, + T: BoxedDsl< + 'static, + Mysql, + Output = BoxedSelectStatement<'static, SqlTypeOf<::AllColumns>, T, Mysql>, + >, + ::Backend: HasSqlType> + + HasSqlType>, + >::Ret: AppearsOnTable, + T::PrimaryKey: EqAll, + &'static L: Identifiable, + <&'static L as Identifiable>::Id: UnRef<'static, UnRefed = Id>, + Id: Queryable<::SqlType, Mysql>, + >::Output: + SelectableExpression + NonAggregate + QueryFragment + 'static, +{ + fn handle_batch_insert( + _selection: Option<&'_ [Selection<'_, WundergraphScalarValue>]>, + executor: &Executor<'_, Ctx, WundergraphScalarValue>, + _batch: Vec, + ) -> ExecutionResult { + let ctx = executor.context(); + let conn = ctx.get_connection(); + conn.transaction(|| -> ExecutionResult { + unimplemented!() + }) + } +} From 5f9478d11f608d379530f6c794ccb703bfde5aa4 Mon Sep 17 00:00:00 2001 From: Georg Semmler Date: Tue, 12 May 2020 14:31:42 +0200 Subject: [PATCH 09/19] Fix wundergraph_example to compile with the mysql backend This requires some manual `HandleInsert` + `HandleBatchInsert` impls because the default provided one only works with integer primary keys, because there is no way to generally receive the last inserted primary key value using mysql. Interestingly we cannot derive `Insertable` then anymore because of an error about conflicting impls with the wild card impl in wundergraph itself. I think that's because rustc will not use associated types to see if an impl actual is conflicting or not. --- wundergraph_example/src/mutations.rs | 191 ++++++++++++++++++++++++++- 1 file changed, 187 insertions(+), 4 deletions(-) diff --git a/wundergraph_example/src/mutations.rs b/wundergraph_example/src/mutations.rs index bd40cf8..2123d5e 100644 --- a/wundergraph_example/src/mutations.rs +++ b/wundergraph_example/src/mutations.rs @@ -9,7 +9,11 @@ use super::Friend; use super::Hero; use super::HomeWorld; use super::Species; +use diesel::prelude::*; use juniper::*; +use wundergraph::query_builder::mutations::{HandleBatchInsert, HandleInsert}; +use wundergraph::query_builder::selection::LoadingHandler; +use wundergraph::{QueryModifier, WundergraphContext}; #[derive(Insertable, GraphQLInputObject, Clone, Debug)] #[table_name = "heros"] @@ -56,20 +60,199 @@ pub struct HomeWorldChangeset { name: Option, } -#[derive(Insertable, GraphQLInputObject, Debug, Copy, Clone)] -#[table_name = "friends"] +#[cfg_attr(not(feature = "mysql"), derive(Insertable))] +#[derive(GraphQLInputObject, Debug, Copy, Clone)] +#[cfg_attr(not(feature = "mysql"), table_name = "friends")] pub struct NewFriend { hero_id: i32, friend_id: i32, } -#[derive(Insertable, GraphQLInputObject, Debug, Copy, Clone)] -#[table_name = "appears_in"] +impl HandleInsert for friends::table +where + Ctx: WundergraphContext + QueryModifier + 'static, + Ctx::Connection: Connection, +{ + fn handle_insert( + selection: Option<&'_ [Selection<'_, wundergraph::scalar::WundergraphScalarValue>]>, + executor: &Executor<'_, Ctx, wundergraph::scalar::WundergraphScalarValue>, + insertable: NewFriend, + ) -> ExecutionResult { + let ctx = executor.context(); + let conn = ctx.get_connection(); + let look_ahead = executor.look_ahead(); + + conn.transaction(|| { + diesel::insert_into(friends::table) + .values(( + friends::hero_id.eq(insertable.hero_id), + friends::friend_id.eq(insertable.friend_id), + )) + .execute(conn)?; + + let query = >::build_query( + &[], + &look_ahead, + )? + .filter( + friends::hero_id + .eq(insertable.hero_id) + .and(friends::friend_id.eq(insertable.friend_id)), + ) + .limit(1); + + let items = Friend::load(&look_ahead, selection, executor, query)?; + Ok(items.into_iter().next().unwrap_or(Value::Null)) + }) + } +} + +impl HandleBatchInsert for friends::table +where + Ctx: WundergraphContext + QueryModifier + 'static, + Ctx::Connection: Connection, +{ + fn handle_batch_insert( + selection: Option<&'_ [Selection<'_, wundergraph::scalar::WundergraphScalarValue>]>, + executor: &Executor<'_, Ctx, wundergraph::scalar::WundergraphScalarValue>, + insertable: Vec, + ) -> ExecutionResult { + let ctx = executor.context(); + let conn = ctx.get_connection(); + let look_ahead = executor.look_ahead(); + + conn.transaction(|| { + { + let insert_values = insertable + .iter() + .map(|NewFriend { hero_id, friend_id }| { + ( + friends::hero_id.eq(hero_id), + friends::friend_id.eq(friend_id), + ) + }) + .collect::>(); + diesel::insert_into(friends::table) + .values(insert_values) + .execute(conn)?; + } + + let mut query = >::build_query( + &[], + &look_ahead, + )?; + + for NewFriend { hero_id, friend_id } in insertable { + query = query.or_filter( + friends::hero_id + .eq(hero_id) + .and(friends::friend_id.eq(friend_id)), + ) + } + + let items = Friend::load(&look_ahead, selection, executor, query)?; + Ok(Value::list(items)) + }) + } +} + +#[cfg_attr(not(feature = "mysql"), derive(Insertable))] +#[derive(GraphQLInputObject, Debug, Copy, Clone)] +#[cfg_attr(not(feature = "mysql"), table_name = "appears_in")] pub struct NewAppearsIn { hero_id: i32, episode: Episode, } +impl HandleInsert for appears_in::table +where + Ctx: WundergraphContext + QueryModifier + 'static, + Ctx::Connection: Connection, +{ + fn handle_insert( + selection: Option<&'_ [Selection<'_, wundergraph::scalar::WundergraphScalarValue>]>, + executor: &Executor<'_, Ctx, wundergraph::scalar::WundergraphScalarValue>, + insertable: NewAppearsIn, + ) -> ExecutionResult { + let ctx = executor.context(); + let conn = ctx.get_connection(); + let look_ahead = executor.look_ahead(); + + conn.transaction(|| { + diesel::insert_into(appears_in::table) + .values(( + appears_in::hero_id.eq(insertable.hero_id), + appears_in::episode.eq(insertable.episode), + )) + .execute(conn)?; + + let query = >::build_query( + &[], + &look_ahead, + )? + .filter( + appears_in::hero_id + .eq(insertable.hero_id) + .and(appears_in::episode.eq(insertable.episode)), + ) + .limit(1); + + let items = AppearsIn::load(&look_ahead, selection, executor, query)?; + Ok(items.into_iter().next().unwrap_or(Value::Null)) + }) + } +} + +impl HandleBatchInsert + for appears_in::table +where + Ctx: WundergraphContext + QueryModifier + 'static, + Ctx::Connection: Connection, +{ + fn handle_batch_insert( + selection: Option<&'_ [Selection<'_, wundergraph::scalar::WundergraphScalarValue>]>, + executor: &Executor<'_, Ctx, wundergraph::scalar::WundergraphScalarValue>, + insertable: Vec, + ) -> ExecutionResult { + let ctx = executor.context(); + let conn = ctx.get_connection(); + let look_ahead = executor.look_ahead(); + + conn.transaction(|| { + { + let insert_values = insertable + .iter() + .map(|NewAppearsIn { hero_id, episode }| { + ( + appears_in::hero_id.eq(hero_id), + appears_in::episode.eq(episode), + ) + }) + .collect::>(); + diesel::insert_into(appears_in::table) + .values(insert_values) + .execute(conn)?; + } + + let mut query = >::build_query( + &[], + &look_ahead, + )?; + + for NewAppearsIn { hero_id, episode } in insertable { + query = query.or_filter( + appears_in::hero_id + .eq(hero_id) + .and(appears_in::episode.eq(episode)), + ) + } + + let items = AppearsIn::load(&look_ahead, selection, executor, query)?; + Ok(Value::list(items)) + }) + } +} + wundergraph::mutation_object! { /// Global mutation object for the schema Mutation { From a92f2c50dde9fb6870e678e41c68285fb302c09c Mon Sep 17 00:00:00 2001 From: Alexei Pastuchov Date: Wed, 22 Apr 2020 11:31:49 +0200 Subject: [PATCH 10/19] Add mysql feature flags Add corresponding feature flags to the wundergraph_derive, wundergraph and wundergraph_cli crate --- wundergraph/Cargo.toml | 1 + wundergraph_cli/Cargo.toml | 3 ++- wundergraph_derive/Cargo.toml | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/wundergraph/Cargo.toml b/wundergraph/Cargo.toml index b0f80b3..92a0fbf 100644 --- a/wundergraph/Cargo.toml +++ b/wundergraph/Cargo.toml @@ -39,6 +39,7 @@ insta = "0.12" [features] default = [] debug = ["wundergraph_derive/debug", "log"] +mysql = ["diesel/mysql", "wundergraph_derive/mysql"] sqlite = ["diesel/sqlite", "wundergraph_derive/sqlite"] postgres = ["diesel/postgres", "wundergraph_derive/postgres"] extras = ["uuid", "chrono"] diff --git a/wundergraph_cli/Cargo.toml b/wundergraph_cli/Cargo.toml index ae1cdfb..55f6519 100644 --- a/wundergraph_cli/Cargo.toml +++ b/wundergraph_cli/Cargo.toml @@ -24,5 +24,6 @@ serde_json = "1" [features] default = ["postgres", "sqlite"] -sqlite = ["diesel/sqlite"] +mysql = ["diesel/mysql"] postgres = ["diesel/postgres"] +sqlite = ["diesel/sqlite"] diff --git a/wundergraph_derive/Cargo.toml b/wundergraph_derive/Cargo.toml index 08dc410..915c714 100644 --- a/wundergraph_derive/Cargo.toml +++ b/wundergraph_derive/Cargo.toml @@ -21,6 +21,7 @@ proc-macro = true [features] default = [] nightly = ["proc-macro2/nightly"] +mysql = [] postgres = [] sqlite = [] debug = [] From e8e1a4160e839ceeb32604e9c2652fba62c9f203 Mon Sep 17 00:00:00 2001 From: Alexei Pastuchov Date: Wed, 22 Apr 2020 11:40:19 +0200 Subject: [PATCH 11/19] Generate an impl for LoadingHandler for diesel::mysql::Mysql --- wundergraph_derive/src/wundergraph_entity.rs | 23 ++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/wundergraph_derive/src/wundergraph_entity.rs b/wundergraph_derive/src/wundergraph_entity.rs index 759274a..0e48b92 100644 --- a/wundergraph_derive/src/wundergraph_entity.rs +++ b/wundergraph_derive/src/wundergraph_entity.rs @@ -29,6 +29,16 @@ pub fn derive(item: &syn::DeriveInput) -> Result { None }; + let mysql_loading_handler = if cfg!(feature = "mysql") { + Some(derive_loading_handler( + &model, + item, + "e!(diesel::mysql::Mysql), + )?) + } else { + None + }; + let pg_non_table_field_filter = if cfg!(feature = "postgres") { Some(derive_non_table_filter( &model, @@ -49,6 +59,17 @@ pub fn derive(item: &syn::DeriveInput) -> Result { None }; + + let mysql_non_table_field_filter = if cfg!(feature = "mysql") { + Some(derive_non_table_filter( + &model, + item, + "e!(diesel::mysql::Mysql), + )?) + } else { + None + }; + let belongs_to = crate::belonging_to::derive_belonging_to(&model, item)?; Ok(wrap_in_dummy_mod( @@ -61,8 +82,10 @@ pub fn derive(item: &syn::DeriveInput) -> Result { #pg_loading_handler #sqlite_loading_handler + #mysql_loading_handler #pg_non_table_field_filter #sqlite_non_table_field_filter + #mysql_non_table_field_filter #(#belongs_to)* }, From add2c6da52a87c6ce71857f48d0efd1e422ba2a0 Mon Sep 17 00:00:00 2001 From: Alexei Pastuchov Date: Wed, 22 Apr 2020 11:42:13 +0200 Subject: [PATCH 12/19] Generate an impl for WundergraphBelongsTo for diesel::mysql::Mysql --- wundergraph_derive/src/belonging_to.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/wundergraph_derive/src/belonging_to.rs b/wundergraph_derive/src/belonging_to.rs index 0120433..5420d94 100644 --- a/wundergraph_derive/src/belonging_to.rs +++ b/wundergraph_derive/src/belonging_to.rs @@ -70,9 +70,22 @@ pub fn derive_belonging_to( } else { None }; + let mysql = if cfg!(feature = "mysql") { + Some(derive_belongs_to( + model, + item, + parent_ty, + &key_ty, + f.sql_name(), + "e!(diesel::mysql::Mysql), + )?) + } else { + None + }; Ok(quote! { #pg #sqlite + #mysql }) }) .collect::, _>>() From 0e82a5c8819d8d360b23bdbe59ef7249a82a2649 Mon Sep 17 00:00:00 2001 From: Alexei Pastuchov Date: Wed, 22 Apr 2020 11:50:28 +0200 Subject: [PATCH 13/19] Generate an impl for BuildFilterHelper for diesel::mysql::Mysql --- wundergraph_derive/src/build_filter_helper.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/wundergraph_derive/src/build_filter_helper.rs b/wundergraph_derive/src/build_filter_helper.rs index e91653f..95cff3a 100644 --- a/wundergraph_derive/src/build_filter_helper.rs +++ b/wundergraph_derive/src/build_filter_helper.rs @@ -28,12 +28,23 @@ pub fn derive(item: &syn::DeriveInput) -> Result { None }; + let mysql = if cfg!(feature = "mysql") { + Some(derive_non_table_filter( + &model, + item, + "e!(diesel::mysql::Mysql), + )?) + } else { + None + }; + Ok(wrap_in_dummy_mod( "build_filter_helper", &model.name, "e! { #pg #sqlite + #mysql }, )) } From f781c3210ab0ee55bd95d99d3522942afc6a0066 Mon Sep 17 00:00:00 2001 From: Alexei Pastuchov Date: Sat, 2 May 2020 08:07:38 +0200 Subject: [PATCH 14/19] Implement ApplyOffset for diesel::mysql::Mysq --- .../src/query_builder/selection/offset.rs | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/wundergraph/src/query_builder/selection/offset.rs b/wundergraph/src/query_builder/selection/offset.rs index 0fb0760..bb69783 100644 --- a/wundergraph/src/query_builder/selection/offset.rs +++ b/wundergraph/src/query_builder/selection/offset.rs @@ -1,15 +1,16 @@ use crate::error::Result; -#[cfg(any(feature = "postgres", feature = "sqlite"))] +#[cfg(any(feature = "postgres", feature = "sqlite", feature = "mysql"))] use crate::error::WundergraphError; -#[cfg(any(feature = "postgres", feature = "sqlite"))] +#[cfg(any(feature = "postgres", feature = "sqlite", feature = "mysql"))] use crate::juniper_ext::FromLookAheadValue; use crate::query_builder::selection::{BoxedQuery, LoadingHandler}; use crate::scalar::WundergraphScalarValue; use diesel::backend::Backend; -#[cfg(feature = "sqlite")] +#[cfg(any(feature = "sqlite", feature = "mysql"))] use diesel::query_dsl::methods::LimitDsl; -#[cfg(any(feature = "postgres", feature = "sqlite"))] +#[cfg(any(feature = "postgres", feature = "sqlite", feature = "mysql"))] use diesel::query_dsl::methods::OffsetDsl; + use juniper::LookAheadSelection; /// A trait abstracting over the different behaviour of limit/offset @@ -72,3 +73,30 @@ impl ApplyOffset for diesel::sqlite::Sqlite { } } } + +#[cfg(feature = "mysql")] +impl ApplyOffset for diesel::mysql::Mysql { + fn apply_offset<'a, L, Ctx>( + query: BoxedQuery<'a, L, Self, Ctx>, + select: &LookAheadSelection<'_, WundergraphScalarValue>, + ) -> Result> + where + L: LoadingHandler, + { + use juniper::LookAheadMethods; + if let Some(offset) = select.argument("offset") { + let q = <_ as OffsetDsl>::offset( + query, + i64::from_look_ahead(offset.value()) + .ok_or(WundergraphError::CouldNotBuildFilterArgument)?, + ); + if select.argument("limit").is_some() { + Ok(q) + } else { + Ok(<_ as LimitDsl>::limit(q, std::i64::MAX)) + } + } else { + Ok(query) + } + } +} From 0bd0c52c11ed7b2551a35cb9219bc954dfd8d89f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B9=20=D0=9F=D0=B0?= =?UTF-8?q?=D1=81=D1=82=D1=83=D1=85=D0=BE=D0=B2?= Date: Sat, 16 May 2020 21:15:44 +0200 Subject: [PATCH 15/19] Update wundergraph/src/query_builder/selection/offset.rs import LimitDsl for sqlite and mysql Co-authored-by: Georg Semmler --- wundergraph/src/query_builder/selection/offset.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/wundergraph/src/query_builder/selection/offset.rs b/wundergraph/src/query_builder/selection/offset.rs index bb69783..b482a58 100644 --- a/wundergraph/src/query_builder/selection/offset.rs +++ b/wundergraph/src/query_builder/selection/offset.rs @@ -10,7 +10,6 @@ use diesel::backend::Backend; use diesel::query_dsl::methods::LimitDsl; #[cfg(any(feature = "postgres", feature = "sqlite", feature = "mysql"))] use diesel::query_dsl::methods::OffsetDsl; - use juniper::LookAheadSelection; /// A trait abstracting over the different behaviour of limit/offset @@ -93,6 +92,12 @@ impl ApplyOffset for diesel::mysql::Mysql { if select.argument("limit").is_some() { Ok(q) } else { + // Mysql requires a limit clause in front of any offset clause + // The documentation proposes the following: + // > To retrieve all rows from a certain offset up to the end of the + // > result set, you can use some large number for the second parameter. + // https://dev.mysql.com/doc/refman/8.0/en/select.html + // Therefore we just use i64::MAX as limit here Ok(<_ as LimitDsl>::limit(q, std::i64::MAX)) } } else { From bafc0060184e4d17f7bdfbbe2846241a5d8645b7 Mon Sep 17 00:00:00 2001 From: Alexei Pastuchov Date: Sat, 16 May 2020 21:32:39 +0200 Subject: [PATCH 16/19] rustfmt adds an empty like for sake of CI --- wundergraph_derive/src/wundergraph_entity.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/wundergraph_derive/src/wundergraph_entity.rs b/wundergraph_derive/src/wundergraph_entity.rs index 0e48b92..049c5e6 100644 --- a/wundergraph_derive/src/wundergraph_entity.rs +++ b/wundergraph_derive/src/wundergraph_entity.rs @@ -59,7 +59,6 @@ pub fn derive(item: &syn::DeriveInput) -> Result { None }; - let mysql_non_table_field_filter = if cfg!(feature = "mysql") { Some(derive_non_table_filter( &model, From 463dafcaa9bc96c5fe4191166b66d74a59f01aed Mon Sep 17 00:00:00 2001 From: Alexei Pastuchov Date: Sun, 17 May 2020 20:40:21 +0200 Subject: [PATCH 17/19] avoid warnings: trait objects without an explicit dyn are deprecated --- wundergraph_cli/src/infer_schema_internals/mysql.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wundergraph_cli/src/infer_schema_internals/mysql.rs b/wundergraph_cli/src/infer_schema_internals/mysql.rs index e9afb69..384a727 100644 --- a/wundergraph_cli/src/infer_schema_internals/mysql.rs +++ b/wundergraph_cli/src/infer_schema_internals/mysql.rs @@ -79,7 +79,7 @@ pub fn load_foreign_key_constraints( Ok(constraints) } -pub fn determine_column_type(attr: &ColumnInformation) -> Result> { +pub fn determine_column_type(attr: &ColumnInformation) -> Result> { let tpe = determine_type_name(&attr.type_name)?; let unsigned = determine_unsigned(&attr.type_name); @@ -91,7 +91,7 @@ pub fn determine_column_type(attr: &ColumnInformation) -> Result Result> { +fn determine_type_name(sql_type_name: &str) -> Result> { let result = if sql_type_name == "tinyint(1)" { "bool" } else if sql_type_name.starts_with("int") { From 858d82f09d7e3037f3ab966e4e7f848100bf3f9b Mon Sep 17 00:00:00 2001 From: Alexei Pastuchov Date: Mon, 15 Jun 2020 09:05:45 +0200 Subject: [PATCH 18/19] wundergraph_example supports MySql --- wundergraph_example/Cargo.toml | 3 +- .../mysql/2018-01-24-131925_setup/down.sql | 7 + .../mysql/2018-01-24-131925_setup/up.sql | 60 ++ .../src/bin/wundergraph_example.rs | 3 + wundergraph_example/src/lib.rs | 3 + wundergraph_example/src/mutations.rs | 3 + wundergraph_example/src/mutations/mysql.rs | 564 ++++++++++++++++++ 7 files changed, 642 insertions(+), 1 deletion(-) create mode 100644 wundergraph_example/migrations/mysql/2018-01-24-131925_setup/down.sql create mode 100644 wundergraph_example/migrations/mysql/2018-01-24-131925_setup/up.sql create mode 100644 wundergraph_example/src/mutations/mysql.rs diff --git a/wundergraph_example/Cargo.toml b/wundergraph_example/Cargo.toml index f21fa61..24639a0 100644 --- a/wundergraph_example/Cargo.toml +++ b/wundergraph_example/Cargo.toml @@ -29,5 +29,6 @@ default-features = false [features] default = ["postgres", "wundergraph/debug"] -sqlite = ["wundergraph/sqlite", "diesel/sqlite"] +mysql = ["wundergraph/mysql", "diesel/mysql"] postgres = ["wundergraph/postgres", "diesel/postgres"] +sqlite = ["wundergraph/sqlite", "diesel/sqlite"] diff --git a/wundergraph_example/migrations/mysql/2018-01-24-131925_setup/down.sql b/wundergraph_example/migrations/mysql/2018-01-24-131925_setup/down.sql new file mode 100644 index 0000000..c93586f --- /dev/null +++ b/wundergraph_example/migrations/mysql/2018-01-24-131925_setup/down.sql @@ -0,0 +1,7 @@ +-- This file should undo anything in `up.sql` + +DROP TABLE friends; +DROP TABLE appears_in; +DROP TABLE heros; +DROP TABLE home_worlds; +DROP TABLE species; diff --git a/wundergraph_example/migrations/mysql/2018-01-24-131925_setup/up.sql b/wundergraph_example/migrations/mysql/2018-01-24-131925_setup/up.sql new file mode 100644 index 0000000..397f41c --- /dev/null +++ b/wundergraph_example/migrations/mysql/2018-01-24-131925_setup/up.sql @@ -0,0 +1,60 @@ +CREATE TABLE `species`( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` TEXT NOT NULL, + PRIMARY KEY (`id`) +); + +CREATE TABLE `home_worlds`( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` TEXT NOT NULL, + PRIMARY KEY (`id`) +); + +CREATE TABLE `heros`( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` TEXT NOT NULL, + `hair_color` TEXT, + `species` INTEGER NOT NULL, + `home_world` INTEGER DEFAULT NULL, + PRIMARY KEY (`id`), + FOREIGN KEY (`species`) REFERENCES `species` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + FOREIGN KEY (`home_world`) REFERENCES `home_worlds` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +); + +CREATE TABLE `appears_in`( + `hero_id` INTEGER NOT NULL, + `episode` SMALLINT(1) NOT NULL CHECK(episode IN (1,2,3)), + PRIMARY KEY(`hero_id`, `episode`), + FOREIGN KEY (`hero_id`) REFERENCES `heros` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +); + +CREATE TABLE `friends`( + `hero_id` INTEGER NOT NULL, + `friend_id` INTEGER NOT NULL, + PRIMARY KEY(`hero_id`, `friend_id`), + FOREIGN KEY (`hero_id`) REFERENCES `heros`(`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + FOREIGN KEY (`friend_id`) REFERENCES `heros` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +); + +INSERT INTO species(id, name) VALUES (1, 'Human'), (2, 'Robot'); + +INSERT INTO home_worlds(id, name) VALUES(1, 'Tatooine'), (2, 'Alderaan'); + +INSERT INTO heros(id, name, species, home_world, hair_color) + VALUES (1, 'Luke Skywalker', 1, 1, 'blond'), + (2, 'Darth Vader', 1, 1, DEFAULT), + (3, 'Han Solo', 1, Null, DEFAULT), + (4, 'Leia Organa', 1, 2, DEFAULT), + (5, 'Wilhuff Tarkin', 1, Null, DEFAULT); + +INSERT INTO appears_in(hero_id, episode) + VALUES (1, 1), (1, 2), (1, 3), + (2, 1), (2, 2), (2, 3), + (3, 1), (3, 2), (3, 3), + (4, 1), (4, 2), (4, 3), + (5, 3); + + +INSERT INTO friends(hero_id, friend_id) + VALUES (1, 3), (1, 4), (2, 5), (3, 1), + (3, 4), (4, 1), (4, 3), (5, 2); diff --git a/wundergraph_example/src/bin/wundergraph_example.rs b/wundergraph_example/src/bin/wundergraph_example.rs index be6e95b..6839b6b 100644 --- a/wundergraph_example/src/bin/wundergraph_example.rs +++ b/wundergraph_example/src/bin/wundergraph_example.rs @@ -83,7 +83,10 @@ fn run_migrations(conn: &DBConnection) { migration_path.push("pg"); } else if cfg!(feature = "sqlite") { migration_path.push("sqlite"); + } else if cfg!(feature = "mysql") { + migration_path.push("mysql"); } + let pending_migrations = ::diesel_migrations::mark_migrations_in_directory(conn, &migration_path) .unwrap() diff --git a/wundergraph_example/src/lib.rs b/wundergraph_example/src/lib.rs index 85fb532..7c6f381 100644 --- a/wundergraph_example/src/lib.rs +++ b/wundergraph_example/src/lib.rs @@ -261,6 +261,9 @@ pub type DBConnection = ::diesel::PgConnection; #[cfg(feature = "sqlite")] pub type DBConnection = ::diesel::SqliteConnection; +#[cfg(feature = "mysql")] +pub type DBConnection = ::diesel::MysqlConnection; + pub type DbBackend = ::Backend; pub type Schema = diff --git a/wundergraph_example/src/mutations.rs b/wundergraph_example/src/mutations.rs index bd40cf8..9e0993b 100644 --- a/wundergraph_example/src/mutations.rs +++ b/wundergraph_example/src/mutations.rs @@ -11,6 +11,9 @@ use super::HomeWorld; use super::Species; use juniper::*; +#[cfg(feature = "mysql")] +pub mod mysql; + #[derive(Insertable, GraphQLInputObject, Clone, Debug)] #[table_name = "heros"] pub struct NewHero { diff --git a/wundergraph_example/src/mutations/mysql.rs b/wundergraph_example/src/mutations/mysql.rs new file mode 100644 index 0000000..c84a81a --- /dev/null +++ b/wundergraph_example/src/mutations/mysql.rs @@ -0,0 +1,564 @@ +use crate::mutations::{NewAppearsIn, NewFriend, NewHero, NewHomeWorld, NewSpecies}; +use diesel::associations::HasTable; +use diesel::deserialize::FromSql; +use diesel::dsl::SqlTypeOf; +use diesel::expression::{Expression, NonAggregate, SelectableExpression}; +use diesel::insertable::CanInsertInSingleQuery; +use diesel::mysql::Mysql; +use diesel::query_builder::{BoxedSelectStatement, QueryFragment}; +use diesel::query_dsl::methods::{BoxedDsl, FilterDsl}; +use diesel::sql_types::{Bigint, HasSqlType, Integer}; +use diesel::{ + no_arg_sql_function, AppearsOnTable, Connection, EqAll, Identifiable, Insertable, RunQueryDsl, + Table, +}; +use juniper::{ExecutionResult, Executor, Selection, Value}; +use std::convert::TryFrom; +use wundergraph::query_builder::mutations::{HandleBatchInsert, HandleInsert}; +use wundergraph::query_builder::selection::fields::WundergraphFieldList; +use wundergraph::query_builder::selection::filter::BuildFilter; +use wundergraph::query_builder::selection::order::BuildOrder; +use wundergraph::query_builder::selection::select::BuildSelect; +use wundergraph::query_builder::selection::{LoadingHandler, QueryModifier, SqlTypeOfPlaceholder}; +use wundergraph::scalar::WundergraphScalarValue; +use wundergraph::WundergraphContext; + +diesel::no_arg_sql_function!(LAST_INSERT_ID, Bigint); + +fn handle_i32_pk_insert( + selection: Option<&'_ [Selection<'_, WundergraphScalarValue>]>, + executor: &Executor<'_, Ctx, WundergraphScalarValue>, + insertable: I, +) -> ExecutionResult +where + L: LoadingHandler + 'static, + L::FieldList: WundergraphFieldList, + >::Ret: AppearsOnTable, + L::Columns: BuildOrder + + BuildSelect>, + &'static L: Identifiable, + I: Insertable, + I::Values: QueryFragment + CanInsertInSingleQuery, + Ctx: WundergraphContext + QueryModifier, + Ctx::Connection: Connection, + ::Backend: HasSqlType> + + HasSqlType>, + T: Table + HasTable
+ 'static, + T::FromClause: QueryFragment, + T: BoxedDsl< + 'static, + Mysql, + Output = BoxedSelectStatement<'static, SqlTypeOf<::AllColumns>, T, Mysql>, + >, + T::PrimaryKey: EqAll, + T::PrimaryKey: Expression, + T::PrimaryKey: QueryFragment + Default, + >::Output: + SelectableExpression + NonAggregate + QueryFragment + 'static, + i32: FromSql, +{ + let ctx = executor.context(); + let conn = ctx.get_connection(); + let look_ahead = executor.look_ahead(); + insertable.insert_into(T::table()).execute(conn).unwrap(); + let last_insert_id: i64 = diesel::select(LAST_INSERT_ID).first(conn)?; + let last_insert_id = i32::try_from(last_insert_id)?; + let q = L::build_query(&[], &look_ahead)?; + let q = FilterDsl::filter(q, T::PrimaryKey::default().eq_all(last_insert_id)); + let items = L::load(&look_ahead, selection, executor, q)?; + Ok(items.into_iter().next().unwrap_or(Value::Null)) +} + +impl HandleInsert for super::home_worlds::table +where + L: LoadingHandler + 'static, + L::FieldList: WundergraphFieldList, + >::Ret: AppearsOnTable, + L::Columns: BuildOrder + + BuildSelect< + super::home_worlds::table, + Mysql, + SqlTypeOfPlaceholder< + L::FieldList, + Mysql, + L::PrimaryKeyIndex, + super::home_worlds::table, + Ctx, + >, + >, + &'static L: Identifiable, + Ctx: WundergraphContext + QueryModifier, + Ctx::Connection: Connection, + ::Backend: HasSqlType> + + HasSqlType< + SqlTypeOfPlaceholder< + L::FieldList, + Mysql, + L::PrimaryKeyIndex, + super::home_worlds::table, + Ctx, + >, + >, +{ + fn handle_insert( + selection: Option<&'_ [Selection<'_, WundergraphScalarValue>]>, + executor: &Executor<'_, Ctx, WundergraphScalarValue>, + insertable: NewHomeWorld, + ) -> ExecutionResult { + handle_i32_pk_insert(selection, executor, insertable) + } +} + +impl HandleBatchInsert for super::home_worlds::table +where + L: LoadingHandler + 'static, + L::FieldList: WundergraphFieldList, + >::Ret: AppearsOnTable, + L::Columns: BuildOrder + + BuildSelect< + super::home_worlds::table, + Mysql, + SqlTypeOfPlaceholder< + L::FieldList, + Mysql, + L::PrimaryKeyIndex, + super::home_worlds::table, + Ctx, + >, + >, + &'static L: Identifiable, + Ctx: WundergraphContext + QueryModifier, + Ctx::Connection: Connection, + ::Backend: HasSqlType> + + HasSqlType< + SqlTypeOfPlaceholder< + L::FieldList, + Mysql, + L::PrimaryKeyIndex, + super::home_worlds::table, + Ctx, + >, + >, +{ + fn handle_batch_insert( + selection: Option<&'_ [Selection<'_, WundergraphScalarValue>]>, + executor: &Executor<'_, Ctx, WundergraphScalarValue>, + batch: Vec, + ) -> ExecutionResult { + let r = batch + .into_iter() + .map(|i| handle_i32_pk_insert(selection, executor, i)) + .collect::, _>>()?; + Ok(Value::List(r)) + } +} + +impl HandleInsert for super::species::table +where + L: LoadingHandler + 'static, + L::FieldList: WundergraphFieldList, + >::Ret: AppearsOnTable, + L::Columns: BuildOrder + + BuildSelect< + super::species::table, + Mysql, + SqlTypeOfPlaceholder< + L::FieldList, + Mysql, + L::PrimaryKeyIndex, + super::species::table, + Ctx, + >, + >, + &'static L: Identifiable, + Ctx: WundergraphContext + QueryModifier, + Ctx::Connection: Connection, + ::Backend: HasSqlType> + + HasSqlType< + SqlTypeOfPlaceholder< + L::FieldList, + Mysql, + L::PrimaryKeyIndex, + super::species::table, + Ctx, + >, + >, +{ + fn handle_insert( + selection: Option<&'_ [Selection<'_, WundergraphScalarValue>]>, + executor: &Executor<'_, Ctx, WundergraphScalarValue>, + insertable: NewSpecies, + ) -> ExecutionResult { + handle_i32_pk_insert(selection, executor, insertable) + } +} + +impl HandleBatchInsert for super::species::table +where + L: LoadingHandler + 'static, + L::FieldList: WundergraphFieldList, + >::Ret: AppearsOnTable, + L::Columns: BuildOrder + + BuildSelect< + super::species::table, + Mysql, + SqlTypeOfPlaceholder< + L::FieldList, + Mysql, + L::PrimaryKeyIndex, + super::species::table, + Ctx, + >, + >, + &'static L: Identifiable, + Ctx: WundergraphContext + QueryModifier, + Ctx::Connection: Connection, + ::Backend: HasSqlType> + + HasSqlType< + SqlTypeOfPlaceholder< + L::FieldList, + Mysql, + L::PrimaryKeyIndex, + super::species::table, + Ctx, + >, + >, +{ + fn handle_batch_insert( + selection: Option<&'_ [Selection<'_, WundergraphScalarValue>]>, + executor: &Executor<'_, Ctx, WundergraphScalarValue>, + batch: Vec, + ) -> ExecutionResult { + let r = batch + .into_iter() + .map(|i| handle_i32_pk_insert(selection, executor, i)) + .collect::, _>>()?; + Ok(Value::List(r)) + } +} + +impl HandleInsert for super::heros::table +where + L: LoadingHandler + 'static, + L::FieldList: WundergraphFieldList, + >::Ret: AppearsOnTable, + L::Columns: BuildOrder + + BuildSelect< + super::heros::table, + Mysql, + SqlTypeOfPlaceholder, + >, + &'static L: Identifiable, + Ctx: WundergraphContext + QueryModifier, + Ctx::Connection: Connection, + ::Backend: HasSqlType> + + HasSqlType< + SqlTypeOfPlaceholder, + >, +{ + fn handle_insert( + selection: Option<&'_ [Selection<'_, WundergraphScalarValue>]>, + executor: &Executor<'_, Ctx, WundergraphScalarValue>, + insertable: NewHero, + ) -> ExecutionResult { + handle_i32_pk_insert(selection, executor, insertable) + } +} + +impl HandleBatchInsert for super::heros::table +where + L: LoadingHandler + 'static, + L::FieldList: WundergraphFieldList, + >::Ret: AppearsOnTable, + L::Columns: BuildOrder + + BuildSelect< + super::heros::table, + Mysql, + SqlTypeOfPlaceholder, + >, + &'static L: Identifiable, + Ctx: WundergraphContext + QueryModifier, + Ctx::Connection: Connection, + ::Backend: HasSqlType> + + HasSqlType< + SqlTypeOfPlaceholder, + >, +{ + fn handle_batch_insert( + selection: Option<&'_ [Selection<'_, WundergraphScalarValue>]>, + executor: &Executor<'_, Ctx, WundergraphScalarValue>, + batch: Vec, + ) -> ExecutionResult { + let r = batch + .into_iter() + .map(|i| handle_i32_pk_insert(selection, executor, i)) + .collect::, _>>()?; + Ok(Value::List(r)) + } +} + +fn handle_single_friend_insert( + selection: Option<&'_ [Selection<'_, WundergraphScalarValue>]>, + executor: &Executor<'_, Ctx, WundergraphScalarValue>, + insertable: NewFriend, +) -> ExecutionResult +where + L: LoadingHandler + 'static, + L::FieldList: WundergraphFieldList, + >::Ret: AppearsOnTable, + L::Columns: BuildOrder + + BuildSelect< + super::friends::table, + Mysql, + SqlTypeOfPlaceholder< + L::FieldList, + Mysql, + L::PrimaryKeyIndex, + super::friends::table, + Ctx, + >, + >, + &'static L: Identifiable, + Ctx: WundergraphContext + QueryModifier, + Ctx::Connection: Connection, + ::Backend: HasSqlType> + + HasSqlType< + SqlTypeOfPlaceholder< + L::FieldList, + Mysql, + L::PrimaryKeyIndex, + super::friends::table, + Ctx, + >, + >, +{ + let ctx = executor.context(); + let conn = ctx.get_connection(); + let look_ahead = executor.look_ahead(); + insertable + .insert_into(super::friends::table) + .execute(conn) + .unwrap(); + let q = L::build_query(&[], &look_ahead)?; + let q = FilterDsl::filter(q, super::friends::friend_id.eq_all(insertable.friend_id)); + let q = FilterDsl::filter(q, super::friends::hero_id.eq_all(insertable.hero_id)); + let items = L::load(&look_ahead, selection, executor, q)?; + Ok(items.into_iter().next().unwrap_or(Value::Null)) +} + +impl HandleInsert for super::friends::table +where + L: LoadingHandler + 'static, + L::FieldList: WundergraphFieldList, + >::Ret: AppearsOnTable, + L::Columns: BuildOrder + + BuildSelect< + super::friends::table, + Mysql, + SqlTypeOfPlaceholder< + L::FieldList, + Mysql, + L::PrimaryKeyIndex, + super::friends::table, + Ctx, + >, + >, + &'static L: Identifiable, + Ctx: WundergraphContext + QueryModifier, + Ctx::Connection: Connection, + ::Backend: HasSqlType> + + HasSqlType< + SqlTypeOfPlaceholder< + L::FieldList, + Mysql, + L::PrimaryKeyIndex, + super::friends::table, + Ctx, + >, + >, +{ + fn handle_insert( + selection: Option<&'_ [Selection<'_, WundergraphScalarValue>]>, + executor: &Executor<'_, Ctx, WundergraphScalarValue>, + insertable: NewFriend, + ) -> ExecutionResult { + handle_single_friend_insert(selection, executor, insertable) + } +} + +impl HandleBatchInsert for super::friends::table +where + L: LoadingHandler + 'static, + L::FieldList: WundergraphFieldList, + >::Ret: AppearsOnTable, + L::Columns: BuildOrder + + BuildSelect< + super::friends::table, + Mysql, + SqlTypeOfPlaceholder< + L::FieldList, + Mysql, + L::PrimaryKeyIndex, + super::friends::table, + Ctx, + >, + >, + &'static L: Identifiable, + Ctx: WundergraphContext + QueryModifier, + Ctx::Connection: Connection, + ::Backend: HasSqlType> + + HasSqlType< + SqlTypeOfPlaceholder< + L::FieldList, + Mysql, + L::PrimaryKeyIndex, + super::friends::table, + Ctx, + >, + >, +{ + fn handle_batch_insert( + selection: Option<&'_ [Selection<'_, WundergraphScalarValue>]>, + executor: &Executor<'_, Ctx, WundergraphScalarValue>, + batch: Vec, + ) -> ExecutionResult { + let r = batch + .into_iter() + .map(|i| handle_single_friend_insert(selection, executor, i)) + .collect::, _>>()?; + Ok(Value::List(r)) + } +} + +fn handle_single_appears_in_insert( + selection: Option<&'_ [Selection<'_, WundergraphScalarValue>]>, + executor: &Executor<'_, Ctx, WundergraphScalarValue>, + insertable: NewAppearsIn, +) -> ExecutionResult +where + L: LoadingHandler + 'static, + L::FieldList: WundergraphFieldList, + >::Ret: AppearsOnTable, + L::Columns: BuildOrder + + BuildSelect< + super::appears_in::table, + Mysql, + SqlTypeOfPlaceholder< + L::FieldList, + Mysql, + L::PrimaryKeyIndex, + super::appears_in::table, + Ctx, + >, + >, + &'static L: Identifiable, + Ctx: WundergraphContext + QueryModifier, + Ctx::Connection: Connection, + ::Backend: HasSqlType> + + HasSqlType< + SqlTypeOfPlaceholder< + L::FieldList, + Mysql, + L::PrimaryKeyIndex, + super::appears_in::table, + Ctx, + >, + >, +{ + let ctx = executor.context(); + let conn = ctx.get_connection(); + let look_ahead = executor.look_ahead(); + insertable + .insert_into(super::appears_in::table) + .execute(conn) + .unwrap(); + let q = L::build_query(&[], &look_ahead)?; + let q = FilterDsl::filter(q, super::appears_in::episode.eq_all(insertable.episode)); + let q = FilterDsl::filter(q, super::appears_in::hero_id.eq_all(insertable.hero_id)); + let items = L::load(&look_ahead, selection, executor, q)?; + Ok(items.into_iter().next().unwrap_or(Value::Null)) +} + +impl HandleInsert for super::appears_in::table +where + L: LoadingHandler + 'static, + L::FieldList: WundergraphFieldList, + >::Ret: AppearsOnTable, + L::Columns: BuildOrder + + BuildSelect< + super::appears_in::table, + Mysql, + SqlTypeOfPlaceholder< + L::FieldList, + Mysql, + L::PrimaryKeyIndex, + super::appears_in::table, + Ctx, + >, + >, + &'static L: Identifiable, + Ctx: WundergraphContext + QueryModifier, + Ctx::Connection: Connection, + ::Backend: HasSqlType> + + HasSqlType< + SqlTypeOfPlaceholder< + L::FieldList, + Mysql, + L::PrimaryKeyIndex, + super::appears_in::table, + Ctx, + >, + >, +{ + fn handle_insert( + selection: Option<&'_ [Selection<'_, WundergraphScalarValue>]>, + executor: &Executor<'_, Ctx, WundergraphScalarValue>, + insertable: NewAppearsIn, + ) -> ExecutionResult { + handle_single_appears_in_insert(selection, executor, insertable) + } +} + +impl HandleBatchInsert for super::appears_in::table +where + L: LoadingHandler + 'static, + L::FieldList: WundergraphFieldList, + >::Ret: AppearsOnTable, + L::Columns: BuildOrder + + BuildSelect< + super::appears_in::table, + Mysql, + SqlTypeOfPlaceholder< + L::FieldList, + Mysql, + L::PrimaryKeyIndex, + super::appears_in::table, + Ctx, + >, + >, + &'static L: Identifiable, + Ctx: WundergraphContext + QueryModifier, + Ctx::Connection: Connection, + ::Backend: HasSqlType> + + HasSqlType< + SqlTypeOfPlaceholder< + L::FieldList, + Mysql, + L::PrimaryKeyIndex, + super::appears_in::table, + Ctx, + >, + >, +{ + fn handle_batch_insert( + selection: Option<&'_ [Selection<'_, WundergraphScalarValue>]>, + executor: &Executor<'_, Ctx, WundergraphScalarValue>, + batch: Vec, + ) -> ExecutionResult { + let r = batch + .into_iter() + .map(|i| handle_single_appears_in_insert(selection, executor, i)) + .collect::, _>>()?; + Ok(Value::List(r)) + } +} From 4ed4ca7dfebea9d08848556536434c3cec14fe42 Mon Sep 17 00:00:00 2001 From: Alexei Pastuchov Date: Mon, 15 Jun 2020 09:06:52 +0200 Subject: [PATCH 19/19] wundergraph_cli supports MySql --- wundergraph_cli/src/print_schema/mod.rs | 110 ++++- .../src/print_schema/print_helper.rs | 194 +++++++++ ...int_schema__tests__infer_schema@mysql.snap | 403 ++++++++++++++++++ 3 files changed, 704 insertions(+), 3 deletions(-) create mode 100644 wundergraph_cli/src/print_schema/snapshots/wundergraph_cli__print_schema__tests__infer_schema@mysql.snap diff --git a/wundergraph_cli/src/print_schema/mod.rs b/wundergraph_cli/src/print_schema/mod.rs index e906a06..c0e01ee 100644 --- a/wundergraph_cli/src/print_schema/mod.rs +++ b/wundergraph_cli/src/print_schema/mod.rs @@ -39,6 +39,45 @@ pub fn print( )?; writeln!(out, "use wundergraph::scalar::WundergraphScalarValue;")?; writeln!(out, "use wundergraph::WundergraphEntity;")?; + if cfg!(feature = "mysql") { + writeln!(out, "use diesel::dsl::SqlTypeOf;")?; + writeln!(out, "use diesel::mysql::Mysql;")?; + writeln!(out, "use diesel::query_dsl::methods::FilterDsl;")?; + writeln!(out, "use diesel::sql_types::{{Bigint, HasSqlType}};")?; + writeln!(out, "use diesel::{{")?; + writeln!(out, " no_arg_sql_function, AppearsOnTable, Connection, EqAll, Identifiable, Insertable, RunQueryDsl,")?; + writeln!(out, "}};")?; + writeln!( + out, + "use juniper::{{ExecutionResult, Executor, Selection, Value}};" + )?; + writeln!(out, "use std::convert::TryFrom;")?; + writeln!( + out, + "use wundergraph::query_builder::mutations::{{HandleBatchInsert, HandleInsert}};" + )?; + writeln!( + out, + "use wundergraph::query_builder::selection::fields::WundergraphFieldList;" + )?; + writeln!( + out, + "use wundergraph::query_builder::selection::filter::BuildFilter;" + )?; + writeln!( + out, + "use wundergraph::query_builder::selection::order::BuildOrder;" + )?; + writeln!( + out, + "use wundergraph::query_builder::selection::select::BuildSelect;" + )?; + writeln!(out, "use wundergraph::query_builder::selection::{{LoadingHandler, QueryModifier, SqlTypeOfPlaceholder}};")?; + writeln!(out, "use wundergraph::WundergraphContext;")?; + writeln!(out)?; + writeln!(out, "diesel::no_arg_sql_function!(LAST_INSERT_ID, Bigint);")?; + } + writeln!(out)?; writeln!(out, "{}", definitions)?; writeln!(out)?; @@ -85,6 +124,9 @@ mod tests { #[cfg(feature = "postgres")] const BACKEND: &str = "postgres"; + #[cfg(feature = "mysql")] + const BACKEND: &str = "mysql"; + #[cfg(feature = "sqlite")] const BACKEND: &str = "sqlite"; @@ -125,6 +167,36 @@ mod tests { );"#, ]; + #[cfg(feature = "mysql")] + const MIGRATION: &[&str] = &[ + r#"DROP TABLE IF EXISTS comments;"#, + r#"DROP TABLE IF EXISTS posts;"#, + r#"DROP TABLE IF EXISTS users;"#, + r#"CREATE TABLE users( + id INTEGER NOT NULL AUTO_INCREMENT, + name TEXT NOT NULL, + PRIMARY KEY (`id`) + );"#, + r#"CREATE TABLE posts( + id INTEGER NOT NULL AUTO_INCREMENT, + author INTEGER DEFAULT NULL, + title TEXT NOT NULL, + datetime TIMESTAMP NULL DEFAULT NULL, + content TEXT, + PRIMARY KEY (`id`), + FOREIGN KEY (`author`) REFERENCES `users` (`id`) + );"#, + r#"CREATE TABLE comments( + id INTEGER NOT NULL AUTO_INCREMENT, + post INTEGER DEFAULT NULL, + commenter INTEGER DEFAULT NULL, + content TEXT NOT NULL, + PRIMARY KEY (`id`), + FOREIGN KEY (`post`) REFERENCES `posts` (`id`), + FOREIGN KEY (`commenter`) REFERENCES `users` (`id`) + );"#, + ]; + fn setup_simple_schema(conn: &InferConnection) { use diesel::prelude::*; use diesel::sql_query; @@ -141,6 +213,12 @@ mod tests { sql_query(*m).execute(conn).unwrap(); } } + #[cfg(feature = "mysql")] + InferConnection::Mysql(conn) => { + for m in MIGRATION { + sql_query(*m).execute(conn).unwrap(); + } + } } } @@ -153,7 +231,7 @@ mod tests { #[cfg(feature = "postgres")] print(&conn, Some("infer_test"), &mut out).unwrap(); - #[cfg(feature = "sqlite")] + #[cfg(any(feature = "mysql", feature = "sqlite"))] print(&conn, None, &mut out).unwrap(); let s = String::from_utf8(out).unwrap(); @@ -187,7 +265,7 @@ mod tests { let mut api_file = File::create(api).unwrap(); #[cfg(feature = "postgres")] print(&conn, Some("infer_test"), &mut api_file).unwrap(); - #[cfg(feature = "sqlite")] + #[cfg(any(feature = "mysql", feature = "sqlite"))] print(&conn, None, &mut api_file).unwrap(); let main = tmp_dir @@ -224,6 +302,17 @@ mod tests { ) .unwrap(); + #[cfg(feature = "mysql")] + write!( + main_file, + include_str!("template_main.rs"), + conn = "MysqlConnection", + db_url = std::env::var("DATABASE_URL").unwrap(), + migrations = migrations, + listen_url = listen_url + ) + .unwrap(); + let cargo_toml = tmp_dir.path().join("wundergraph_roundtrip_test/Cargo.toml"); let mut cargo_toml_file = std::fs::OpenOptions::new() .write(true) @@ -269,6 +358,21 @@ mod tests { ) .unwrap(); } + #[cfg(feature = "mysql")] + { + writeln!( + cargo_toml_file, + r#"diesel = {{version = "1.4", features = ["mysql", "chrono"]}}"# + ) + .unwrap(); + + writeln!( + cargo_toml_file, + "wundergraph = {{path = \"{}\", features = [\"mysql\", \"chrono\"] }}", + wundergraph_dir + ) + .unwrap(); + } writeln!(cargo_toml_file, r#"juniper = "0.14""#).unwrap(); writeln!(cargo_toml_file, r#"failure = "0.1""#).unwrap(); writeln!(cargo_toml_file, r#"actix-web = "1""#).unwrap(); @@ -316,7 +420,7 @@ mod tests { println!("Started server"); let client = reqwest::Client::new(); - std::thread::sleep(std::time::Duration::from_secs(1)); + std::thread::sleep(std::time::Duration::from_secs(5)); let query = "{\"query\": \"{ Users { id name } } \"}"; let mutation = r#"{"query":"mutation CreateUser {\n CreateUser(NewUser: {name: \"Max\"}) {\n id\n name\n }\n}","variables":null,"operationName":"CreateUser"}"#; diff --git a/wundergraph_cli/src/print_schema/print_helper.rs b/wundergraph_cli/src/print_schema/print_helper.rs index 16636a3..c49e475 100644 --- a/wundergraph_cli/src/print_schema/print_helper.rs +++ b/wundergraph_cli/src/print_schema/print_helper.rs @@ -494,6 +494,200 @@ impl<'a> Display for GraphqlInsertable<'a> { } } writeln!(f, "}}")?; + if cfg!(feature = "mysql") && self.table.primary_key.iter().len() == 1 { + let mut out = PadAdapter::new(f); + writeln!(out)?; + // FIXME ensure type of id is appropriate for i32 + let id = self.table.primary_key.iter().next().unwrap(); + let table_name = &self.table.name.name; + + writeln!( + out, + "impl HandleInsert for {}::table", + fix_table_name(&self.table.name.name), + &table_name + )?; + writeln!(out, "where")?; + writeln!( + out, + " L: LoadingHandler + 'static,", + &table_name + )?; + writeln!(out, " L::FieldList: WundergraphFieldList,", &table_name)?; + writeln!( + out, + " >::Ret: AppearsOnTable<{}::table>,", + &table_name + )?; + writeln!( + out, + " L::Columns: BuildOrder<{}::table, Mysql>", + &table_name + )?; + writeln!(out, " + BuildSelect<")?; + writeln!(out, " {}::table,", &table_name)?; + writeln!(out, " Mysql,")?; + writeln!(out, " SqlTypeOfPlaceholder,", &table_name)?; + writeln!(out, " >,")?; + writeln!(out, " &'static L: Identifiable,")?; + writeln!( + out, + " Ctx: WundergraphContext + QueryModifier," + )?; + writeln!(out, " Ctx::Connection: Connection,")?; + writeln!( + out, + " ::Backend: HasSqlType>", + &table_name + )?; + writeln!(out, " + HasSqlType>,", table_name)?; + writeln!(out, "{{")?; + writeln!(out, " fn handle_insert(")?; + writeln!( + out, + " selection: Option<&'_ [Selection<'_, WundergraphScalarValue>]>," + )?; + writeln!( + out, + " executor: &Executor<'_, Ctx, WundergraphScalarValue>," + )?; + writeln!( + out, + " insertable: New{},", + fix_table_name(&self.table.name.name) + )?; + writeln!(out, " ) -> ExecutionResult {{")?; + writeln!(out, " let ctx = executor.context();")?; + writeln!(out, " let conn = ctx.get_connection();")?; + writeln!(out, " let look_ahead = executor.look_ahead();")?; + writeln!( + out, + " insertable.insert_into({}::table).execute(conn).unwrap();", + &table_name + )?; + writeln!( + out, + " let last_insert_id: i64 = diesel::select(LAST_INSERT_ID).first(conn)?;" + )?; + writeln!( + out, + " let last_insert_id = i32::try_from(last_insert_id)?;" + )?; + writeln!(out, " let q = L::build_query(&[], &look_ahead)?;")?; + writeln!( + out, + " let q = FilterDsl::filter(q, {}::{}.eq_all(last_insert_id));", + &table_name, &id + )?; + writeln!( + out, + " let items = L::load(&look_ahead, selection, executor, q)?;" + )?; + writeln!( + out, + " Ok(items.into_iter().next().unwrap_or(Value::Null))" + )?; + writeln!(out, " }}")?; + writeln!(out, "}}")?; + writeln!(out)?; + + writeln!( + out, + "impl HandleBatchInsert for {}::table", + fix_table_name(&self.table.name.name), + &table_name + )?; + writeln!(out, "where")?; + writeln!( + out, + " L: LoadingHandler + 'static,", + &table_name + )?; + writeln!(out, " L::FieldList: WundergraphFieldList,", &table_name)?; + writeln!( + out, + " >::Ret: AppearsOnTable<{}::table>,", + &table_name + )?; + writeln!( + out, + " L::Columns: BuildOrder<{}::table, Mysql>", + &table_name + )?; + writeln!(out, " + BuildSelect<")?; + writeln!(out, " {}::table,", &table_name)?; + writeln!(out, " Mysql,")?; + writeln!(out, " SqlTypeOfPlaceholder,", &table_name)?; + writeln!(out, " >,")?; + writeln!(out, " &'static L: Identifiable,")?; + writeln!( + out, + " Ctx: WundergraphContext + QueryModifier," + )?; + writeln!(out, " Ctx::Connection: Connection,")?; + writeln!( + out, + " ::Backend: HasSqlType>", + &table_name + )?; + writeln!(out, " + HasSqlType>,", table_name)?; + writeln!(out, "{{")?; + writeln!(out, " fn handle_batch_insert(")?; + writeln!( + out, + " selection: Option<&'_ [Selection<'_, WundergraphScalarValue>]>," + )?; + writeln!( + out, + " executor: &Executor<'_, Ctx, WundergraphScalarValue>," + )?; + writeln!( + out, + " batch: Vec,", + fix_table_name(&self.table.name.name) + )?; + writeln!(out, " ) -> ExecutionResult {{")?; + writeln!(out, " let ctx = executor.context();")?; + writeln!(out, " let conn = ctx.get_connection();")?; + writeln!(out, " let look_ahead = executor.look_ahead();")?; + writeln!(out, " let single_insert = |insertable: New{}| -> ExecutionResult {{", fix_table_name(&self.table.name.name))?; + writeln!( + out, + " insertable.insert_into({}::table).execute(conn).unwrap();", + &table_name + )?; + writeln!(out, " let last_insert_id: i64 = diesel::select(LAST_INSERT_ID).first(conn)?;")?; + writeln!( + out, + " let last_insert_id = i32::try_from(last_insert_id)?;" + )?; + writeln!( + out, + " let q = L::build_query(&[], &look_ahead)?;" + )?; + writeln!( + out, + " let q = FilterDsl::filter(q, {}::{}.eq_all(last_insert_id));", + &table_name, &id + )?; + writeln!( + out, + " let items = L::load(&look_ahead, selection, executor, q)?;" + )?; + writeln!( + out, + " Ok(items.into_iter().next().unwrap_or(Value::Null))" + )?; + writeln!(out, " }};")?; + writeln!(out, " let r = batch")?; + writeln!(out, " .into_iter()")?; + writeln!(out, " .map(|i| single_insert(i))")?; + writeln!(out, " .collect::, _>>()?;")?; + writeln!(out, " Ok(Value::List(r))")?; + writeln!(out, " }}")?; + writeln!(out, "}}")?; + writeln!(out)?; + } Ok(()) } } diff --git a/wundergraph_cli/src/print_schema/snapshots/wundergraph_cli__print_schema__tests__infer_schema@mysql.snap b/wundergraph_cli/src/print_schema/snapshots/wundergraph_cli__print_schema__tests__infer_schema@mysql.snap new file mode 100644 index 0000000..51681a7 --- /dev/null +++ b/wundergraph_cli/src/print_schema/snapshots/wundergraph_cli__print_schema__tests__infer_schema@mysql.snap @@ -0,0 +1,403 @@ +--- +source: wundergraph_cli/src/print_schema/mod.rs +expression: "&s" +--- +use wundergraph::query_builder::types::{HasMany, HasOne}; +use wundergraph::scalar::WundergraphScalarValue; +use wundergraph::WundergraphEntity; +use diesel::dsl::SqlTypeOf; +use diesel::mysql::Mysql; +use diesel::query_dsl::methods::FilterDsl; +use diesel::sql_types::{Bigint, HasSqlType}; +use diesel::{ + no_arg_sql_function, AppearsOnTable, Connection, EqAll, Identifiable, Insertable, RunQueryDsl, +}; +use juniper::{ExecutionResult, Executor, Selection, Value}; +use std::convert::TryFrom; +use wundergraph::query_builder::mutations::{HandleBatchInsert, HandleInsert}; +use wundergraph::query_builder::selection::fields::WundergraphFieldList; +use wundergraph::query_builder::selection::filter::BuildFilter; +use wundergraph::query_builder::selection::order::BuildOrder; +use wundergraph::query_builder::selection::select::BuildSelect; +use wundergraph::query_builder::selection::{LoadingHandler, QueryModifier, SqlTypeOfPlaceholder}; +use wundergraph::WundergraphContext; + +diesel::no_arg_sql_function!(LAST_INSERT_ID, Bigint); + +table! { + comments (id) { + id -> Integer, + post -> Nullable, + commenter -> Nullable, + content -> Text, + } +} + +table! { + posts (id) { + id -> Integer, + author -> Nullable, + title -> Text, + datetime -> Nullable, + content -> Nullable, + } +} + +table! { + users (id) { + id -> Integer, + name -> Text, + } +} + +allow_tables_to_appear_in_same_query!( + comments, + posts, + users, +); + + +#[derive(Clone, Debug, Identifiable, WundergraphEntity)] +#[table_name = "comments"] +#[primary_key(id)] +pub struct Comment { + id: i32, + post: Option>, + commenter: Option>, + content: String, +} + +#[derive(Clone, Debug, Identifiable, WundergraphEntity)] +#[table_name = "posts"] +#[primary_key(id)] +pub struct Post { + id: i32, + author: Option>, + title: String, + datetime: Option, + content: Option, + comments: HasMany, +} + +#[derive(Clone, Debug, Identifiable, WundergraphEntity)] +#[table_name = "users"] +#[primary_key(id)] +pub struct User { + id: i32, + name: String, + comments: HasMany, + posts: HasMany, +} + + + +wundergraph::query_object!{ + Query { + Comment, + Post, + User, + } +} + + +#[derive(Insertable, juniper::GraphQLInputObject, Clone, Debug)] +#[graphql(scalar = "WundergraphScalarValue")] +#[table_name = "comments"] +pub struct NewComment { + post: Option, + commenter: Option, + content: String, +} + + impl HandleInsert for comments::table + where + L: LoadingHandler + 'static, + L::FieldList: WundergraphFieldList, + >::Ret: AppearsOnTable, + L::Columns: BuildOrder + + BuildSelect< + comments::table, + Mysql, + SqlTypeOfPlaceholder, + >, + &'static L: Identifiable, + Ctx: WundergraphContext + QueryModifier, + Ctx::Connection: Connection, + ::Backend: HasSqlType> + + HasSqlType>, + { + fn handle_insert( + selection: Option<&'_ [Selection<'_, WundergraphScalarValue>]>, + executor: &Executor<'_, Ctx, WundergraphScalarValue>, + insertable: NewComment, + ) -> ExecutionResult { + let ctx = executor.context(); + let conn = ctx.get_connection(); + let look_ahead = executor.look_ahead(); + insertable.insert_into(comments::table).execute(conn).unwrap(); + let last_insert_id: i64 = diesel::select(LAST_INSERT_ID).first(conn)?; + let last_insert_id = i32::try_from(last_insert_id)?; + let q = L::build_query(&[], &look_ahead)?; + let q = FilterDsl::filter(q, comments::id.eq_all(last_insert_id)); + let items = L::load(&look_ahead, selection, executor, q)?; + Ok(items.into_iter().next().unwrap_or(Value::Null)) + } + } + + impl HandleBatchInsert for comments::table + where + L: LoadingHandler + 'static, + L::FieldList: WundergraphFieldList, + >::Ret: AppearsOnTable, + L::Columns: BuildOrder + + BuildSelect< + comments::table, + Mysql, + SqlTypeOfPlaceholder, + >, + &'static L: Identifiable, + Ctx: WundergraphContext + QueryModifier, + Ctx::Connection: Connection, + ::Backend: HasSqlType> + + HasSqlType>, + { + fn handle_batch_insert( + selection: Option<&'_ [Selection<'_, WundergraphScalarValue>]>, + executor: &Executor<'_, Ctx, WundergraphScalarValue>, + batch: Vec, + ) -> ExecutionResult { + let ctx = executor.context(); + let conn = ctx.get_connection(); + let look_ahead = executor.look_ahead(); + let single_insert = |insertable: NewComment| -> ExecutionResult { + insertable.insert_into(comments::table).execute(conn).unwrap(); + let last_insert_id: i64 = diesel::select(LAST_INSERT_ID).first(conn)?; + let last_insert_id = i32::try_from(last_insert_id)?; + let q = L::build_query(&[], &look_ahead)?; + let q = FilterDsl::filter(q, comments::id.eq_all(last_insert_id)); + let items = L::load(&look_ahead, selection, executor, q)?; + Ok(items.into_iter().next().unwrap_or(Value::Null)) + }; + let r = batch + .into_iter() + .map(|i| single_insert(i)) + .collect::, _>>()?; + Ok(Value::List(r)) + } + } + + +#[derive(AsChangeset, Identifiable, juniper::GraphQLInputObject, Clone, Debug)] +#[graphql(scalar = "WundergraphScalarValue")] +#[table_name = "comments"] +#[primary_key(id)] +pub struct CommentChangeset { + id: i32, + post: Option, + commenter: Option, + content: String, +} + +#[derive(Insertable, juniper::GraphQLInputObject, Clone, Debug)] +#[graphql(scalar = "WundergraphScalarValue")] +#[table_name = "posts"] +pub struct NewPost { + author: Option, + title: String, + datetime: Option, + content: Option, +} + + impl HandleInsert for posts::table + where + L: LoadingHandler + 'static, + L::FieldList: WundergraphFieldList, + >::Ret: AppearsOnTable, + L::Columns: BuildOrder + + BuildSelect< + posts::table, + Mysql, + SqlTypeOfPlaceholder, + >, + &'static L: Identifiable, + Ctx: WundergraphContext + QueryModifier, + Ctx::Connection: Connection, + ::Backend: HasSqlType> + + HasSqlType>, + { + fn handle_insert( + selection: Option<&'_ [Selection<'_, WundergraphScalarValue>]>, + executor: &Executor<'_, Ctx, WundergraphScalarValue>, + insertable: NewPost, + ) -> ExecutionResult { + let ctx = executor.context(); + let conn = ctx.get_connection(); + let look_ahead = executor.look_ahead(); + insertable.insert_into(posts::table).execute(conn).unwrap(); + let last_insert_id: i64 = diesel::select(LAST_INSERT_ID).first(conn)?; + let last_insert_id = i32::try_from(last_insert_id)?; + let q = L::build_query(&[], &look_ahead)?; + let q = FilterDsl::filter(q, posts::id.eq_all(last_insert_id)); + let items = L::load(&look_ahead, selection, executor, q)?; + Ok(items.into_iter().next().unwrap_or(Value::Null)) + } + } + + impl HandleBatchInsert for posts::table + where + L: LoadingHandler + 'static, + L::FieldList: WundergraphFieldList, + >::Ret: AppearsOnTable, + L::Columns: BuildOrder + + BuildSelect< + posts::table, + Mysql, + SqlTypeOfPlaceholder, + >, + &'static L: Identifiable, + Ctx: WundergraphContext + QueryModifier, + Ctx::Connection: Connection, + ::Backend: HasSqlType> + + HasSqlType>, + { + fn handle_batch_insert( + selection: Option<&'_ [Selection<'_, WundergraphScalarValue>]>, + executor: &Executor<'_, Ctx, WundergraphScalarValue>, + batch: Vec, + ) -> ExecutionResult { + let ctx = executor.context(); + let conn = ctx.get_connection(); + let look_ahead = executor.look_ahead(); + let single_insert = |insertable: NewPost| -> ExecutionResult { + insertable.insert_into(posts::table).execute(conn).unwrap(); + let last_insert_id: i64 = diesel::select(LAST_INSERT_ID).first(conn)?; + let last_insert_id = i32::try_from(last_insert_id)?; + let q = L::build_query(&[], &look_ahead)?; + let q = FilterDsl::filter(q, posts::id.eq_all(last_insert_id)); + let items = L::load(&look_ahead, selection, executor, q)?; + Ok(items.into_iter().next().unwrap_or(Value::Null)) + }; + let r = batch + .into_iter() + .map(|i| single_insert(i)) + .collect::, _>>()?; + Ok(Value::List(r)) + } + } + + +#[derive(AsChangeset, Identifiable, juniper::GraphQLInputObject, Clone, Debug)] +#[graphql(scalar = "WundergraphScalarValue")] +#[table_name = "posts"] +#[primary_key(id)] +pub struct PostChangeset { + id: i32, + author: Option, + title: String, + datetime: Option, + content: Option, +} + +#[derive(Insertable, juniper::GraphQLInputObject, Clone, Debug)] +#[graphql(scalar = "WundergraphScalarValue")] +#[table_name = "users"] +pub struct NewUser { + name: String, +} + + impl HandleInsert for users::table + where + L: LoadingHandler + 'static, + L::FieldList: WundergraphFieldList, + >::Ret: AppearsOnTable, + L::Columns: BuildOrder + + BuildSelect< + users::table, + Mysql, + SqlTypeOfPlaceholder, + >, + &'static L: Identifiable, + Ctx: WundergraphContext + QueryModifier, + Ctx::Connection: Connection, + ::Backend: HasSqlType> + + HasSqlType>, + { + fn handle_insert( + selection: Option<&'_ [Selection<'_, WundergraphScalarValue>]>, + executor: &Executor<'_, Ctx, WundergraphScalarValue>, + insertable: NewUser, + ) -> ExecutionResult { + let ctx = executor.context(); + let conn = ctx.get_connection(); + let look_ahead = executor.look_ahead(); + insertable.insert_into(users::table).execute(conn).unwrap(); + let last_insert_id: i64 = diesel::select(LAST_INSERT_ID).first(conn)?; + let last_insert_id = i32::try_from(last_insert_id)?; + let q = L::build_query(&[], &look_ahead)?; + let q = FilterDsl::filter(q, users::id.eq_all(last_insert_id)); + let items = L::load(&look_ahead, selection, executor, q)?; + Ok(items.into_iter().next().unwrap_or(Value::Null)) + } + } + + impl HandleBatchInsert for users::table + where + L: LoadingHandler + 'static, + L::FieldList: WundergraphFieldList, + >::Ret: AppearsOnTable, + L::Columns: BuildOrder + + BuildSelect< + users::table, + Mysql, + SqlTypeOfPlaceholder, + >, + &'static L: Identifiable, + Ctx: WundergraphContext + QueryModifier, + Ctx::Connection: Connection, + ::Backend: HasSqlType> + + HasSqlType>, + { + fn handle_batch_insert( + selection: Option<&'_ [Selection<'_, WundergraphScalarValue>]>, + executor: &Executor<'_, Ctx, WundergraphScalarValue>, + batch: Vec, + ) -> ExecutionResult { + let ctx = executor.context(); + let conn = ctx.get_connection(); + let look_ahead = executor.look_ahead(); + let single_insert = |insertable: NewUser| -> ExecutionResult { + insertable.insert_into(users::table).execute(conn).unwrap(); + let last_insert_id: i64 = diesel::select(LAST_INSERT_ID).first(conn)?; + let last_insert_id = i32::try_from(last_insert_id)?; + let q = L::build_query(&[], &look_ahead)?; + let q = FilterDsl::filter(q, users::id.eq_all(last_insert_id)); + let items = L::load(&look_ahead, selection, executor, q)?; + Ok(items.into_iter().next().unwrap_or(Value::Null)) + }; + let r = batch + .into_iter() + .map(|i| single_insert(i)) + .collect::, _>>()?; + Ok(Value::List(r)) + } + } + + +#[derive(AsChangeset, Identifiable, juniper::GraphQLInputObject, Clone, Debug)] +#[graphql(scalar = "WundergraphScalarValue")] +#[table_name = "users"] +#[primary_key(id)] +pub struct UserChangeset { + id: i32, + name: String, +} + +wundergraph::mutation_object!{ + Mutation{ + Comment(insert = NewComment, update = CommentChangeset, ), + Post(insert = NewPost, update = PostChangeset, ), + User(insert = NewUser, update = UserChangeset, ), + } +} + +