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<Object> 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<CqlRowSliceQueryImpl<?,?>> SelectAllColumnsForRowRange = new QueryGenCache<CqlRowSliceQueryImpl<?,?>>(sessionRef) { @Override public Callable<RegularStatement> getQueryGen(final CqlRowSliceQueryImpl<?, ?> rowSliceQuery) { return new Callable<RegularStatement>() { @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<Object> values = new ArrayList<Object>(); bindWhereClauseForRowRange(values, rowSliceQuery.getRowSlice().getRange()); return pStatement.bind(values.toArray(new Object[values.size()])); } }; private QueryGenCache<CqlRowSliceQueryImpl<?,?>> SelectColumnSetForRowRange = new QueryGenCache<CqlRowSliceQueryImpl<?,?>>(sessionRef) { @Override public Callable<RegularStatement> getQueryGen(final CqlRowSliceQueryImpl<?, ?> rowSliceQuery) { return new Callable<RegularStatement>() { @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<Object> values = new ArrayList<Object>(); 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<CqlRowSliceQueryImpl<?,?>> SelectColumnRangeForRowRange = new QueryGenCache<CqlRowSliceQueryImpl<?,?>>(sessionRef) { @Override public Callable<RegularStatement> getQueryGen(final CqlRowSliceQueryImpl<?, ?> rowSliceQuery) { return new Callable<RegularStatement>() { @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<Object> values = new ArrayList<Object>(); 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<CqlRowSliceQueryImpl<?,?>> SelectCompositeColumnRangeForRowRange = new QueryGenCache<CqlRowSliceQueryImpl<?,?>>(sessionRef) { @Override public Callable<RegularStatement> getQueryGen(final CqlRowSliceQueryImpl<?, ?> rowSliceQuery) { return new Callable<RegularStatement>() { @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<Object> values = new ArrayList<Object>(); 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."); } } }