/* * Copyright 2017-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 io.r2dbc.spi.test; import io.r2dbc.spi.Blob; import io.r2dbc.spi.Clob; import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactory; import io.r2dbc.spi.Result; import io.r2dbc.spi.Statement; import io.r2dbc.spi.ValidationDepth; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.core.support.AbstractLobCreatingPreparedStatementCallback; import org.springframework.jdbc.support.lob.DefaultLobHandler; import org.springframework.jdbc.support.lob.LobCreator; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.function.Function; import java.util.stream.IntStream; import static org.junit.jupiter.api.Assertions.assertThrows; /** * R2DBC TCK implementation to verify a driver. * <p>Functionality that is not supported by a driver should exclude the individual tests by using {@link Disabled @Disabled}. * Implementors need to provide the following methods: * * <ul> * <li>{@link #getConnectionFactory()}: Provide the {@link ConnectionFactory} under test.</li> * <li>{@link #getCreateTableWithAutogeneratedKey()}: Return the {@code CREATE TABLE} statement to create a table.</li> * <li>{@link #getPlaceholder(int)}: Return an parameter placeholder for the {@code n}th argument. Arguments start at zero.</li> * <li>{@link #getIdentifier(int)}: Return an identifier for the {@code n}th argument. The placeholder was obtained prior to this call via {@link #getPlaceholder(int)}. Arguments start at zero * <li>{@link #getJdbcOperations}: Return a handle to {@link JdbcOperations} configured to the same data source as {@link #getConnectionFactory()}. Used for test fixture setup/teardown.</li> * </ul> * * <p>The following customization hooks can be overridden to adapt the TCK to a specific database: * <ul> * <li>{@link #getInsertIntoWithAutogeneratedKey()}: Return the {@code INSERT INTO} statement to insert data into a table supporting auto-generated keys.</li> * <li>{@link #blobType()}: Returns the SQL type for a {@link Blob} column.</li> * <li>{@link #clobType()}: Returns the SQL type for a {@link Clob} column.</li> * </ul> * * @param <T> type of parameter placeholder identifiers, see {@link #getIdentifier(int)}. */ public interface TestKit<T> { static <T> Mono<T> close(Connection connection) { return Mono.from(connection .close()) .then(Mono.empty()); } static <T> Mono<T> discard(Blob blob) { return Mono.from(blob .discard()) .then(Mono.empty()); } static <T> Mono<T> discard(Clob clob) { return Mono.from(clob .discard()) .then(Mono.empty()); } static Mono<List<Integer>> extractColumns(Result result) { return Flux.from(result .map((row, rowMetadata) -> row.get("value", Integer.class))) .collectList(); } static Mono<Integer> extractRowsUpdated(Result result) { return Mono.from(result.getRowsUpdated()); } /** * Returns a {@link ConnectionFactory} for the connected database. * * @return a {@link ConnectionFactory} for the connected database */ ConnectionFactory getConnectionFactory(); /** * Returns the {@code CREATE TABLE} statement for a table named {@code test} * with two columns: First one uses auto-generated keys, second one is named {@code value} of type {@code INT}. * <p> * Example: * <pre class="code"> * CREATE TABLE test ( id SERIAL, value INTEGER ); * // or * CREATE TABLE test ( id INTEGER IDENTITY, value INTEGER ); * </pre> * * @return the {@code CREATE TABLE} statement */ String getCreateTableWithAutogeneratedKey(); /** * Returns the database-specific placeholder for a given substitution. * * @param index the zero-index number of the substitution * @return the database-specific placeholder for a given substitution */ String getPlaceholder(int index); /** * Returns the bind identifier for a given substitution. * * @param index the zero-index number of the substitution * @return the bind identifier for a given substitution */ T getIdentifier(int index); /** * Returns a {@link JdbcOperations} for the connected database. * * @return a {@link JdbcOperations} for the connected database */ JdbcOperations getJdbcOperations(); @Test default void autoCommitByDefault() { Mono.from(getConnectionFactory().create()) .flatMapMany(connection -> Flux.just(connection.isAutoCommit()) .concatWith(close(connection))) .as(StepVerifier::create) .expectNext(true).as("new connections are in auto-commit mode") .verifyComplete(); } @Test default void changeAutoCommitCommitsTransaction() { Mono.from(getConnectionFactory().create()) .flatMapMany(connection -> Flux.from(connection.setAutoCommit(false)) .thenMany(connection.beginTransaction()) .thenMany(connection.createStatement("INSERT INTO test VALUES(200)").execute()) .flatMap(Result::getRowsUpdated) .thenMany(connection.setAutoCommit(true)) .thenMany(connection.createStatement("SELECT value FROM test").execute()) .flatMap(it -> it.map((row, metadata) -> row.get("value"))) .concatWith(close(connection)) ) .as(StepVerifier::create) .expectNext(200).as("autoCommit(true) committed the transaction. Expecting a value to be present") .verifyComplete(); } @Test default void sameAutoCommitLeavesTransactionUnchanged() { Mono.from(getConnectionFactory().create()) .flatMapMany(connection -> Flux.from(connection.setAutoCommit(false)) .thenMany(connection.beginTransaction()) .thenMany(connection.createStatement("INSERT INTO test VALUES(200)").execute()) .flatMap(Result::getRowsUpdated) .thenMany(connection.setAutoCommit(false)) .thenMany(connection.rollbackTransaction()) .thenMany(connection.createStatement("SELECT value FROM test").execute()) .flatMap(it -> it.map((row, metadata) -> row.get("value"))) .concatWith(close(connection)) ) .as(StepVerifier::create) .verifyComplete(); } @Test default void batch() { getJdbcOperations().execute("INSERT INTO test VALUES (100)"); Mono.from(getConnectionFactory().create()) .flatMapMany(connection -> Flux.from(connection .createBatch() .add("INSERT INTO test VALUES(200)") .add("SELECT value FROM test") .execute()) .concatWith(close(connection))) .flatMap(Result::getRowsUpdated) .then() .as(StepVerifier::create) .verifyComplete(); } @Test default void bindFails() { Mono.from(getConnectionFactory().create()) .flatMap(connection -> { Statement statement = connection.createStatement(String.format("INSERT INTO test VALUES(%s)", getPlaceholder(0))); assertThrows(IllegalArgumentException.class, () -> statement.bind(0, null), "bind(0, null) should fail"); assertThrows(IndexOutOfBoundsException.class, () -> statement.bind(99, ""), "bind(nonexistent-index, null) should fail"); assertThrows(IllegalArgumentException.class, () -> bind(statement, getIdentifier(0), null), "bind(identifier, null) should fail"); assertThrows(IllegalArgumentException.class, () -> bind(statement, getIdentifier(0), Class.class), "bind(identifier, Class.class) should fail"); assertThrows(IllegalArgumentException.class, () -> statement.bind("unknown", ""), "bind(unknown-placeholder, \"\") should fail"); return close(connection); }) .as(StepVerifier::create) .verifyComplete(); } @Test default void bindNull() { Mono.from(getConnectionFactory().create()) .flatMapMany(connection -> { Statement statement = connection .createStatement(String.format("INSERT INTO test VALUES(%s)", getPlaceholder(0))); bindNull(statement, getIdentifier(0), Integer.class); return Flux.from(statement.add() .execute()) .flatMap(TestKit::extractRowsUpdated) .concatWith(close(connection)); }) .as(StepVerifier::create) .expectNextCount(1).as("rows inserted") .verifyComplete(); } @Test default void bindNullFails() { Mono.from(getConnectionFactory().create()) .flatMap(connection -> { Statement statement = connection.createStatement(String.format("INSERT INTO test VALUES(%s)", getPlaceholder(0))); assertThrows(IllegalArgumentException.class, () -> statement.bindNull(null, String.class), "bindNull(null, …) should fail"); assertThrows(IllegalArgumentException.class, () -> bind(statement, getIdentifier(0), null), "bindNull(identifier, null) should fail"); return close(connection); }) .as(StepVerifier::create) .verifyComplete(); } @Test default void blobInsert() { Mono.from(getConnectionFactory().create()) .flatMapMany(connection -> { Statement statement = connection.createStatement(String.format("INSERT INTO blob_test VALUES (%s)", getPlaceholder(0))); bind(statement, getIdentifier(0), Blob.from(Mono.just(StandardCharsets.UTF_8.encode("test-value")))); return Flux.from(statement.execute()) .flatMap(TestKit::extractRowsUpdated) .concatWith(close(connection)); }) .as(StepVerifier::create) .expectNextCount(1).as("rows inserted") .verifyComplete(); } @Test default void blobSelect() { getJdbcOperations().execute("INSERT INTO blob_test VALUES (?)", new AbstractLobCreatingPreparedStatementCallback(new DefaultLobHandler()) { @Override protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException { lobCreator.setBlobAsBytes(ps, 1, StandardCharsets.UTF_8.encode("test-value").array()); } }); // BLOB as ByteBuffer Mono.from(getConnectionFactory().create()) .flatMapMany(connection -> Flux.from(connection .createStatement("SELECT * from blob_test") .execute()) .flatMap(result -> result .map((row, rowMetadata) -> row.get("value"))) .cast(ByteBuffer.class) .map(buffer -> { byte[] bytes = new byte[buffer.remaining()]; buffer.get(bytes); return bytes; }) .concatWith(close(connection))) .as(StepVerifier::create) .expectNextMatches(actual -> { ByteBuffer expected = StandardCharsets.UTF_8.encode("test-value"); return Arrays.equals(expected.array(), actual); }) .verifyComplete(); // BLOB as Blob Mono.from(getConnectionFactory().create()) .flatMapMany(connection -> Flux.from(connection .createStatement("SELECT * from blob_test") .execute()) .flatMap(result -> result .map((row, rowMetadata) -> row.get("value", Blob.class))) .flatMap(blob -> Flux.from(blob.stream()) .reduce(ByteBuffer::put) .concatWith(discard(blob))) .concatWith(close(connection))) .as(StepVerifier::create) .expectNextMatches(actual -> { ByteBuffer expected = StandardCharsets.UTF_8.encode("test-value"); return Arrays.equals(expected.array(), actual.array()); }) .verifyComplete(); } /** * Customization hook: Returns the SQL type for a {@link Blob} column. * * @return the SQL type for a {@link Blob} column. */ default String blobType() { return "BLOB"; } @Test default void clobInsert() { Mono.from(getConnectionFactory().create()) .flatMapMany(connection -> { Statement statement = connection.createStatement(String.format("INSERT INTO clob_test VALUES (%s)", getPlaceholder(0))); bind(statement, getIdentifier(0), Clob.from(Mono.just("test-value"))); return Flux.from(statement.execute()) .flatMap(Result::getRowsUpdated) .concatWith(close(connection)); }) .as(StepVerifier::create) .expectNextCount(1).as("rows inserted") .verifyComplete(); } @Test default void clobSelect() { getJdbcOperations().execute("INSERT INTO clob_test VALUES (?)", new AbstractLobCreatingPreparedStatementCallback(new DefaultLobHandler()) { @Override protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException { lobCreator.setClobAsString(ps, 1, "test-value"); } }); // CLOB defaults to String Mono.from(getConnectionFactory().create()) .flatMapMany(connection -> Flux.from(connection .createStatement("SELECT * from clob_test") .execute()) .flatMap(result -> result .map((row, rowMetadata) -> row.get("value"))) .concatWith(close(connection))) .as(StepVerifier::create) .expectNext("test-value").as("value from select") .verifyComplete(); // CLOB consume as Clob Mono.from(getConnectionFactory().create()) .flatMapMany(connection -> Flux.from(connection .createStatement("SELECT * from clob_test") .execute()) .flatMap(result -> result .map((row, rowMetadata) -> row.get("value", Clob.class))) .flatMap(clob -> Flux.from(clob.stream()) .reduce(new StringBuilder(), StringBuilder::append) .map(StringBuilder::toString) .concatWith(discard(clob))) .concatWith(close(connection))) .as(StepVerifier::create) .expectNext("test-value").as("value from select") .verifyComplete(); } /** * Customization hook: Returns the SQL type for a {@link Clob} column. * * @return the SQL type for a {@link Clob} column. */ default String clobType() { return "CLOB"; } @Test default void columnMetadata() { getJdbcOperations().execute("INSERT INTO test_two_column VALUES (100, 'hello')"); Mono.from(getConnectionFactory().create()) .flatMapMany(connection -> Flux.from(connection .createStatement("SELECT col1 AS value, col2 AS value FROM test_two_column") .execute()) .flatMap(result -> { return result.map((row, rowMetadata) -> { Collection<String> columnNames = rowMetadata.getColumnNames(); return Arrays.asList(rowMetadata.getColumnMetadata("value").getName(), rowMetadata.getColumnMetadata("VALUE").getName(), columnNames.contains("value"), columnNames.contains( "VALUE")); }); }) .flatMapIterable(Function.identity()) .concatWith(close(connection))) .as(StepVerifier::create) .expectNext("value").as("Column label col1") .expectNext("value").as("Column label col1 (get by uppercase)") .expectNext(true).as("getColumnNames.contains(value)") .expectNext(true).as("getColumnNames.contains(VALUE)") .verifyComplete(); } @Test default void compoundStatement() { getJdbcOperations().execute("INSERT INTO test VALUES (100)"); Mono.from(getConnectionFactory().create()) .flatMapMany(connection -> Flux.from(connection .createStatement("SELECT value FROM test; SELECT value FROM test") .execute()) .flatMap(TestKit::extractColumns) .concatWith(close(connection))) .as(StepVerifier::create) .expectNext(Collections.singletonList(100)).as("value from first select") .expectNext(Collections.singletonList(100)).as("value from second select") .verifyComplete(); } @Test default void createStatementFails() { Mono.from(getConnectionFactory().create()) .flatMap(connection -> { assertThrows(IllegalArgumentException.class, () -> connection.createStatement(null)); return close(connection); }) .as(StepVerifier::create) .verifyComplete(); } @BeforeEach default void createTable() { getJdbcOperations().execute("CREATE TABLE test ( value INTEGER )"); getJdbcOperations().execute("CREATE TABLE test_two_column ( col1 INTEGER, col2 VARCHAR(100) )"); getJdbcOperations().execute(String.format("CREATE TABLE blob_test ( value %s )", blobType())); getJdbcOperations().execute(String.format("CREATE TABLE clob_test ( value %s )", clobType())); } @AfterEach default void dropTable() { getJdbcOperations().execute("DROP TABLE test"); getJdbcOperations().execute("DROP TABLE test_two_column"); getJdbcOperations().execute("DROP TABLE blob_test"); getJdbcOperations().execute("DROP TABLE clob_test"); } @Test default void duplicateColumnNames() { getJdbcOperations().execute("INSERT INTO test_two_column VALUES (100, 'hello')"); Mono.from(getConnectionFactory().create()) .flatMapMany(connection -> Flux.from(connection .createStatement("SELECT col1 AS value, col2 AS value FROM test_two_column") .execute()) .flatMap(result -> result .map((row, rowMetadata) -> Arrays.asList(row.get("value"), row.get("VALUE")))) .flatMapIterable(Function.identity()) .concatWith(close(connection))) .as(StepVerifier::create) .expectNext(100).as("value from col1") .expectNext(100).as("value from col1 (upper case)") .verifyComplete(); } @Test default void prepareStatement() { Mono.from(getConnectionFactory().create()) .flatMapMany(connection -> { Statement statement = connection.createStatement(String.format("INSERT INTO test VALUES(%s)", getPlaceholder(0))); IntStream.range(0, 10) .forEach(i -> bind(statement, getIdentifier(0), i).add()); return Flux.from(statement .execute()) .flatMap(TestKit::extractRowsUpdated) .concatWith(close(connection)); }) .as(StepVerifier::create) .expectNextCount(10).as("values from insertions") .verifyComplete(); } @Test default void prepareStatementWithIncompleteBatchFails() { Mono.from(getConnectionFactory().create()) .flatMapMany(connection -> { Statement statement = connection.createStatement(String.format("INSERT INTO test VALUES(%s,%s)", getPlaceholder(0), getPlaceholder(1))); bind(statement, getIdentifier(0), 0); assertThrows(IllegalStateException.class, statement::add); return close(connection); }) .as(StepVerifier::create) .verifyComplete(); } @Test default void prepareStatementWithIncompleteBindingFails() { Mono.from(getConnectionFactory().create()) .flatMapMany(connection -> { Statement statement = connection.createStatement(String.format("INSERT INTO test VALUES(%s,%s)", getPlaceholder(0), getPlaceholder(1))); bind(statement, getIdentifier(0), 0); assertThrows(IllegalStateException.class, statement::execute); return close(connection); }) .as(StepVerifier::create) .verifyComplete(); } /** * Customization hook: Returns the {@code INSERT INTO} statement for a table which created * by {@link #getCreateTableWithAutogeneratedKey}, only contains the * data for second column {@code value} which should be {@literal 100}. * <p> * Example: * <pre class="code"> * INSERT INTO test VALUES(100); * // or * INSERT INTO test VALUES(DEFAULT,100); * </pre> * * @return the {@code INSERT INTO} statement */ default String getInsertIntoWithAutogeneratedKey() { return "INSERT INTO test VALUES(100)"; } @Test default void returnGeneratedValues() { getJdbcOperations().execute("DROP TABLE test"); getJdbcOperations().execute(getCreateTableWithAutogeneratedKey()); Mono.from(getConnectionFactory().create()) .flatMapMany(connection -> { Statement statement = connection.createStatement(getInsertIntoWithAutogeneratedKey()); statement.returnGeneratedValues(); return Flux.from(statement .execute()) .concatWith(close(connection)).flatMap(it -> it.map((row, rowMetadata) -> row.get(0))); }) .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); } @Test default void returnGeneratedValuesFails() { Mono.from(getConnectionFactory().create()) .flatMapMany(connection -> { Statement statement = connection.createStatement("INSERT INTO test"); assertThrows(IllegalArgumentException.class, () -> statement.returnGeneratedValues((String[]) null)); return close(connection); }) .as(StepVerifier::create) .verifyComplete(); } @Test default void savePoint() { getJdbcOperations().execute("INSERT INTO test VALUES (100)"); Mono.from(getConnectionFactory().create()) .flatMapMany(connection -> Mono.from(connection .beginTransaction()) .<Object>thenMany(Flux.from(connection.createStatement("SELECT value FROM test") .execute()) .flatMap(TestKit::extractColumns)) .concatWith(Flux.defer(() -> { Statement statement = connection.createStatement(String.format("INSERT INTO test VALUES (%s)", getPlaceholder(0))); bind(statement, getIdentifier(0), 200); return statement.execute(); }) .flatMap(TestKit::extractRowsUpdated)) .concatWith(Flux.from(connection.createStatement("SELECT value FROM test") .execute()) .flatMap(TestKit::extractColumns)) .concatWith(connection.createSavepoint("test_savepoint")) .concatWith(Flux.defer(() -> { Statement statement = connection.createStatement(String.format("INSERT INTO test VALUES (%s)", getPlaceholder(0))); bind(statement, getIdentifier(0), 300); return statement.execute(); }) .flatMap(TestKit::extractRowsUpdated)) .concatWith(Flux.from(connection.createStatement("SELECT value FROM test") .execute()) .flatMap(TestKit::extractColumns)) .concatWith(connection.rollbackTransactionToSavepoint("test_savepoint")) .concatWith(Flux.from(connection.createStatement("SELECT value FROM test") .execute()) .flatMap(TestKit::extractColumns)) .concatWith(close(connection))) .as(StepVerifier::create) .expectNext(Collections.singletonList(100)).as("value from select") .expectNext(1).as("rows inserted") .expectNext(Arrays.asList(100, 200)).as("values from select") .expectNext(1).as("rows inserted") .expectNext(Arrays.asList(100, 200, 300)).as("values from select") .expectNext(Arrays.asList(100, 200)).as("values from select") .verifyComplete(); } @Test default void savePointStartsTransaction() { Mono.from(getConnectionFactory().create()) .flatMapMany(connection -> Mono.from(connection .createSavepoint("test_savepoint")) .then(Mono.fromSupplier(connection::isAutoCommit)) .concatWith(close(connection))) .as(StepVerifier::create) .expectNext(false).as("createSavepoint starts a transaction") .verifyComplete(); } @Test default void transactionCommit() { getJdbcOperations().execute("INSERT INTO test VALUES (100)"); Mono.from(getConnectionFactory().create()) .flatMapMany(connection -> Mono.from(connection .beginTransaction()) .<Object>thenMany(Flux.from(connection.createStatement("SELECT value FROM test") .execute()) .flatMap(TestKit::extractColumns)) .concatWith(Flux.defer(() -> { Statement statement = connection.createStatement(String.format("INSERT INTO test VALUES (%s)", getPlaceholder(0))); bind(statement, getIdentifier(0), 200); return statement.execute(); }) .flatMap(TestKit::extractRowsUpdated)) .concatWith(Flux.from(connection.createStatement("SELECT value FROM test") .execute()) .flatMap(TestKit::extractColumns)) .concatWith(connection.commitTransaction()) .concatWith(Flux.from(connection.createStatement("SELECT value FROM test") .execute()) .flatMap(TestKit::extractColumns)) .concatWith(close(connection))) .as(StepVerifier::create) .expectNext(Collections.singletonList(100)).as("value from select") .expectNext(1).as("rows inserted") .expectNext(Arrays.asList(100, 200)).as("values from select") .expectNext(Arrays.asList(100, 200)).as("values from select") .verifyComplete(); } @Test default void transactionRollback() { getJdbcOperations().execute("INSERT INTO test VALUES (100)"); Mono.from(getConnectionFactory().create()) .flatMapMany(connection -> Mono.from(connection .beginTransaction()) .<Object>thenMany(Flux.from(connection.createStatement("SELECT value FROM test") .execute()) .flatMap(TestKit::extractColumns)) .concatWith(Flux.defer(() -> { Statement statement = connection.createStatement(String.format("INSERT INTO test VALUES (%s)", getPlaceholder(0))); bind(statement, getIdentifier(0), 200); return statement.execute(); }) .flatMap(TestKit::extractRowsUpdated)) .concatWith(Flux.from(connection.createStatement("SELECT value FROM test") .execute()) .flatMap(TestKit::extractColumns)) .concatWith(connection.rollbackTransaction()) .concatWith(Flux.from(connection.createStatement("SELECT value FROM test") .execute()) .flatMap(TestKit::extractColumns)) .concatWith(close(connection))) .as(StepVerifier::create) .expectNext(Collections.singletonList(100)).as("value from select") .expectNext(1).as("rows inserted") .expectNext(Arrays.asList(100, 200)).as("values from select") .expectNext(Collections.singletonList(100)).as("value from select") .verifyComplete(); } @Test default void validate() { Mono.from(getConnectionFactory().create()) .flatMapMany(connection -> Flux.concat(connection.validate(ValidationDepth.LOCAL), connection.validate(ValidationDepth.REMOTE), connection.close(), connection.validate(ValidationDepth.LOCAL), connection.validate(ValidationDepth.REMOTE))) .as(StepVerifier::create) .expectNext(true).as("successful local validation") .expectNext(true).as("successful remote validation") .expectNext(false).as("failed local validation after close") .expectNext(false).as("failed remote validation after close") .verifyComplete(); } static Statement bind(Statement statement, Object identifier, Object value) { Assert.requireNonNull(identifier, "Identifier must not be null"); if (identifier instanceof String) { return statement.bind((String) identifier, value); } if (identifier instanceof Integer) { return statement.bind((Integer) identifier, value); } throw new IllegalArgumentException(String.format("Identifier %s must be a String or Integer. Was: %s", identifier, identifier.getClass().getName())); } static Statement bindNull(Statement statement, Object identifier, Class<?> type) { Assert.requireNonNull(identifier, "Identifier must not be null"); if (identifier instanceof String) { return statement.bindNull((String) identifier, type); } if (identifier instanceof Integer) { return statement.bindNull((Integer) identifier, type); } throw new IllegalArgumentException(String.format("Identifier %s must be a String or Integer. Was: %s", identifier, identifier.getClass().getName())); } }