/*! ******************************************************************************
*
* Pentaho Data Integration
*
* Copyright (C) 2002-2016 by Pentaho : http://www.pentaho.com
*
*******************************************************************************
*
* 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
*
* http://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.pentaho.di.trans.steps.databaselookup;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockingDetails;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.util.Arrays;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.Matchers;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.pentaho.di.core.KettleEnvironment;
import org.pentaho.di.core.RowSet;
import org.pentaho.di.core.database.Database;
import org.pentaho.di.core.database.DatabaseMeta;
import org.pentaho.di.core.database.MySQLDatabaseMeta;
import org.pentaho.di.core.exception.KettleException;
import org.pentaho.di.core.logging.LoggingObjectInterface;
import org.pentaho.di.core.row.RowMeta;
import org.pentaho.di.core.row.RowMetaInterface;
import org.pentaho.di.core.row.ValueMetaInterface;
import org.pentaho.di.core.row.value.ValueMetaBinary;
import org.pentaho.di.core.row.value.ValueMetaInteger;
import org.pentaho.di.core.row.value.ValueMetaString;
import org.pentaho.di.core.variables.VariableSpace;
import org.pentaho.di.repository.Repository;
import org.pentaho.di.trans.Trans;
import org.pentaho.di.trans.TransMeta;
import org.pentaho.di.trans.step.StepDataInterface;
import org.pentaho.di.trans.step.StepMeta;
import org.pentaho.di.trans.steps.databaselookup.readallcache.ReadAllCache;
import org.pentaho.di.trans.steps.mock.StepMockHelper;
import org.pentaho.metastore.api.IMetaStore;
/**
* @author Andrey Khayrutdinov
*/
public class DatabaseLookupUTest {
private static final String BINARY_FIELD = "aBinaryFieldInDb";
private static final String ID_FIELD = "id";
@BeforeClass
public static void setUp() throws Exception {
KettleEnvironment.init();
}
@Test
public void mySqlVariantDbIsLazyConverted() throws Exception {
StepMockHelper<DatabaseLookupMeta, DatabaseLookupData> mockHelper = createMockHelper();
DatabaseLookupMeta meta = createDatabaseMeta();
DatabaseLookupData data = createDatabaseData();
Database db = createVirtualDb( meta.getDatabaseMeta() );
DatabaseLookup lookup = spyLookup( mockHelper, db, meta.getDatabaseMeta() );
lookup.init( meta, data );
lookup.processRow( meta, data );
verify( db ).getLookup( any( PreparedStatement.class ), anyBoolean(), eq( true ) );
}
private StepMockHelper<DatabaseLookupMeta, DatabaseLookupData> createMockHelper() {
StepMockHelper<DatabaseLookupMeta, DatabaseLookupData> mockHelper =
new StepMockHelper<DatabaseLookupMeta, DatabaseLookupData>( "test DatabaseLookup", DatabaseLookupMeta.class,
DatabaseLookupData.class );
when( mockHelper.logChannelInterfaceFactory.create( any(), any( LoggingObjectInterface.class ) ) )
.thenReturn( mockHelper.logChannelInterface );
when( mockHelper.trans.isRunning() ).thenReturn( true );
RowMeta inputRowMeta = new RowMeta();
RowSet rowSet = mock( RowSet.class );
when( rowSet.getRowWait( anyLong(), any( TimeUnit.class ) ) ).thenReturn( new Object[ 0 ] ).thenReturn( null );
when( rowSet.getRowMeta() ).thenReturn( inputRowMeta );
when( mockHelper.trans.findRowSet( anyString(), anyInt(), anyString(), anyInt() ) ).thenReturn( rowSet );
when( mockHelper.transMeta.findNextSteps( Matchers.any( StepMeta.class ) ) )
.thenReturn( Collections.singletonList( mock( StepMeta.class ) ) );
when( mockHelper.transMeta.findPreviousSteps( any( StepMeta.class ), anyBoolean() ) )
.thenReturn( Collections.singletonList( mock( StepMeta.class ) ) );
return mockHelper;
}
private DatabaseLookupMeta createDatabaseMeta() throws KettleException {
MySQLDatabaseMeta mysql = new MySQLDatabaseMeta();
mysql.setName( "MySQL" );
DatabaseMeta dbMeta = new DatabaseMeta();
dbMeta.setDatabaseInterface( mysql );
DatabaseLookupMeta meta = new DatabaseLookupMeta();
meta.setDatabaseMeta( dbMeta );
meta.setTablename( "VirtualTable" );
meta.setTableKeyField( new String[] { ID_FIELD } );
meta.setKeyCondition( new String[] { "=" } );
meta.setReturnValueNewName( new String[] { "returned value" } );
meta.setReturnValueField( new String[] { BINARY_FIELD } );
meta.setReturnValueDefaultType( new int[] { ValueMetaInterface.TYPE_BINARY } );
meta.setStreamKeyField1( new String[ 0 ] );
meta.setStreamKeyField2( new String[ 0 ] );
meta.setReturnValueDefault( new String[] { "" } );
meta = spy( meta );
doAnswer( new Answer() {
@Override public Object answer( InvocationOnMock invocation ) throws Throwable {
RowMetaInterface row = (RowMetaInterface) invocation.getArguments()[ 0 ];
ValueMetaInterface v = new ValueMetaBinary( BINARY_FIELD );
row.addValueMeta( v );
return null;
}
} ).when( meta ).getFields(
any( RowMetaInterface.class ),
anyString(),
any( RowMetaInterface[].class ),
any( StepMeta.class ),
any( VariableSpace.class ),
any( Repository.class ),
any( IMetaStore.class ) );
return meta;
}
private DatabaseLookupData createDatabaseData() {
return new DatabaseLookupData();
}
private Database createVirtualDb( DatabaseMeta meta ) throws Exception {
ResultSet rs = mock( ResultSet.class );
when( rs.getMetaData() ).thenReturn( mock( ResultSetMetaData.class ) );
PreparedStatement ps = mock( PreparedStatement.class );
when( ps.executeQuery() ).thenReturn( rs );
Connection connection = mock( Connection.class );
when( connection.prepareStatement( anyString() ) ).thenReturn( ps );
Database db = new Database( mock( LoggingObjectInterface.class ), meta );
db.setConnection( connection );
db = spy( db );
doNothing().when( db ).normalConnect( anyString() );
ValueMetaInterface binary = new ValueMetaString( BINARY_FIELD );
binary.setStorageType( ValueMetaInterface.STORAGE_TYPE_BINARY_STRING );
ValueMetaInterface id = new ValueMetaInteger( ID_FIELD );
RowMetaInterface metaByQuerying = new RowMeta();
metaByQuerying.addValueMeta( binary );
metaByQuerying.addValueMeta( id );
doReturn( metaByQuerying ).when( db ).getTableFields( anyString() );
return db;
}
private DatabaseLookup spyLookup( StepMockHelper<DatabaseLookupMeta, DatabaseLookupData> mocks, Database db,
DatabaseMeta dbMeta ) {
DatabaseLookup lookup =
new DatabaseLookup( mocks.stepMeta, mocks.stepDataInterface, 1, mocks.transMeta, mocks.trans );
lookup = Mockito.spy( lookup );
doReturn( db ).when( lookup ).getDatabase( eq( dbMeta ) );
for ( RowSet rowSet : lookup.getOutputRowSets() ) {
if ( mockingDetails( rowSet ).isMock() ) {
when( rowSet.putRow( any( RowMetaInterface.class ), any( Object[].class ) ) ).thenReturn( true );
}
}
return lookup;
}
@Test
public void testEqualsAndIsNullAreCached() throws Exception {
StepMockHelper<DatabaseLookupMeta, DatabaseLookupData> mockHelper =
new StepMockHelper<>( "Test", DatabaseLookupMeta.class, DatabaseLookupData.class );
when( mockHelper.logChannelInterfaceFactory.create( any(), any( LoggingObjectInterface.class ) ) )
.thenReturn( mockHelper.logChannelInterface );
DatabaseLookup look =
new MockDatabaseLookup( mockHelper.stepMeta, mockHelper.stepDataInterface, 0, mockHelper.transMeta,
mockHelper.trans );
DatabaseLookupData lookData = new DatabaseLookupData();
lookData.cache = DefaultCache.newCache( lookData, 0 );
lookData.lookupMeta = new RowMeta();
MySQLDatabaseMeta mysql = new MySQLDatabaseMeta();
mysql.setName( "MySQL" );
DatabaseMeta dbMeta = new DatabaseMeta();
dbMeta.setDatabaseInterface( mysql );
DatabaseLookupMeta meta = new DatabaseLookupMeta();
meta.setDatabaseMeta( dbMeta );
meta.setTablename( "VirtualTable" );
meta.setTableKeyField( new String[] { "ID1", "ID2" } );
meta.setKeyCondition( new String[] { "=", "IS NULL" } );
meta.setReturnValueNewName( new String[] { "val1", "val2" } );
meta.setReturnValueField( new String[] { BINARY_FIELD, BINARY_FIELD } );
meta.setReturnValueDefaultType( new int[] { ValueMetaInterface.TYPE_BINARY, ValueMetaInterface.TYPE_BINARY } );
meta.setStreamKeyField1( new String[ 0 ] );
meta.setStreamKeyField2( new String[ 0 ] );
meta.setReturnValueDefault( new String[] { "", "" } );
meta = spy( meta );
doAnswer( new Answer() {
@Override public Object answer( InvocationOnMock invocation ) throws Throwable {
RowMetaInterface row = (RowMetaInterface) invocation.getArguments()[ 0 ];
ValueMetaInterface v = new ValueMetaBinary( BINARY_FIELD );
row.addValueMeta( v );
return null;
}
} ).when( meta ).getFields(
any( RowMetaInterface.class ),
anyString(),
any( RowMetaInterface[].class ),
any( StepMeta.class ),
any( VariableSpace.class ),
any( Repository.class ),
any( IMetaStore.class ) );
look.init( meta, lookData );
assertTrue( lookData.allEquals ); // Test for fix on PDI-15202
}
@Test
public void getRowInCacheTest() throws KettleException {
StepMockHelper<DatabaseLookupMeta, DatabaseLookupData> mockHelper =
new StepMockHelper<>( "Test", DatabaseLookupMeta.class, DatabaseLookupData.class );
when( mockHelper.logChannelInterfaceFactory.create( any(), any( LoggingObjectInterface.class ) ) )
.thenReturn( mockHelper.logChannelInterface );
DatabaseLookup look =
new DatabaseLookup( mockHelper.stepMeta, mockHelper.stepDataInterface, 0, mockHelper.transMeta,
mockHelper.trans );
DatabaseLookupData lookData = new DatabaseLookupData();
lookData.cache = DefaultCache.newCache( lookData, 0 );
lookData.lookupMeta = new RowMeta();
look.init( new DatabaseLookupMeta(), lookData );
ValueMetaInterface valueMeta = new ValueMetaInteger( "fieldTest" );
RowMeta lookupMeta = new RowMeta();
lookupMeta.setValueMetaList( Collections.singletonList( valueMeta ) );
Object[] kgsRow1 = new Object[ 1 ];
kgsRow1[ 0 ] = 1L;
Object[] kgsRow2 = new Object[ 1 ];
kgsRow2[ 0 ] = 2L;
Object[] add1 = new Object[ 1 ];
add1[ 0 ] = 10L;
Object[] add2 = new Object[ 1 ];
add2[ 0 ] = 20L;
lookData.cache.storeRowInCache( mockHelper.processRowsStepMetaInterface, lookupMeta, kgsRow1, add1 );
lookData.cache.storeRowInCache( mockHelper.processRowsStepMetaInterface, lookupMeta, kgsRow2, add2 );
Object[] rowToCache = new Object[ 1 ];
rowToCache[ 0 ] = 0L;
lookData.conditions = new int[ 1 ];
lookData.conditions[ 0 ] = DatabaseLookupMeta.CONDITION_GE;
Object[] dataFromCache = lookData.cache.getRowFromCache( lookupMeta, rowToCache );
assertArrayEquals( dataFromCache, add1 );
}
@Test
public void createsReadOnlyCache_WhenReadAll_AndNotAllEquals() throws Exception {
DatabaseLookupData data = getCreatedData( false );
assertThat( data.cache, is( instanceOf( ReadAllCache.class ) ) );
}
@Test
public void createsReadDefaultCache_WhenReadAll_AndAllEquals() throws Exception {
DatabaseLookupData data = getCreatedData( true );
assertThat( data.cache, is( instanceOf( DefaultCache.class ) ) );
}
private DatabaseLookupData getCreatedData( boolean allEquals ) throws Exception {
Database db = mock( Database.class );
when( db.getRows( anyString(), anyInt() ) )
.thenReturn( Collections.singletonList( new Object[] { 1L } ) );
RowMeta returnRowMeta = new RowMeta();
returnRowMeta.addValueMeta( new ValueMetaInteger() );
when( db.getReturnRowMeta() ).thenReturn( returnRowMeta );
StepMockHelper<DatabaseLookupMeta, DatabaseLookupData> mockHelper = createMockHelper();
DatabaseLookupMeta meta = createTestMeta();
DatabaseLookupData data = new DatabaseLookupData();
DatabaseLookup step = createSpiedStep( db, mockHelper, meta );
step.init( meta, data );
data.db = db;
data.keytypes = new int[] { ValueMetaInterface.TYPE_INTEGER };
if ( allEquals ) {
data.allEquals = true;
data.conditions = new int[] { DatabaseLookupMeta.CONDITION_EQ };
} else {
data.allEquals = false;
data.conditions = new int[] { DatabaseLookupMeta.CONDITION_LT };
}
step.processRow( meta, data );
return data;
}
private DatabaseLookupMeta createTestMeta() {
DatabaseLookupMeta meta = new DatabaseLookupMeta();
meta.setCached( true );
meta.setLoadingAllDataInCache( true );
meta.setDatabaseMeta( mock( DatabaseMeta.class ) );
// it's ok here, we won't do actual work
meta.allocate( 1, 0 );
meta.setStreamKeyField1( new String[] { "Test" } );
return meta;
}
private DatabaseLookup createSpiedStep( Database db,
StepMockHelper<DatabaseLookupMeta, DatabaseLookupData> mockHelper,
DatabaseLookupMeta meta ) throws KettleException {
DatabaseLookup step = spyLookup( mockHelper, db, meta.getDatabaseMeta() );
doNothing().when( step ).determineFieldsTypesQueryingDb();
doReturn( null ).when( step ).lookupValues( any( RowMetaInterface.class ), any( Object[].class ) );
RowMeta input = new RowMeta();
input.addValueMeta( new ValueMetaInteger( "Test" ) );
step.setInputRowMeta( input );
return step;
}
@Test
public void createsReadDefaultCache_AndUsesOnlyNeededFieldsFromMeta() throws Exception {
Database db = mock( Database.class );
when( db.getRows( anyString(), anyInt() ) )
.thenReturn( Arrays.asList( new Object[] { 1L }, new Object[] { 2L } ) );
RowMeta returnRowMeta = new RowMeta();
returnRowMeta.addValueMeta( new ValueMetaInteger() );
returnRowMeta.addValueMeta( new ValueMetaInteger() );
when( db.getReturnRowMeta() ).thenReturn( returnRowMeta );
StepMockHelper<DatabaseLookupMeta, DatabaseLookupData> mockHelper = createMockHelper();
DatabaseLookupMeta meta = createTestMeta();
DatabaseLookupData data = new DatabaseLookupData();
DatabaseLookup step = createSpiedStep( db, mockHelper, meta );
step.init( meta, data );
data.db = db;
data.keytypes = new int[] { ValueMetaInterface.TYPE_INTEGER };
data.allEquals = true;
data.conditions = new int[] { DatabaseLookupMeta.CONDITION_EQ };
step.processRow( meta, data );
data.lookupMeta = new RowMeta();
data.lookupMeta.addValueMeta( new ValueMetaInteger() );
assertNotNull( data.cache.getRowFromCache( data.lookupMeta, new Object[] { 1L } ) );
assertNotNull( data.cache.getRowFromCache( data.lookupMeta, new Object[] { 2L } ) );
}
public class MockDatabaseLookup extends DatabaseLookup {
public MockDatabaseLookup( StepMeta stepMeta, StepDataInterface stepDataInterface, int copyNr, TransMeta transMeta, Trans trans ) {
super( stepMeta, stepDataInterface, copyNr, transMeta, trans );
}
@Override
Database getDatabase( DatabaseMeta meta ) {
try {
return createVirtualDb( meta );
} catch ( Exception ex ) {
throw new RuntimeException( ex );
}
}
}
}