All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.netflix.astyanax.cql.reads.CFRowRangeQueryGen Maven / Gradle / Ivy

package com.netflix.astyanax.cql.reads;

import static com.datastax.driver.core.querybuilder.QueryBuilder.gte;
import static com.datastax.driver.core.querybuilder.QueryBuilder.in;
import static com.datastax.driver.core.querybuilder.QueryBuilder.lte;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Callable;

import com.datastax.driver.core.BoundStatement;
import com.datastax.driver.core.PreparedStatement;
import com.datastax.driver.core.RegularStatement;
import com.datastax.driver.core.Session;
import com.datastax.driver.core.querybuilder.Select;
import com.datastax.driver.core.querybuilder.Select.Where;
import com.netflix.astyanax.cql.reads.model.CqlColumnSlice;
import com.netflix.astyanax.cql.reads.model.CqlRowSlice.RowRange;
import com.netflix.astyanax.cql.schema.CqlColumnFamilyDefinitionImpl;
import com.netflix.astyanax.serializers.CompositeRangeBuilder.CompositeByteBufferRange;

/**
 * This class encapsulates all the query generators for row range queries. There are different row query
 * generators depending on the specific query signature. 
 * 
 * e.g 
 * 1. Select all columns for all the rows in the row range
 * 2. Select row ranges with column slice
 * 3. Select row ranges with column range
 * 4. Select row ranges using a composite range builder for composite column based schema
 * 
 * Note that for simplicity and brevity, there is another class that handles similar operations for queries that 
 * specify a collection of row keys as opposed to a row range. 
 * See {@link CFRowKeysQueryGen} for that implementation. The current class is meant for row range queries only.  
 * 
 * Each of the query generators uses the {@link QueryGenCache} so that it can cache the {@link PreparedStatement} as well
 * for future use by queries with the same signatures.
 * 
 * But one must use this with care, since the subsequent query must have the exact signature, else binding values with 
 * the previously constructed prepared statement will break. 
 * 
 * Here is a simple example of a bad query that is not cacheable. 
 * 
 * Say that we want a simple query with a column range in it. 
 * 
 *     ks.prepareQuery(myCF)
 *       .getRow("1")
 *       .withColumnSlice("colStart")
 *       .execute();
 *       
 *     In most cases this query lends itself to a CQL3 representation as follows
 *     
 *     SELECT * FROM ks.mfCF WHERE KEY = ? AND COLUMN1 > ?;
 *     
 * Now say that we want  to perform a successive query (with caching turned ON), but add to the column range query
 *    
 *     ks.prepareQuery(myCF)
 *       .getRow("1")
 *       .withColumnSlice("colStart", "colEnd")
 *       .execute();
 *       
 *     NOTE THE USE OF BOTH colStart AND colEnd     <----- THIS IS A DIFFERENT QUERY SIGNATURE
 *     AND THE CQL QUERY WILL PROBABLY LOOK LIKE 
 *     
 *     SELECT * FROM ks.mfCF WHERE KEY = ? AND COLUMN1 > ?  AND COLUMN1 < ?;     <----- NOTE THE EXTRA BIND MARKER AT THE END FOR THE colEnd
 * 
 * If we re-use the previously cached prepared statement, then it will not work for the new query signature. The way out of this is to NOT
 * use caching with different query signatures. 
 * 
 * @author poberai
 *
 */
public class CFRowRangeQueryGen extends CFRowSliceQueryGen {

	/**
	 * Constructor
	 * 
	 * @param session
	 * @param keyspaceName
	 * @param cfDefinition
	 */
	public CFRowRangeQueryGen(Session session, String keyspaceName, CqlColumnFamilyDefinitionImpl cfDefinition) {
		super(session, keyspaceName, cfDefinition);
	}

	/**
	 * Private helper for constructing the where clause for row ranges
	 * @param keyAlias
	 * @param select
	 * @param rowRange
	 * @return
	 */
	private Where addWhereClauseForRowRange(String keyAlias, Select select, RowRange rowRange) {

		Where where = null;

		boolean keyIsPresent = false;
		boolean tokenIsPresent = false; 

		if (rowRange.getStartKey() != null || rowRange.getEndKey() != null) {
			keyIsPresent = true;
		}
		if (rowRange.getStartToken() != null || rowRange.getEndToken() != null) {
			tokenIsPresent = true;
		}

		if (keyIsPresent && tokenIsPresent) {
			throw new RuntimeException("Cannot provide both token and keys for range query");
		}
		
		if (keyIsPresent) {
			if (rowRange.getStartKey() != null && rowRange.getEndKey() != null) {

				where = select.where(gte(keyAlias, BIND_MARKER))
						.and(lte(keyAlias, BIND_MARKER));

			} else if (rowRange.getStartKey() != null) {				
				where = select.where(gte(keyAlias, BIND_MARKER));

			} else if (rowRange.getEndKey() != null) {
				where = select.where(lte(keyAlias, BIND_MARKER));
			}

		} else if (tokenIsPresent) {
			String tokenOfKey ="token(" + keyAlias + ")";

			if (rowRange.getStartToken() != null && rowRange.getEndToken() != null) {

				where = select.where(gte(tokenOfKey, BIND_MARKER))
						.and(lte(tokenOfKey, BIND_MARKER));

			} else if (rowRange.getStartToken() != null) {
				where = select.where(gte(tokenOfKey, BIND_MARKER));

			} else if (rowRange.getEndToken() != null) {
				where = select.where(lte(tokenOfKey, BIND_MARKER));
			}
		} else { 
			where = select.where();
		}

		if (rowRange.getCount() > 0) {
			// TODO: fix this
			//where.limit(rowRange.getCount());
		}
		return where; 
	}

