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/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..7b55531 --- /dev/null +++ b/wundergraph/src/query_builder/mutations/insert/mysql.rs @@ -0,0 +1,148 @@ +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::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, EqAll, Identifiable}; +use diesel::{AppearsOnTable, Connection, Insertable, RunQueryDsl, Table}; +use juniper::{ExecutionResult, Executor, Selection, Value}; +use std::convert::TryFrom; + +// 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 { + handle_single_insert(selection, executor, insertable) + } +} + +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, + 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_batch_insert( + selection: Option<&'_ [Selection<'_, WundergraphScalarValue>]>, + executor: &Executor<'_, Ctx, WundergraphScalarValue>, + batch: Vec, + ) -> ExecutionResult { + let r = batch + .into_iter() + .map(|i| handle_single_insert(selection, executor, i)) + .collect::, _>>()?; + Ok(Value::List(r)) + } +} + +fn handle_single_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)) +} diff --git a/wundergraph/src/query_builder/selection/offset.rs b/wundergraph/src/query_builder/selection/offset.rs index 0fb0760..b782b07 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,36 @@ 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 { + // 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 { + Ok(query) + } + } +} 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_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") { diff --git a/wundergraph_cli/src/print_schema/mod.rs b/wundergraph_cli/src/print_schema/mod.rs index e906a06..16c4e2c 100644 --- a/wundergraph_cli/src/print_schema/mod.rs +++ b/wundergraph_cli/src/print_schema/mod.rs @@ -85,6 +85,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 +128,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 +174,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 +192,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 +226,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 +263,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 +319,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 +381,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/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..76c8a6c --- /dev/null +++ b/wundergraph_cli/src/print_schema/snapshots/wundergraph_cli__print_schema__tests__infer_schema@mysql.snap @@ -0,0 +1,151 @@ +--- +source: wundergraph_cli/src/print_schema/mod.rs +expression: "&s" +--- +use wundergraph::query_builder::types::{HasMany, HasOne}; +use wundergraph::scalar::WundergraphScalarValue; +use wundergraph::WundergraphEntity; + +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, +} + +#[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, +} + +#[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, +} + +#[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, ), + } +} + + 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 = [] 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::, _>>() 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 }, )) } diff --git a/wundergraph_derive/src/wundergraph_entity.rs b/wundergraph_derive/src/wundergraph_entity.rs index 759274a..049c5e6 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,16 @@ 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 +81,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)* }, diff --git a/wundergraph_example/Cargo.toml b/wundergraph_example/Cargo.toml index f21fa61..a254ebf 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" @@ -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..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/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..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 = diff --git a/wundergraph_example/src/mutations.rs b/wundergraph_example/src/mutations.rs index bd40cf8..11a2561 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 { @@ -56,15 +59,17 @@ 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"] +#[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, diff --git a/wundergraph_example/src/mutations/mysql.rs b/wundergraph_example/src/mutations/mysql.rs new file mode 100644 index 0000000..541fc72 --- /dev/null +++ b/wundergraph_example/src/mutations/mysql.rs @@ -0,0 +1,184 @@ +use crate::mutations::{AppearsIn, Friend, NewAppearsIn, NewFriend}; +use diesel::prelude::*; +use juniper::{ExecutionResult, Executor, Selection, Value}; +use wundergraph::query_builder::mutations::{HandleBatchInsert, HandleInsert}; +use wundergraph::query_builder::selection::LoadingHandler; +use wundergraph::{QueryModifier, WundergraphContext}; + +impl HandleInsert for super::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(super::friends::table) + .values(( + super::friends::hero_id.eq(insertable.hero_id), + super::friends::friend_id.eq(insertable.friend_id), + )) + .execute(conn)?; + + let query = >::build_query( + &[], + &look_ahead, + )? + .filter( + super::friends::hero_id + .eq(insertable.hero_id) + .and(super::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 super::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 }| { + ( + super::friends::hero_id.eq(hero_id), + super::friends::friend_id.eq(friend_id), + ) + }) + .collect::>(); + diesel::insert_into(super::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( + super::friends::hero_id + .eq(hero_id) + .and(super::friends::friend_id.eq(friend_id)), + ) + } + + let items = Friend::load(&look_ahead, selection, executor, query)?; + Ok(Value::list(items)) + }) + } +} + +impl HandleInsert + for super::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(super::appears_in::table) + .values(( + super::appears_in::hero_id.eq(insertable.hero_id), + super::appears_in::episode.eq(insertable.episode), + )) + .execute(conn)?; + + let query = >::build_query( + &[], + &look_ahead, + )? + .filter( + super::appears_in::hero_id + .eq(insertable.hero_id) + .and(super::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 super::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 }| { + ( + super::appears_in::hero_id.eq(hero_id), + super::appears_in::episode.eq(episode), + ) + }) + .collect::>(); + diesel::insert_into(super::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( + super::appears_in::hero_id + .eq(hero_id) + .and(super::appears_in::episode.eq(episode)), + ) + } + + let items = AppearsIn::load(&look_ahead, selection, executor, query)?; + Ok(Value::list(items)) + }) + } +}