package me.prettyprint.cassandra.model; import java.nio.ByteBuffer; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import me.prettyprint.cassandra.model.thrift.ThriftConverter; import me.prettyprint.cassandra.serializers.StringSerializer; import me.prettyprint.cassandra.service.Operation; import me.prettyprint.cassandra.service.OperationType; import me.prettyprint.hector.api.HConsistencyLevel; import me.prettyprint.hector.api.Keyspace; import me.prettyprint.hector.api.Serializer; import me.prettyprint.hector.api.exceptions.HectorException; import me.prettyprint.hector.api.query.QueryResult; import org.apache.cassandra.thrift.ConsistencyLevel; import org.apache.cassandra.thrift.Column; import org.apache.cassandra.thrift.Compression; import org.apache.cassandra.thrift.CqlResult; import org.apache.cassandra.thrift.CqlRow; import org.apache.cassandra.thrift.Cassandra.Client; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * First cut at a CQL implementation. Not too much time has been spent here * as this API is currently a moving target. We have a lot of experience with * these hijinks by the Apache Cassandra team, so it was deemed prudent to do * something simple initially until the dust settles. * * You are expected to know what you are getting into if you plan on using * CQL queries in your application. Spend some time looking through the * unit tests here in Hector and the Cassandra source tree. For a number of * detailed examples, see test_cql.py in the test/system folder of the * Apache Cassandra source distribution. * * Note: if you immediately get an exception such as: * "InvalidRequestException(why:cannot parse 'foo' as hex bytes)" * It means one of two things: * <ol> * <li>you have not formatted your query correct</li> * <li>You have not configured the correct validators on your column family</li> * </ol> * * In both cases, even though the query is most likely a string, it is up to you to format * this query according to the comparator (used for the column name), key validator * and value validator. This can be a little confusing as only the comparator is required. * The other two default to BytesType. * * See the docs on {@link CqlRows} for additional details. * * @author zznate * */ public class CqlQuery<K, N, V> extends AbstractBasicQuery<K, N, CqlRows<K,N,V>> { private static Logger log = LoggerFactory.getLogger(CqlQuery.class); private Serializer<V> valueSerializer; private ByteBuffer query; private boolean useCompression; private boolean suppressKeyInColumns; public CqlQuery(Keyspace k, Serializer<K> keySerializer, Serializer<N> nameSerializer, Serializer<V> valueSerializer) { super(k, keySerializer, nameSerializer); this.valueSerializer = valueSerializer; } /** * Set the query as a String. Here for convienience. See above for some * caveats. Calls {@link StringSerializer#toByteBuffer(String)} directly. * @param query * @return */ public CqlQuery<K, N, V> setQuery(String query) { this.query = StringSerializer.get().toByteBuffer(query); return this; } public CqlQuery<K, N, V> setQuery(ByteBuffer qeury) { this.query = qeury; return this; } /** * Method will check to see if the version starts with "3" and set * the boolean cql3 to true. This makes it easier for execute * method and avoids doing lots of cqlVersion.startsWith() tests. */ @Override public CqlQuery<K, N, V> setCqlVersion(String version){ this.cqlVersion=version; if (this.cqlVersion.startsWith("3")) { cql3 = true; } return this; } public CqlQuery<K, N, V> setSuppressKeyInColumns(boolean suppressKeyInColumns) { this.suppressKeyInColumns = suppressKeyInColumns; return this; } public CqlQuery<K, N, V> useCompression() { useCompression = true; return this; } protected HConsistencyLevel getConsistency() { if (consistency != null) { return consistency; } else { return HConsistencyLevel.ONE; } } /** * For future releases, we should consider refactoring how execute * method converts the thrift CqlResult to Hector CqlRows. Starting * with CQL3 the KEY is no longer returned by default and is no * longer treated as a "special" thing. To get the KEY from the * resultset, we have to look at the columns. Using the KEY * in CqlRows is nice, and makes it easy to get results by the KEY. * The downside is that users have to explicitly include the KEY in * the statement. Changing how CqlRows work "might" break some * existing use cases. For now, the implementation finds the column * and expects the statement to have the KEY. */ @Override public QueryResult<CqlRows<K, N, V>> execute() { return new QueryResultImpl<CqlRows<K, N, V>>( keyspace.doExecuteOperation(new Operation<CqlRows<K, N, V>>(OperationType.READ) { @Override public CqlRows<K, N, V> execute(Client cassandra) throws HectorException { CqlRows<K, N, V> rows = null; try { if (cqlVersion != null) { cassandra.set_cql_version(cqlVersion); } CqlResult result = null; if (cql3) { result = cassandra.execute_cql3_query(query, useCompression ? Compression.GZIP : Compression.NONE, ThriftConverter.consistencyLevel(getConsistency())); } else { result = cassandra.execute_cql_query(query, useCompression ? Compression.GZIP : Compression.NONE); } if ( log.isDebugEnabled() ) { log.debug("Found CqlResult: {}", result); } switch (result.getType()) { case VOID: rows = new CqlRows<K, N, V>(); break; default: if ( result.getRowsSize() > 0 ) { LinkedHashMap<ByteBuffer, List<Column>> ret = new LinkedHashMap<ByteBuffer, List<Column>>(result.getRowsSize()); int keyColumnIndex = -1; for (Iterator<CqlRow> rowsIter = result.getRowsIterator(); rowsIter.hasNext(); ) { CqlRow row = rowsIter.next(); ByteBuffer kbb = ByteBuffer.wrap(row.getKey()); // if CQL3 is used row.getKey() always returns null, so we have // to find the KEY in the columns. if (cql3) { List<Column> rowcolumns = row.getColumns(); // first time through we find the column and then use the // column index to avoid needless work. if (keyColumnIndex == -1) { for (Column c: rowcolumns) { keyColumnIndex++; String name = StringSerializer.get().fromBytes(c.getName()); if (name.toUpperCase().equals("KEY")) { kbb = ByteBuffer.wrap(c.getValue()); break; } } } else { kbb = ByteBuffer.wrap(row.getColumns().get(keyColumnIndex).getValue()); } } ret.put(kbb, filterKeyColumn(row)); } Map<K, List<Column>> thriftRet = keySerializer.fromBytesMap(ret); rows = new CqlRows<K, N, V>((LinkedHashMap<K, List<Column>>)thriftRet, columnNameSerializer, valueSerializer); } break; } } catch (Exception ex) { throw keyspace.getExceptionsTranslator().translate(ex); } return rows; } }), this); } /* * Trims the first column from the row if it's name is equal to "KEY" */ private List<Column> filterKeyColumn(CqlRow row) { if ( suppressKeyInColumns && row.isSetColumns() && row.columns.size() > 0) { Iterator<Column> columnsIterator = row.getColumnsIterator(); Column column = columnsIterator.next(); if ( column.name.duplicate().equals(KEY_BB) ) { columnsIterator.remove(); } } return row.getColumns(); } private static ByteBuffer KEY_BB = StringSerializer.get().toByteBuffer("KEY"); }