	/**
	 * Private helper for constructing the bind values for the given row range. Note that the assumption here is that 
	 * we have a previously constructed prepared statement that we can bind these values with. 
	 * 
	 * @param keyAlias
	 * @param select
	 * @param rowRange
	 * @return
	 */
	private void bindWhereClauseForRowRange(List values, RowRange rowRange) {

		boolean keyIsPresent = false;
		boolean tokenIsPresent = false; 

		if (rowRange.getStartKey() != null || rowRange.getEndKey() != null) {
			keyIsPresent = true;
		}
		if (rowRange.getStartToken() != null || rowRange.getEndToken() != null) {
			tokenIsPresent = true;
		}

		if (keyIsPresent && tokenIsPresent) {
			throw new RuntimeException("Cannot provide both token and keys for range query");
		}

		if (keyIsPresent) {
			if (rowRange.getStartKey() != null) {
				values.add(rowRange.getStartKey());
			}
			if (rowRange.getEndKey() != null) {
				values.add(rowRange.getEndKey());
			}

		} else if (tokenIsPresent) {

			BigInteger startTokenB = rowRange.getStartToken() != null ? new BigInteger(rowRange.getStartToken()) : null; 
			BigInteger endTokenB = rowRange.getEndToken() != null ? new BigInteger(rowRange.getEndToken()) : null; 

			Long startToken = startTokenB.longValue();
			Long endToken = endTokenB.longValue();
			
			if (startToken != null && endToken != null) {
				if (startToken != null) {
					values.add(startToken);
				}
				if (endToken != null) {
					values.add(endToken);
				}
			}

			if (rowRange.getCount() > 0) {
				// TODO: fix this
				//where.limit(rowRange.getCount());
			}
			return; 
		}
	}

	/**
	 * Query generator for selecting all columns for the specified row range. 
	 * 
	 * Note that this object is an implementation of {@link QueryGenCache}
	 * and hence it maintains a cached reference to the previously constructed {@link PreparedStatement} for row range queries with the same 
	 * signature  (i.e all columns)
	 */
	private QueryGenCache> SelectAllColumnsForRowRange = new QueryGenCache>(sessionRef) {

		@Override
		public Callable getQueryGen(final CqlRowSliceQueryImpl rowSliceQuery) {
			return new Callable() {

				@Override
				public RegularStatement call() throws Exception {
					Select select = selectAllColumnsFromKeyspaceAndCF();
					return addWhereClauseForRowRange(partitionKeyCol, select, rowSliceQuery.getRowSlice().getRange());
				}
			};
		}

		@Override
		public BoundStatement bindValues(PreparedStatement pStatement, CqlRowSliceQueryImpl rowSliceQuery) {

			List values = new ArrayList();
			bindWhereClauseForRowRange(values, rowSliceQuery.getRowSlice().getRange());
			return pStatement.bind(values.toArray(new Object[values.size()])); 
		}
	};
	
