@ -17,14 +17,26 @@
package org.springframework.jdbc.core.metadata ;
package org.springframework.jdbc.core.metadata ;
import java.sql.DatabaseMetaData ;
import java.sql.DatabaseMetaData ;
import java.sql.ResultSet ;
import java.sql.SQLException ;
import java.sql.SQLException ;
import java.util.ArrayList ;
import java.util.Arrays ;
import java.util.List ;
import java.util.function.IntFunction ;
import org.junit.jupiter.api.Test ;
import org.junit.jupiter.api.Test ;
import org.mockito.InOrder ;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException ;
import org.springframework.dao.InvalidDataAccessApiUsageException ;
import org.springframework.lang.Nullable ;
import org.springframework.util.function.ThrowingBiFunction ;
import static org.assertj.core.api.Assertions.assertThat ;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType ;
import static org.mockito.BDDMockito.given ;
import static org.mockito.BDDMockito.given ;
import static org.mockito.Mockito.inOrder ;
import static org.mockito.Mockito.mock ;
import static org.mockito.Mockito.mock ;
import static org.mockito.Mockito.verify ;
import static org.mockito.Mockito.verifyNoMoreInteractions ;
/ * *
/ * *
* Tests for { @link GenericCallMetaDataProvider } .
* Tests for { @link GenericCallMetaDataProvider } .
@ -36,36 +48,230 @@ class GenericCallMetaDataProviderTests {
private final DatabaseMetaData databaseMetaData = mock ( DatabaseMetaData . class ) ;
private final DatabaseMetaData databaseMetaData = mock ( DatabaseMetaData . class ) ;
@Test
@Test
void procedureNameWithPatternIsEscape ( ) throws SQLException {
void procedureNameWithNoMatch ( ) throws SQLException {
given ( this . databaseMetaData . getSearchStringEscape ( ) ) . willReturn ( "@" ) ;
GenericCallMetaDataProvider provider = new GenericCallMetaDataProvider ( this . databaseMetaData ) ;
GenericCallMetaDataProvider provider = new GenericCallMetaDataProvider ( this . databaseMetaData ) ;
given ( this . databaseMetaData . getProcedures ( null , null , "MY@_PROCEDURE" ) )
. willThrow ( new IllegalStateException ( "Expected" ) ) ;
ResultSet noProcedure = mockProcedures ( ) ;
assertThatIllegalStateException ( ) . isThrownBy ( ( ) - > provider . initializeWithProcedureColumnMetaData (
given ( this . databaseMetaData . getProcedures ( null , null , "MY_PROCEDURE" ) )
this . databaseMetaData , null , null , "my_procedure" ) ) ;
. willReturn ( noProcedure ) ;
verify ( this . databaseMetaData ) . getProcedures ( null , null , "MY@_PROCEDURE" ) ;
ResultSet noFunction = mockProcedures ( ) ;
given ( this . databaseMetaData . getFunctions ( null , null , "MY_PROCEDURE" ) )
. willReturn ( noFunction ) ;
assertThatExceptionOfType ( InvalidDataAccessApiUsageException . class )
. isThrownBy ( ( ) - > provider . initializeWithProcedureColumnMetaData ( this . databaseMetaData , null , null , "my_procedure" ) )
. withMessageContaining ( "'MY_PROCEDURE'" ) ;
InOrder inOrder = inOrder ( this . databaseMetaData ) ;
inOrder . verify ( this . databaseMetaData ) . getUserName ( ) ;
inOrder . verify ( this . databaseMetaData ) . getProcedures ( null , null , "MY_PROCEDURE" ) ;
inOrder . verify ( this . databaseMetaData ) . getFunctions ( null , null , "MY_PROCEDURE" ) ;
inOrder . verify ( this . databaseMetaData ) . getDatabaseProductName ( ) ;
verifyNoMoreInteractions ( this . databaseMetaData ) ;
}
}
@Test
@Test
void schemaNameWithPatternIsEscape ( ) throws SQLException {
void procedureNameWithExactMatch ( ) throws SQLException {
given ( this . databaseMetaData . getSearchStringEscape ( ) ) . willReturn ( "@" ) ;
GenericCallMetaDataProvider provider = new GenericCallMetaDataProvider ( this . databaseMetaData ) ;
ResultSet myProcedure = mockProcedures ( new Procedure ( null , null , "MY_PROCEDURE" ) ) ;
given ( this . databaseMetaData . getProcedures ( null , null , "MY_PROCEDURE" ) )
. willReturn ( myProcedure ) ;
ResultSet myProcedureColumn = mockProcedureColumns ( "TEST" , DatabaseMetaData . procedureColumnIn ) ;
given ( this . databaseMetaData . getProcedureColumns ( null , null , "MY_PROCEDURE" , null ) )
. willReturn ( myProcedureColumn ) ;
provider . initializeWithProcedureColumnMetaData ( this . databaseMetaData , null , null , "my_procedure" ) ;
assertThat ( provider . getCallParameterMetaData ( ) ) . singleElement ( ) . satisfies ( callParameterMetaData - > {
assertThat ( callParameterMetaData . getParameterName ( ) ) . isEqualTo ( "TEST" ) ;
assertThat ( callParameterMetaData . getParameterType ( ) ) . isEqualTo ( DatabaseMetaData . procedureColumnIn ) ;
} ) ;
InOrder inOrder = inOrder ( this . databaseMetaData ) ;
inOrder . verify ( this . databaseMetaData ) . getUserName ( ) ;
inOrder . verify ( this . databaseMetaData ) . getProcedures ( null , null , "MY_PROCEDURE" ) ;
inOrder . verify ( this . databaseMetaData ) . getProcedureColumns ( null , null , "MY_PROCEDURE" , null ) ;
verifyNoMoreInteractions ( this . databaseMetaData ) ;
}
@Test
void procedureNameWithSeveralMatchesFallBackOnEscaped ( ) throws SQLException {
GenericCallMetaDataProvider provider = new GenericCallMetaDataProvider ( this . databaseMetaData ) ;
GenericCallMetaDataProvider provider = new GenericCallMetaDataProvider ( this . databaseMetaData ) ;
given ( this . databaseMetaData . getProcedures ( null , "MY@_SCHEMA" , "TEST" ) )
. willThrow ( new IllegalStateException ( "Expected" ) ) ;
given ( this . databaseMetaData . getSearchStringEscape ( ) ) . willReturn ( "@" ) ;
assertThatIllegalStateException ( ) . isThrownBy ( ( ) - > provider . initializeWithProcedureColumnMetaData (
ResultSet myProcedures = mockProcedures ( new Procedure ( null , null , "MY_PROCEDURE" ) ,
this . databaseMetaData , null , "my_schema" , "test" ) ) ;
new Procedure ( null , null , "MYBPROCEDURE" ) ) ;
verify ( this . databaseMetaData ) . getProcedures ( null , "MY@_SCHEMA" , "TEST" ) ;
given ( this . databaseMetaData . getProcedures ( null , null , "MY_PROCEDURE" ) )
. willReturn ( myProcedures ) ;
ResultSet myProcedureEscaped = mockProcedures ( new Procedure ( null , null , "MY@_PROCEDURE" ) ) ;
given ( this . databaseMetaData . getProcedures ( null , null , "MY@_PROCEDURE" ) )
. willReturn ( myProcedureEscaped ) ;
ResultSet myProcedureColumn = mockProcedureColumns ( "TEST" , DatabaseMetaData . procedureColumnIn ) ;
given ( this . databaseMetaData . getProcedureColumns ( null , null , "MY@_PROCEDURE" , null ) )
. willReturn ( myProcedureColumn ) ;
provider . initializeWithProcedureColumnMetaData ( this . databaseMetaData , null , null , "my_procedure" ) ;
assertThat ( provider . getCallParameterMetaData ( ) ) . singleElement ( ) . satisfies ( callParameterMetaData - > {
assertThat ( callParameterMetaData . getParameterName ( ) ) . isEqualTo ( "TEST" ) ;
assertThat ( callParameterMetaData . getParameterType ( ) ) . isEqualTo ( DatabaseMetaData . procedureColumnIn ) ;
} ) ;
InOrder inOrder = inOrder ( this . databaseMetaData ) ;
inOrder . verify ( this . databaseMetaData ) . getUserName ( ) ;
inOrder . verify ( this . databaseMetaData ) . getProcedures ( null , null , "MY_PROCEDURE" ) ;
inOrder . verify ( this . databaseMetaData ) . getSearchStringEscape ( ) ;
inOrder . verify ( this . databaseMetaData ) . getProcedures ( null , null , "MY@_PROCEDURE" ) ;
inOrder . verify ( this . databaseMetaData ) . getProcedureColumns ( null , null , "MY@_PROCEDURE" , null ) ;
verifyNoMoreInteractions ( this . databaseMetaData ) ;
}
}
@Test
@Test
void nameIsNotEscapedIfEscapeCharacterIsNotAvailable ( ) throws SQLException {
void procedureNameWithSeveralMatchesDoesNotFallBackOn EscapedIfEscapeCharacterIsNotAvailable( ) throws SQLException {
given ( this . databaseMetaData . getSearchStringEscape ( ) ) . willReturn ( null ) ;
given ( this . databaseMetaData . getSearchStringEscape ( ) ) . willReturn ( null ) ;
GenericCallMetaDataProvider provider = new GenericCallMetaDataProvider ( this . databaseMetaData ) ;
GenericCallMetaDataProvider provider = new GenericCallMetaDataProvider ( this . databaseMetaData ) ;
given ( this . databaseMetaData . getProcedures ( null , "MY_SCHEMA" , "MY_TEST" ) )
. willThrow ( new IllegalStateException ( "Expected" ) ) ;
ResultSet myProcedures = mockProcedures ( new Procedure ( null , null , "MY_PROCEDURE" ) ,
assertThatIllegalStateException ( ) . isThrownBy ( ( ) - > provider . initializeWithProcedureColumnMetaData (
new Procedure ( null , null , "MYBPROCEDURE" ) ) ;
this . databaseMetaData , null , "my_schema" , "my_test" ) ) ;
given ( this . databaseMetaData . getProcedures ( null , null , "MY_PROCEDURE" ) )
verify ( this . databaseMetaData ) . getProcedures ( null , "MY_SCHEMA" , "MY_TEST" ) ;
. willReturn ( myProcedures ) ;
assertThatExceptionOfType ( InvalidDataAccessApiUsageException . class )
. isThrownBy ( ( ) - > provider . initializeWithProcedureColumnMetaData ( this . databaseMetaData , null , null , "my_procedure" ) )
. withMessageContainingAll ( "'MY_PROCEDURE'" , "null.null.MY_PROCEDURE" , "null.null.MYBPROCEDURE" ) ;
InOrder inOrder = inOrder ( this . databaseMetaData ) ;
inOrder . verify ( this . databaseMetaData ) . getUserName ( ) ;
inOrder . verify ( this . databaseMetaData ) . getProcedures ( null , null , "MY_PROCEDURE" ) ;
inOrder . verify ( this . databaseMetaData ) . getSearchStringEscape ( ) ;
verifyNoMoreInteractions ( this . databaseMetaData ) ;
}
@Test
void procedureNameWitNoMatchFallbackOnFunction ( ) throws SQLException {
GenericCallMetaDataProvider provider = new GenericCallMetaDataProvider ( this . databaseMetaData ) ;
given ( this . databaseMetaData . getSearchStringEscape ( ) ) . willReturn ( "@" ) ;
ResultSet noProcedure = mockProcedures ( ) ;
given ( this . databaseMetaData . getProcedures ( null , null , "MY_PROCEDURE" ) )
. willReturn ( noProcedure ) ;
ResultSet noProcedureWithEscaped = mockProcedures ( ) ;
given ( this . databaseMetaData . getProcedures ( null , null , "MY@_PROCEDURE" ) )
. willReturn ( noProcedureWithEscaped ) ;
ResultSet function = mockFunctions ( new Procedure ( null , null , "MY_PROCEDURE" ) ) ;
given ( this . databaseMetaData . getFunctions ( null , null , "MY_PROCEDURE" ) )
. willReturn ( function ) ;
ResultSet myProcedureColumn = mockProcedureColumns ( "TEST" , DatabaseMetaData . procedureColumnIn ) ;
given ( this . databaseMetaData . getFunctionColumns ( null , null , "MY_PROCEDURE" , null ) )
. willReturn ( myProcedureColumn ) ;
provider . initializeWithProcedureColumnMetaData ( this . databaseMetaData , null , null , "my_procedure" ) ;
assertThat ( provider . getCallParameterMetaData ( ) ) . singleElement ( ) . satisfies ( callParameterMetaData - > {
assertThat ( callParameterMetaData . getParameterName ( ) ) . isEqualTo ( "TEST" ) ;
assertThat ( callParameterMetaData . getParameterType ( ) ) . isEqualTo ( DatabaseMetaData . procedureColumnIn ) ;
} ) ;
InOrder inOrder = inOrder ( this . databaseMetaData ) ;
inOrder . verify ( this . databaseMetaData ) . getUserName ( ) ;
inOrder . verify ( this . databaseMetaData ) . getProcedures ( null , null , "MY_PROCEDURE" ) ;
inOrder . verify ( this . databaseMetaData ) . getFunctions ( null , null , "MY_PROCEDURE" ) ;
inOrder . verify ( this . databaseMetaData ) . getFunctionColumns ( null , null , "MY_PROCEDURE" , null ) ;
verifyNoMoreInteractions ( this . databaseMetaData ) ;
}
@Test
void procedureNameWitNoMatchAndSeveralFunctionsFallbacksOnEscaped ( ) throws SQLException {
GenericCallMetaDataProvider provider = new GenericCallMetaDataProvider ( this . databaseMetaData ) ;
given ( this . databaseMetaData . getSearchStringEscape ( ) ) . willReturn ( "@" ) ;
ResultSet noProcedure = mockProcedures ( ) ;
given ( this . databaseMetaData . getProcedures ( null , null , "MY_PROCEDURE" ) )
. willReturn ( noProcedure ) ;
ResultSet functions = mockFunctions ( new Procedure ( null , null , "MY_PROCEDURE" ) ,
new Procedure ( null , null , "MYBPROCEDURE" ) ) ;
given ( this . databaseMetaData . getFunctions ( null , null , "MY_PROCEDURE" ) )
. willReturn ( functions ) ;
ResultSet functionEscaped = mockFunctions ( new Procedure ( null , null , "MY@_PROCEDURE" ) ) ;
given ( this . databaseMetaData . getFunctions ( null , null , "MY@_PROCEDURE" ) )
. willReturn ( functionEscaped ) ;
ResultSet myProcedureColumn = mockProcedureColumns ( "TEST" , DatabaseMetaData . procedureColumnIn ) ;
given ( this . databaseMetaData . getFunctionColumns ( null , null , "MY@_PROCEDURE" , null ) )
. willReturn ( myProcedureColumn ) ;
provider . initializeWithProcedureColumnMetaData ( this . databaseMetaData , null , null , "my_procedure" ) ;
assertThat ( provider . getCallParameterMetaData ( ) ) . singleElement ( ) . satisfies ( callParameterMetaData - > {
assertThat ( callParameterMetaData . getParameterName ( ) ) . isEqualTo ( "TEST" ) ;
assertThat ( callParameterMetaData . getParameterType ( ) ) . isEqualTo ( DatabaseMetaData . procedureColumnIn ) ;
} ) ;
InOrder inOrder = inOrder ( this . databaseMetaData ) ;
inOrder . verify ( this . databaseMetaData ) . getUserName ( ) ;
inOrder . verify ( this . databaseMetaData ) . getProcedures ( null , null , "MY_PROCEDURE" ) ;
inOrder . verify ( this . databaseMetaData ) . getFunctions ( null , null , "MY_PROCEDURE" ) ;
inOrder . verify ( this . databaseMetaData ) . getSearchStringEscape ( ) ;
inOrder . verify ( this . databaseMetaData ) . getFunctions ( null , null , "MY@_PROCEDURE" ) ;
inOrder . verify ( this . databaseMetaData ) . getFunctionColumns ( null , null , "MY@_PROCEDURE" , null ) ;
verifyNoMoreInteractions ( this . databaseMetaData ) ;
}
private ResultSet mockProcedures ( Procedure . . . procedures ) {
ResultSet resultSet = mock ( ResultSet . class ) ;
List < Boolean > next = new ArrayList < > ( ) ;
Arrays . stream ( procedures ) . forEach ( p - > next . add ( true ) ) ;
applyStrings ( Arrays . stream ( procedures ) . map ( Procedure : : catalog ) . toList ( ) , ( first , then ) - >
given ( resultSet . getString ( "PROCEDURE_CAT" ) ) . willReturn ( first , then ) ) ;
applyStrings ( Arrays . stream ( procedures ) . map ( Procedure : : schema ) . toList ( ) , ( first , then ) - >
given ( resultSet . getString ( "PROCEDURE_SCHEM" ) ) . willReturn ( first , then ) ) ;
applyStrings ( Arrays . stream ( procedures ) . map ( Procedure : : name ) . toList ( ) , ( first , then ) - >
given ( resultSet . getString ( "PROCEDURE_NAME" ) ) . willReturn ( first , then ) ) ;
next . add ( false ) ;
applyBooleans ( next , ( first , then ) - > given ( resultSet . next ( ) ) . willReturn ( first , then ) ) ;
return resultSet ;
}
private ResultSet mockFunctions ( Procedure . . . procedures ) {
ResultSet resultSet = mock ( ResultSet . class ) ;
List < Boolean > next = new ArrayList < > ( ) ;
Arrays . stream ( procedures ) . forEach ( p - > next . add ( true ) ) ;
applyStrings ( Arrays . stream ( procedures ) . map ( Procedure : : catalog ) . toList ( ) , ( first , then ) - >
given ( resultSet . getString ( "FUNCTION_CAT" ) ) . willReturn ( first , then ) ) ;
applyStrings ( Arrays . stream ( procedures ) . map ( Procedure : : schema ) . toList ( ) , ( first , then ) - >
given ( resultSet . getString ( "FUNCTION_SCHEM" ) ) . willReturn ( first , then ) ) ;
applyStrings ( Arrays . stream ( procedures ) . map ( Procedure : : name ) . toList ( ) , ( first , then ) - >
given ( resultSet . getString ( "FUNCTION_NAME" ) ) . willReturn ( first , then ) ) ;
next . add ( false ) ;
applyBooleans ( next , ( first , then ) - > given ( resultSet . next ( ) ) . willReturn ( first , then ) ) ;
return resultSet ;
}
private ResultSet mockProcedureColumns ( String columnName , int columnType ) throws SQLException {
ResultSet resultSet = mock ( ResultSet . class ) ;
given ( resultSet . next ( ) ) . willReturn ( true , false ) ;
given ( resultSet . getString ( "COLUMN_NAME" ) ) . willReturn ( columnName ) ;
given ( resultSet . getInt ( "COLUMN_TYPE" ) ) . willReturn ( columnType ) ;
return resultSet ;
}
record Procedure ( @Nullable String catalog , @Nullable String schema , String name ) {
}
private void applyBooleans ( List < Boolean > content , ThrowingBiFunction < Boolean , Boolean [ ] , Object > split ) {
apply ( content , Boolean [ ] : : new , split ) ;
}
private void applyStrings ( List < String > content , ThrowingBiFunction < String , String [ ] , Object > split ) {
apply ( content , String [ ] : : new , split ) ;
}
private < T > void apply ( List < T > content , IntFunction < T [ ] > generator , ThrowingBiFunction < T , T [ ] , Object > split ) {
if ( content . isEmpty ( ) ) {
return ;
}
T first = content . get ( 0 ) ;
T [ ] array = content . subList ( 1 , content . size ( ) ) . toArray ( generator ) ;
split . apply ( first , array ) ;
}
}
}
}