diff --git a/pom.xml b/pom.xml index a977b39d3..03f3665de 100644 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,8 @@ 0.1.4 2.4.1 42.2.5 + 5.1.47 + 0.9.38 7.1.2.jre8-preview Arabba-M7 1.0.1 @@ -173,6 +175,13 @@ test + + mysql + mysql-connector-java + ${mysql.version} + test + + com.microsoft.sqlserver mssql-jdbc @@ -198,6 +207,13 @@ test + + com.github.jasync-sql + jasync-r2dbc-mysql + ${jasync.version} + test + + de.schauderhaft.degraph degraph-check @@ -295,7 +311,8 @@ ${querydsl} ${spring} ${r2dbc-spi.version} - ${reactive-streams.version} + ${reactive-streams.version} + ${releasetrain} true 3 @@ -416,6 +433,10 @@ spring-libs-snapshot https://repo.spring.io/libs-snapshot + + jcenter + https://jcenter.bintray.com/ + diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/AnonymousBindMarkers.java b/src/main/java/org/springframework/data/r2dbc/dialect/AnonymousBindMarkers.java index d2ef0b13c..32b4cc103 100644 --- a/src/main/java/org/springframework/data/r2dbc/dialect/AnonymousBindMarkers.java +++ b/src/main/java/org/springframework/data/r2dbc/dialect/AnonymousBindMarkers.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/Database.java b/src/main/java/org/springframework/data/r2dbc/dialect/Database.java index b59b9259e..0eb27bf82 100644 --- a/src/main/java/org/springframework/data/r2dbc/dialect/Database.java +++ b/src/main/java/org/springframework/data/r2dbc/dialect/Database.java @@ -53,6 +53,18 @@ public enum Database { public Dialect defaultDialect() { return H2Dialect.INSTANCE; } + }, + + MYSQL { + @Override + public String driverName() { + return "MySQL"; + } + + @Override + public Dialect defaultDialect() { + return MySqlDialect.INSTANCE; + } }; /** diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/IndexedBindMarker.java b/src/main/java/org/springframework/data/r2dbc/dialect/IndexedBindMarker.java index 109d9295a..b7d84d658 100644 --- a/src/main/java/org/springframework/data/r2dbc/dialect/IndexedBindMarker.java +++ b/src/main/java/org/springframework/data/r2dbc/dialect/IndexedBindMarker.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/MySqlDialect.java b/src/main/java/org/springframework/data/r2dbc/dialect/MySqlDialect.java new file mode 100644 index 000000000..d3cd260e0 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/dialect/MySqlDialect.java @@ -0,0 +1,109 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.dialect; + +import java.net.InetAddress; +import java.net.URI; +import java.net.URL; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +/** + * An SQL dialect for MySQL. + * + * @author Mark Paluch + */ +public class MySqlDialect implements Dialect { + + private static final Set> SIMPLE_TYPES = new HashSet<>( + Arrays.asList(UUID.class, URL.class, URI.class, InetAddress.class)); + + /** + * Singleton instance. + */ + public static final MySqlDialect INSTANCE = new MySqlDialect(); + + private static final BindMarkersFactory ANONYMOUS = BindMarkersFactory.anonymous("?"); + + private static final LimitClause LIMIT_CLAUSE = new LimitClause() { + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.dialect.LimitClause#getClause(long, long) + */ + @Override + public String getClause(long limit, long offset) { + return String.format("LIMIT %d,%d", limit, offset); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.dialect.LimitClause#getClause(long) + */ + @Override + public String getClause(long limit) { + return "LIMIT " + limit; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.dialect.LimitClause#getClausePosition() + */ + @Override + public Position getClausePosition() { + return Position.END; + } + }; + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.dialect.Dialect#getBindMarkersFactory() + */ + @Override + public BindMarkersFactory getBindMarkersFactory() { + return ANONYMOUS; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.dialect.Dialect#getSimpleTypesKeys() + */ + @Override + public Collection> getSimpleTypes() { + return SIMPLE_TYPES; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.dialect.Dialect#limit() + */ + @Override + public LimitClause limit() { + return LIMIT_CLAUSE; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.dialect.Dialect#getArraySupport() + */ + @Override + public ArrayColumns getArraySupport() { + return ArrayColumns.Unsupported.INSTANCE; + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/dialect/AnonymousBindMarkersUnitTests.java b/src/test/java/org/springframework/data/r2dbc/dialect/AnonymousBindMarkersUnitTests.java index 112f5b500..663af7525 100644 --- a/src/test/java/org/springframework/data/r2dbc/dialect/AnonymousBindMarkersUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/dialect/AnonymousBindMarkersUnitTests.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java index 102645123..628020f0d 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java @@ -32,6 +32,7 @@ import javax.sql.DataSource; import org.junit.Before; import org.junit.Test; + import org.springframework.dao.DataAccessException; import org.springframework.data.r2dbc.testing.R2dbcIntegrationTestSupport; import org.springframework.jdbc.core.JdbcTemplate; @@ -201,15 +202,21 @@ public abstract class AbstractTransactionalDatabaseClientIntegrationTests extend assertThat(count).isEqualTo(0); } - @Test // gh-2 + @Test // gh-2, gh-75 public void emitTransactionIds() { TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory); Flux transactionIds = databaseClient.inTransaction(db -> { + Mono insert = db.execute().sql(getInsertIntoLegosetStatement()) // + .bind(0, 42055) // + .bind(1, "SCHAUFELRADBAGGER") // + .bindNull(2, Integer.class) // + .fetch().rowsUpdated(); + Flux txId = db.execute().sql(getCurrentTransactionIdStatement()).map((r, md) -> r.get(0)).all(); - return txId.concatWith(txId); + return insert.thenMany(txId.concatWith(txId)); }); transactionIds.collectList().as(StepVerifier::create) // diff --git a/src/test/java/org/springframework/data/r2dbc/function/MySqlDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/MySqlDatabaseClientIntegrationTests.java new file mode 100644 index 000000000..3fc544c83 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/function/MySqlDatabaseClientIntegrationTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function; + +import io.r2dbc.spi.ConnectionFactory; + +import javax.sql.DataSource; + +import org.junit.ClassRule; +import org.junit.Ignore; +import org.junit.Test; + +import org.springframework.data.r2dbc.testing.ExternalDatabase; +import org.springframework.data.r2dbc.testing.MySqlTestSupport; + +/** + * Integration tests for {@link DatabaseClient} against MySQL. + * + * @author Mark Paluch + */ +public class MySqlDatabaseClientIntegrationTests extends AbstractDatabaseClientIntegrationTests { + + @ClassRule public static final ExternalDatabase database = MySqlTestSupport.database(); + + @Override + protected DataSource createDataSource() { + return MySqlTestSupport.createDataSource(database); + } + + @Override + protected ConnectionFactory createConnectionFactory() { + return MySqlTestSupport.createConnectionFactory(database); + } + + @Override + protected String getCreateTableStatement() { + return MySqlTestSupport.CREATE_TABLE_LEGOSET; + } + + @Override + @Ignore("Jasync currently uses its own exceptions, see jasync-sql/jasync-sql#106") + @Test + public void shouldTranslateDuplicateKeyException() {} +} diff --git a/src/test/java/org/springframework/data/r2dbc/function/MySqlTransactionalDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/MySqlTransactionalDatabaseClientIntegrationTests.java new file mode 100644 index 000000000..615e29cf5 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/function/MySqlTransactionalDatabaseClientIntegrationTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function; + +import io.r2dbc.spi.ConnectionFactory; + +import javax.sql.DataSource; + +import org.junit.ClassRule; +import org.junit.Ignore; +import org.junit.Test; + +import org.springframework.data.r2dbc.testing.ExternalDatabase; +import org.springframework.data.r2dbc.testing.MySqlTestSupport; + +/** + * Integration tests for {@link TransactionalDatabaseClient} against MySQL. + * + * @author Mark Paluch + */ +public class MySqlTransactionalDatabaseClientIntegrationTests + extends AbstractTransactionalDatabaseClientIntegrationTests { + + @ClassRule public static final ExternalDatabase database = MySqlTestSupport.database(); + + @Override + protected DataSource createDataSource() { + return MySqlTestSupport.createDataSource(database); + } + + @Override + protected ConnectionFactory createConnectionFactory() { + return MySqlTestSupport.createConnectionFactory(database); + } + + @Override + protected String getCreateTableStatement() { + return MySqlTestSupport.CREATE_TABLE_LEGOSET; + } + + @Override + protected String getCurrentTransactionIdStatement() { + return "SELECT tx.trx_id FROM information_schema.innodb_trx tx WHERE tx.trx_mysql_thread_id = connection_id()"; + } + + @Override + @Test + @Ignore("MySQL creates transactions only on interaction with transactional tables. BEGIN does not create a txid") + public void shouldManageUserTransaction() {} +} diff --git a/src/test/java/org/springframework/data/r2dbc/repository/MySqlR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/MySqlR2dbcRepositoryIntegrationTests.java new file mode 100644 index 000000000..5faf2cd41 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/repository/MySqlR2dbcRepositoryIntegrationTests.java @@ -0,0 +1,99 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.repository; + +import io.r2dbc.spi.ConnectionFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import javax.sql.DataSource; + +import org.junit.ClassRule; +import org.junit.runner.RunWith; + +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; +import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration; +import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories; +import org.springframework.data.r2dbc.repository.query.Query; +import org.springframework.data.r2dbc.repository.support.R2dbcRepositoryFactory; +import org.springframework.data.r2dbc.testing.ExternalDatabase; +import org.springframework.data.r2dbc.testing.MySqlTestSupport; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * Integration tests for {@link LegoSetRepository} using {@link R2dbcRepositoryFactory} against MySQL. + * + * @author Mark Paluch + */ +@RunWith(SpringRunner.class) +@ContextConfiguration +public class MySqlR2dbcRepositoryIntegrationTests extends AbstractR2dbcRepositoryIntegrationTests { + + @ClassRule public static final ExternalDatabase database = MySqlTestSupport.database(); + + @Configuration + @EnableR2dbcRepositories(considerNestedRepositories = true, + includeFilters = @Filter(classes = MySqlLegoSetRepository.class, type = FilterType.ASSIGNABLE_TYPE)) + static class IntegrationTestConfiguration extends AbstractR2dbcConfiguration { + + @Override + public ConnectionFactory connectionFactory() { + return MySqlTestSupport.createConnectionFactory(database); + } + } + + @Override + protected DataSource createDataSource() { + return MySqlTestSupport.createDataSource(database); + } + + @Override + protected ConnectionFactory createConnectionFactory() { + return MySqlTestSupport.createConnectionFactory(database); + } + + @Override + protected String getCreateTableStatement() { + return MySqlTestSupport.CREATE_TABLE_LEGOSET_WITH_ID_GENERATION; + } + + @Override + protected Class getRepositoryInterfaceType() { + return MySqlLegoSetRepository.class; + } + + interface MySqlLegoSetRepository extends LegoSetRepository { + + @Override + @Query("SELECT * FROM legoset WHERE name like ?") + Flux findByNameContains(String name); + + @Override + @Query("SELECT * FROM legoset") + Flux findAsProjection(); + + @Override + @Query("SELECT * FROM legoset WHERE manual = :manual") + Mono findByManual(int manual); + + @Override + @Query("SELECT id FROM legoset") + Flux findAllIds(); + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/testing/MySqlTestSupport.java b/src/test/java/org/springframework/data/r2dbc/testing/MySqlTestSupport.java new file mode 100644 index 000000000..8c13bda57 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/testing/MySqlTestSupport.java @@ -0,0 +1,151 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.testing; + +import io.r2dbc.spi.ConnectionFactory; + +import java.util.function.Supplier; +import java.util.stream.Stream; + +import javax.sql.DataSource; + +import org.springframework.data.r2dbc.testing.ExternalDatabase.ProvidedDatabase; + +import org.testcontainers.containers.MySQLContainer; + +import com.github.jasync.r2dbc.mysql.JasyncConnectionFactory; +import com.github.jasync.sql.db.Configuration; +import com.github.jasync.sql.db.mysql.pool.MySQLConnectionFactory; +import com.mysql.jdbc.jdbc2.optional.MysqlDataSource; + +/** + * Utility class for testing against MySQL. + * + * @author Mark Paluch + */ +public class MySqlTestSupport { + + private static ExternalDatabase testContainerDatabase; + + public static String CREATE_TABLE_LEGOSET = "CREATE TABLE legoset (\n" // + + " id integer PRIMARY KEY,\n" // + + " name varchar(255) NOT NULL,\n" // + + " manual integer NULL\n" // + + ") ENGINE=InnoDB;"; + + public static String CREATE_TABLE_LEGOSET_WITH_ID_GENERATION = "CREATE TABLE legoset (\n" // + + " id integer AUTO_INCREMENT PRIMARY KEY,\n" // + + " name varchar(255) NOT NULL,\n" // + + " manual integer NULL\n" // + + ") ENGINE=InnoDB;"; + + /** + * Returns a database either hosted locally at {@code postgres:@localhost:5432/postgres} or running inside Docker. + * + * @return information about the database. Guaranteed to be not {@literal null}. + */ + public static ExternalDatabase database() { + + if (Boolean.getBoolean("spring.data.r2dbc.test.preferLocalDatabase")) { + + return getFirstWorkingDatabase( // + MySqlTestSupport::local, // + MySqlTestSupport::testContainer // + ); + } else { + + return getFirstWorkingDatabase( // + MySqlTestSupport::testContainer, // + MySqlTestSupport::local // + ); + } + } + + @SafeVarargs + private static ExternalDatabase getFirstWorkingDatabase(Supplier... suppliers) { + + return Stream.of(suppliers).map(Supplier::get) // + .filter(ExternalDatabase::checkValidity) // + .findFirst() // + .orElse(ExternalDatabase.unavailable()); + } + + /** + * Returns a locally provided database at {@code postgres:@localhost:5432/postgres}. + */ + private static ExternalDatabase local() { + + return ProvidedDatabase.builder() // + .hostname("localhost") // + .port(3306) // + .database("mysql") // + .username("root") // + .password("my-secret-pw").build(); + } + + /** + * Returns a database provided via Testcontainers. + */ + private static ExternalDatabase testContainer() { + + if (testContainerDatabase == null) { + + try { + MySQLContainer mySQLContainer = new MySQLContainer("mysql:5.6.43"); + mySQLContainer.start(); + + testContainerDatabase = ProvidedDatabase.builder() // + .hostname("localhost") // + .port(mySQLContainer.getFirstMappedPort()) // + .database(mySQLContainer.getDatabaseName()) // + .username("root") // + .password(mySQLContainer.getPassword()).build(); + } catch (IllegalStateException ise) { + // docker not available. + testContainerDatabase = ExternalDatabase.unavailable(); + } + } + + return testContainerDatabase; + } + + /** + * Creates a new {@link ConnectionFactory} configured from the {@link ExternalDatabase}.. + */ + public static ConnectionFactory createConnectionFactory(ExternalDatabase database) { + + MySQLConnectionFactory jasync = new MySQLConnectionFactory(new Configuration(database.getUsername(), + database.getHostname(), database.getPort(), database.getPassword(), database.getDatabase())); + return new JasyncConnectionFactory(jasync); + } + + /** + * Creates a new {@link DataSource} configured from the {@link ExternalDatabase}. + */ + public static DataSource createDataSource(ExternalDatabase database) { + + MysqlDataSource dataSource = new MysqlDataSource(); + + dataSource.setUser(database.getUsername()); + dataSource.setPassword(database.getPassword()); + dataSource.setDatabaseName(database.getDatabase()); + dataSource.setServerName(database.getHostname()); + dataSource.setPortNumber(database.getPort()); + + return dataSource; + } + +}