	private QueryGenCache> SelectColumnSetForRowRange = new QueryGenCache>(sessionRef) {

		@Override
		public Callable getQueryGen(final CqlRowSliceQueryImpl rowSliceQuery) {
			return new Callable() {

				@Override
				public RegularStatement call() throws Exception {
					
					// THIS IS A QUERY WHERE THE COLUMN NAME IS DYNAMIC  E.G TIME SERIES
					RowRange range = rowSliceQuery.getRowSlice().getRange();
					Collection cols = rowSliceQuery.getColumnSlice().getColumns();
					Object[] columns = cols.toArray(new Object[cols.size()]); 

					Select select = selectAllColumnsFromKeyspaceAndCF();

					if (columns != null && columns.length > 0) {
						select.allowFiltering();
					}
					Where where = addWhereClauseForRowRange(partitionKeyCol, select, range);
					where.and(in(clusteringKeyCols.get(0).getName(), columns));

					return where;
				}
			};
		}

		@Override
		public BoundStatement bindValues(PreparedStatement pStatement, CqlRowSliceQueryImpl rowSliceQuery) {
			
			List values = new ArrayList();

			bindWhereClauseForRowRange(values, rowSliceQuery.getRowSlice().getRange());
			values.addAll(rowSliceQuery.getColumnSlice().getColumns());

			return pStatement.bind(values.toArray());
		}
	};

	
	/**
	 * Query generator for selecting a specified column range with a specified row range. 
	 * 
	 * Note that this object is an implementation of {@link QueryGenCache}
	 * and hence it maintains a cached reference to the previously constructed {@link PreparedStatement} for row range queries with the same 
	 * signature  (i.e similar column range for the row range)
	 */
	private QueryGenCache> SelectColumnRangeForRowRange = new QueryGenCache>(sessionRef) {

		@Override
		public Callable getQueryGen(final CqlRowSliceQueryImpl rowSliceQuery) {
			return new Callable() {

				@Override
				public RegularStatement call() throws Exception {

					Select select = selectAllColumnsFromKeyspaceAndCF();
					
					CqlColumnSlice columnSlice = rowSliceQuery.getColumnSlice();
					
					if (columnSlice != null && columnSlice.isRangeQuery()) {
						select.allowFiltering();
					}

					Where where = addWhereClauseForRowRange(partitionKeyCol, select, rowSliceQuery.getRowSlice().getRange());			
					where = addWhereClauseForColumnRange(where, columnSlice);
					return where;
				}
			};
		}

		@Override
		public BoundStatement bindValues(PreparedStatement pStatement, CqlRowSliceQueryImpl rowSliceQuery) {

			List values = new ArrayList();

			bindWhereClauseForRowRange(values, rowSliceQuery.getRowSlice().getRange());
			bindWhereClauseForColumnRange(values, rowSliceQuery.getColumnSlice());
			
			return pStatement.bind(values.toArray());
		}
	};
	
	/**
	 * Query generator for selecting a specified composite column range with a specified row range. 
	 * 
	 * Note that this object is an implementation of {@link QueryGenCache}
	 * and hence it maintains a cached reference to the previously constructed {@link PreparedStatement} for row range queries with the same 
	 * signature  (i.e similar composite column range for the row range)
	 */
	private QueryGenCache> SelectCompositeColumnRangeForRowRange = new QueryGenCache>(sessionRef) {

		@Override
		public Callable getQueryGen(final CqlRowSliceQueryImpl rowSliceQuery) {
			return new Callable() {

				@Override
				public RegularStatement call() throws Exception {
					Select select = selectAllColumnsFromKeyspaceAndCF();
					CompositeByteBufferRange compositeRange = rowSliceQuery.getCompositeRange();
					if (compositeRange != null) {
						select.allowFiltering();
					}

					Where where = addWhereClauseForRowRange(partitionKeyCol, select, rowSliceQuery.getRowSlice().getRange());	
					where = addWhereClauseForCompositeColumnRange(where, compositeRange);
					return where;
				}
				
			};
		}

		@Override
		public BoundStatement bindValues(PreparedStatement pStatement, CqlRowSliceQueryImpl rowSliceQuery) {

			List values = new ArrayList();

			bindWhereClauseForRowRange(values, rowSliceQuery.getRowSlice().getRange());
			bindWhereClauseForCompositeColumnRange(values, rowSliceQuery.getCompositeRange());

			return pStatement.bind(values.toArray());
		}
	};
	
	/**
	 * Main method used to generate the query for the specified row slice query. 
	 * Note that depending on the query signature, the caller may choose to enable/disable caching
	 * 
	 * @param rowSliceQuery: The Astaynax query for which we need to generate a java driver query
	 * @param useCaching: boolean condition indicating whether we should use a previously cached prepared stmt or not. 
	 *                    If false, then the cache is ignored and we generate the prepared stmt for this query
	 *                    If true, then the cached prepared stmt is used. If the cache has not been inited, 
	 *                    then the prepared stmt is constructed for this query and subsequently cached
	 *                    
	 * @return BoundStatement: they statement for this Astyanax query
	 */
	public BoundStatement getQueryStatement(CqlRowSliceQueryImpl rowSliceQuery, boolean useCaching) {

		switch (rowSliceQuery.getColQueryType()) {

		case AllColumns:
			return SelectAllColumnsForRowRange.getBoundStatement(rowSliceQuery, useCaching);
		case ColumnSet: 
			return SelectColumnSetForRowRange.getBoundStatement(rowSliceQuery, useCaching);
		case ColumnRange:
			if (isCompositeColumn) {
				return SelectCompositeColumnRangeForRowRange.getBoundStatement(rowSliceQuery, useCaching);
			} else {
				return SelectColumnRangeForRowRange.getBoundStatement(rowSliceQuery, useCaching);
			}
		default :
			throw new RuntimeException("RowSliceQuery with row range use case not supported.");
		}
	}
}