/*
* (C) Copyright IBM Corp. 2008
*
* LICENSE: Eclipse Public License v1.0
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.ibm.gaiandb;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.SortedMap;
import java.util.concurrent.ConcurrentMap;
import org.apache.derby.iapi.store.access.Qualifier;
import org.apache.derby.iapi.types.DataValueDescriptor;
import com.ibm.gaiandb.diags.GDBMessages;
/**
* @author DavidVyvyan
*/
public class InMemoryRows implements GaianChildVTI {
// Use PROPRIETARY notice if class contains a main() method, otherwise use COPYRIGHT notice.
public static final String COPYRIGHT_NOTICE = "(c) Copyright IBM Corp. 2008";
private static final Logger logger = new Logger( "InMemoryRows", 30 );
// private DataValueDescriptor[][] rows = new DataValueDescriptor[0][], result;
private ArrayList<DataValueDescriptor[]> rows;
private ArrayList<DataValueDescriptor[]> result;
// We use ArrayList instead of Vector because we don't need or want the overhead of synchronization
// private ArrayList rows, result;
private ConcurrentMap<Integer, SortedMap<DataValueDescriptor, Object>> indexes;
private int rowCount;
private int index = 0;
// logical projection is used to only set columns that are selected in the query, which is not necessarily
// all the physical columns mapped to the logical table.
private int[] logicalProjection = null;
private int[] columnsMapping = null;
private int colCount = 0;
// NOTE: The loaded in-memory 'rows' are shared between all instances of InMemoryRows
// These will change when a reload is done, so synchronization is required.
// This is why the execute() and reinitialise() methods of each VTIWrapper are synchronized.
// However this means that the rows Vector can only be worked with within mthods called by one or
// the other of these methods, i.e. setExtractConditions(), but definitely not in next()
// public InMemoryRows( Vector rows ) {
// this.rows = rows;
// }
/*
* This method should not be called whilst the rows are being processed...
*/
// public void setRowsAndIndexes( DataValueDescriptor[][] rows, Hashtable indexes ) {
public void setRowsAndIndexes( ArrayList<DataValueDescriptor[]> rows, ConcurrentMap<Integer, SortedMap<DataValueDescriptor, Object>> indexes ) {
this.rows = rows;
this.indexes = indexes;
}
/**
* qualifiers and pushedProjection respectively represent predicates structure and queried column ids.
* These both reference column ids that are relative to the physical data.
*/
public void setExtractConditions( Qualifier[][] qualifiers, int[] logicalProjection, int[] columnsMapping ) throws Exception {
this.logicalProjection = logicalProjection;
this.columnsMapping = columnsMapping;
logger.logInfo("All query's logical col ids: " + Util.intArrayAsString(logicalProjection) +
", columnsMapping: " + Util.intArrayAsString(columnsMapping) );
colCount = logicalProjection.length;
if ( null == qualifiers || 0 == qualifiers.length )
result = new ArrayList<DataValueDescriptor[]>( rows ); // Disassociate rows from result, so that a cleanup cannot impact the fetch.
else {
// Get a copy of the qualifiers structure, but with column ids swapped to the physical ones.
// Cannot modify the orignal qualifiers structure as is being shared between physical nodes.
Qualifier[][] mappedQualifiers = RowsFilter.getQualifiersDeepCopyWithColumnsMapped( qualifiers, columnsMapping );
logger.logInfo("New Physical Column Mapped Qualifiers: " + RowsFilter.reconstructSQLWhereClause( mappedQualifiers ) +
" #colmappings: " + Util.intArrayAsString(columnsMapping));
// Apply indexes - and prune the mappedQualifiers from them
SortedMap<DataValueDescriptor, Object> remainingMap = RowsFilter.applyAndPruneIndexQualifiers( indexes, mappedQualifiers );
if ( null != remainingMap ) {
// The index reduced the set of keys - now filter the rest using remaining predicates - if any
// Get values - this just creates an inner class impl of AbstractCollection that can work on the TreeMap's values collection
// Getting the values() here does not parse the results...
Collection<Object> c = remainingMap.values();
result = new ArrayList<DataValueDescriptor[]>( c.size() );
Iterator<Object> it = c.iterator();
if ( 0 == mappedQualifiers.length )
while (it.hasNext()) {
Object keyRows = it.next();
if ( keyRows instanceof ArrayList<?> ) {
ArrayList<?> a = (ArrayList<?>) keyRows;
int alen = a.size();
for ( int i=0; i<alen; i++ )
result.add( (DataValueDescriptor[]) a.get(i) );
} else
result.add( (DataValueDescriptor[]) keyRows );
}
else
while (it.hasNext()) {
Object keyRows = it.next();
if ( keyRows instanceof ArrayList<?> ) {
ArrayList<?> a = (ArrayList<?>) keyRows;
int alen = a.size();
for ( int i=0; i<alen; i++ ) {
DataValueDescriptor[] row = (DataValueDescriptor[])a.get(i);
if ( RowsFilter.testQualifiers( row, mappedQualifiers ) )
result.add( row );
}
} else
if ( RowsFilter.testQualifiers( (DataValueDescriptor[])keyRows, mappedQualifiers ) )
result.add( (DataValueDescriptor[]) keyRows );
}
logger.logInfo("Applied remaining predicates on submap of index");
// Copy into array - will parse results through AbstractCollection
// result = (DataValueDescriptor[][]) c.toArray( new DataValueDescriptor[c.size()][] );
} else { //if ( 0 != mappedQualifiers.length ) {
// The index didn't help - and we have qualifiers
// Apply table scan using all qualifiers
int len = rows.size();
result = new ArrayList<DataValueDescriptor[]>( len/10 );
for ( int i=0; i<len; i++ ) {
DataValueDescriptor[] row = (DataValueDescriptor[]) rows.get(i);
if ( RowsFilter.testQualifiers( row, mappedQualifiers ) )
result.add( row );
}
logger.logInfo("Applied remaining predicates on full table scan");
// This is a System.arrayCopy
// result = (DataValueDescriptor[][]) resultList.toArray( new DataValueDescriptor[resultList.size()][] );
}
}
rowCount = result.size();
logger.logInfo("Determined InMemoryRows rowCount = " + rowCount + ", index set to " + index);
}
/**
* This method is not implemented yet - it could be used in future to combine indexes...
* It would AND 2 submaps, each of which had been derived from separate indexes (indexed by a different column).
*
* @param t1
* @param t2
* @return
*/
// private DataValueDescriptor[] mergeTrees( SortedMap<DataValueDescriptor, Object> t1, SortedMap<DataValueDescriptor, Object> t2 ) {
//
// // Algo 1:
// // Iterate over smallest tree's values, inserting them into a new tree indexed by the larger tree's column.
// // Identify the overlapping range between the new tree and the larger tree - get the values.
//
// // Algo 2 (alternative):
// // Get the keySet from the smaller tree and the values from the larger one
// // Iterate over values, building a HashSet by picking out for each row the column that the smaller set is indexed by.
// // keySet.retailAll( <new HashSet> )
// // Get the values from the smaller TreeMap, which should now be even smaller as changes in the keySet are reflected in the Map.
//
// return null;
// }
public void setArgs(String[] args) {
// No variable arguments processing
}
// /* (non-Javadoc)
// * @see java.sql.ResultSet#next()
// */
// public boolean next() throws SQLException {
//
// if ( rowCount <= index ) return false;
// index++;
// return true;
// }
public boolean fetchNextRow( DataValueDescriptor[] row ) throws SQLException {
if ( rowCount <= index ) return false;
DataValueDescriptor[] nextRow = (DataValueDescriptor[]) result.get( index++ ); // result[index++];
if ( Logger.LOG_ALL == Logger.logLevel )
logger.logDetail("New in-mem result row: " + Arrays.asList(nextRow) );
for (int i=0; i<colCount; i++) {
int lcolID = logicalProjection[i]-1;
int pcolID = columnsMapping[lcolID]; // pcolID is the in-mem table col index
// Check if column is not in the in memory rows.
// This happens for columns that were not in the pushed projection for this query.
// Derby is expected to ignore these columns in the row array.
// This may also happen when the column is actually queried (as it exists on other physical sources but not this one)
// A null will be returned in the column for this source in that case - so the user will have a partial result.
// Note that we don't distinguish between null values and non-existant columns... the fact that a column does not
// exist for a given source is considered as it having a null value for it.
// if ( 0 > pcolID ) continue; // if the col is not mapped then pcolID will be numcols+1, which is a null dvd
try {
if ( Logger.LOG_ALL == Logger.logLevel )
logger.logDetail("Setting InMemory col " + (pcolID+1) + " as type " + row[lcolID].getTypeName());
row[lcolID].setValue( nextRow[pcolID] ); // note that nextRow[number of pcols] is a null dvd.. this is used for a non-existant pcol
} catch ( ArrayIndexOutOfBoundsException e ) {
logger.logException( GDBMessages.RESULT_LOGICAL_COLUMN_REF_ERROR, "Error referencing Logical column " + (i+1) + // must be 1-based from user pt of view
" which does not exist in physical table. Null ResultSet will be returned for this node", e);
return false;
} catch (Exception e) {
throw new SQLException( "Unable to set value for in-memory row's column id " + (pcolID+1) +
" to the intended DVD column type: " + row[lcolID].getTypeName() + ", cause: " + e );
}
}
return true;
}
/* (non-Javadoc)
* @see java.sql.ResultSet#close()
*/
public void close() { reinitialise(); }
@Override
public boolean reinitialise() {
if ( null != result && result != rows ) {
result.clear();
result.trimToSize();
}
result = null;
index = 0;
return true;
}
public boolean isBeforeFirst() {
return 0 == index;
}
/* (non-Javadoc)
* @see java.sql.ResultSet#getMetaData()
*/
public ResultSetMetaData getMetaData() throws SQLException {
return null;
}
public int getRowCount() throws Exception {
index = rowCount; // invalidate this structure until close() has been called
return rowCount;
}
public boolean isScrollable() {
return true;
}
}