/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.cassandra.cql3;
import java.nio.ByteBuffer;
import java.util.*;
import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap;
import org.antlr.runtime.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.cql3.statements.*;
import org.apache.cassandra.transport.messages.ResultMessage;
import org.apache.cassandra.db.*;
import org.apache.cassandra.exceptions.*;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.service.QueryState;
import org.apache.cassandra.tracing.Tracing;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.MD5Digest;
import org.apache.cassandra.utils.SemanticVersion;
public class QueryProcessor
{
public static final SemanticVersion CQL_VERSION = new SemanticVersion("3.1.1");
private static final Logger logger = LoggerFactory.getLogger(QueryProcessor.class);
public static final int MAX_CACHE_PREPARED = 100000; // Enough to keep buggy clients from OOM'ing us
private static final Map<MD5Digest, CQLStatement> preparedStatements = new ConcurrentLinkedHashMap.Builder<MD5Digest, CQLStatement>()
.maximumWeightedCapacity(MAX_CACHE_PREPARED)
.build();
private static final Map<Integer, CQLStatement> thriftPreparedStatements = new ConcurrentLinkedHashMap.Builder<Integer, CQLStatement>()
.maximumWeightedCapacity(MAX_CACHE_PREPARED)
.build();
public static CQLStatement getPrepared(MD5Digest id)
{
return preparedStatements.get(id);
}
public static CQLStatement getPrepared(Integer id)
{
return thriftPreparedStatements.get(id);
}
public static void validateKey(ByteBuffer key) throws InvalidRequestException
{
if (key == null || key.remaining() == 0)
{
throw new InvalidRequestException("Key may not be empty");
}
// check that key can be handled by FBUtilities.writeShortByteArray
if (key.remaining() > FBUtilities.MAX_UNSIGNED_SHORT)
{
throw new InvalidRequestException("Key length of " + key.remaining() +
" is longer than maximum of " + FBUtilities.MAX_UNSIGNED_SHORT);
}
}
public static void validateColumnNames(Iterable<ByteBuffer> columns)
throws InvalidRequestException
{
for (ByteBuffer name : columns)
{
if (name.remaining() > Column.MAX_NAME_LENGTH)
throw new InvalidRequestException(String.format("column name is too long (%s > %s)",
name.remaining(),
Column.MAX_NAME_LENGTH));
if (name.remaining() == 0)
throw new InvalidRequestException("zero-length column name");
}
}
private static ResultMessage processStatement(CQLStatement statement, QueryState queryState, QueryOptions options)
throws RequestExecutionException, RequestValidationException
{
logger.trace("Process {} @CL.{}", statement, options.getConsistency());
ClientState clientState = queryState.getClientState();
statement.checkAccess(clientState);
statement.validate(clientState);
ResultMessage result = statement.execute(queryState, options);
return result == null ? new ResultMessage.Void() : result;
}
public static ResultMessage process(String queryString, ConsistencyLevel cl, QueryState queryState)
throws RequestExecutionException, RequestValidationException
{
return process(queryString, queryState, new QueryOptions(cl, Collections.<ByteBuffer>emptyList()));
}
public static ResultMessage process(String queryString, QueryState queryState, QueryOptions options)
throws RequestExecutionException, RequestValidationException
{
CQLStatement prepared = getStatement(queryString, queryState.getClientState()).statement;
if (prepared.getBoundsTerms() != options.getValues().size())
throw new InvalidRequestException("Invalid amount of bind variables");
return processStatement(prepared, queryState, options);
}
public static CQLStatement parseStatement(String queryStr, QueryState queryState) throws RequestValidationException
{
return getStatement(queryStr, queryState.getClientState()).statement;
}
public static UntypedResultSet process(String query, ConsistencyLevel cl) throws RequestExecutionException
{
try
{
ResultMessage result = process(query, QueryState.forInternalCalls(), new QueryOptions(cl, Collections.<ByteBuffer>emptyList()));
if (result instanceof ResultMessage.Rows)
return new UntypedResultSet(((ResultMessage.Rows)result).result);
else
return null;
}
catch (RequestValidationException e)
{
throw new RuntimeException(e);
}
}
public static UntypedResultSet processInternal(String query)
{
try
{
ClientState state = ClientState.forInternalCalls();
QueryState qState = new QueryState(state);
state.setKeyspace(Keyspace.SYSTEM_KS);
CQLStatement statement = getStatement(query, state).statement;
statement.validate(state);
ResultMessage result = statement.executeInternal(qState);
if (result instanceof ResultMessage.Rows)
return new UntypedResultSet(((ResultMessage.Rows)result).result);
else
return null;
}
catch (RequestExecutionException e)
{
throw new RuntimeException(e);
}
catch (RequestValidationException e)
{
throw new RuntimeException("Error validating " + query, e);
}
}
public static UntypedResultSet resultify(String query, Row row)
{
try
{
SelectStatement ss = (SelectStatement) getStatement(query, null).statement;
ResultSet cqlRows = ss.process(Collections.singletonList(row));
return new UntypedResultSet(cqlRows);
}
catch (RequestValidationException e)
{
throw new AssertionError(e);
}
}
public static ResultMessage.Prepared prepare(String queryString, ClientState clientState, boolean forThrift)
throws RequestValidationException
{
ParsedStatement.Prepared prepared = getStatement(queryString, clientState);
ResultMessage.Prepared msg = storePreparedStatement(queryString, clientState.getRawKeyspace(), prepared, forThrift);
assert prepared.statement.getBoundsTerms() == prepared.boundNames.size();
return msg;
}
private static ResultMessage.Prepared storePreparedStatement(String queryString, String keyspace, ParsedStatement.Prepared prepared, boolean forThrift)
{
// Concatenate the current keyspace so we don't mix prepared statements between keyspace (#5352).
// (if the keyspace is null, queryString has to have a fully-qualified keyspace so it's fine.
String toHash = keyspace == null ? queryString : keyspace + queryString;
if (forThrift)
{
int statementId = toHash.hashCode();
thriftPreparedStatements.put(statementId, prepared.statement);
logger.trace(String.format("Stored prepared statement #%d with %d bind markers",
statementId,
prepared.statement.getBoundsTerms()));
return ResultMessage.Prepared.forThrift(statementId, prepared.boundNames);
}
else
{
MD5Digest statementId = MD5Digest.compute(toHash);
logger.trace(String.format("Stored prepared statement %s with %d bind markers",
statementId,
prepared.statement.getBoundsTerms()));
preparedStatements.put(statementId, prepared.statement);
return new ResultMessage.Prepared(statementId, prepared);
}
}
public static ResultMessage processPrepared(CQLStatement statement, QueryState queryState, QueryOptions options)
throws RequestExecutionException, RequestValidationException
{
List<ByteBuffer> variables = options.getValues();
// Check to see if there are any bound variables to verify
if (!(variables.isEmpty() && (statement.getBoundsTerms() == 0)))
{
if (variables.size() != statement.getBoundsTerms())
throw new InvalidRequestException(String.format("there were %d markers(?) in CQL but %d bound variables",
statement.getBoundsTerms(),
variables.size()));
// at this point there is a match in count between markers and variables that is non-zero
if (logger.isTraceEnabled())
for (int i = 0; i < variables.size(); i++)
logger.trace("[{}] '{}'", i+1, variables.get(i));
}
return processStatement(statement, queryState, options);
}
public static ResultMessage processBatch(BatchStatement batch, ConsistencyLevel cl, QueryState queryState, List<List<ByteBuffer>> variables)
throws RequestExecutionException, RequestValidationException
{
ClientState clientState = queryState.getClientState();
batch.checkAccess(clientState);
batch.validate(clientState);
batch.executeWithPerStatementVariables(cl, queryState, variables);
return new ResultMessage.Void();
}
private static ParsedStatement.Prepared getStatement(String queryStr, ClientState clientState)
throws RequestValidationException
{
Tracing.trace("Parsing {}", queryStr);
ParsedStatement statement = parseStatement(queryStr);
// Set keyspace for statement that require login
if (statement instanceof CFStatement)
((CFStatement)statement).prepareKeyspace(clientState);
Tracing.trace("Preparing statement");
return statement.prepare();
}
public static ParsedStatement parseStatement(String queryStr) throws SyntaxException
{
try
{
// Lexer and parser
CharStream stream = new ANTLRStringStream(queryStr);
CqlLexer lexer = new CqlLexer(stream);
TokenStream tokenStream = new CommonTokenStream(lexer);
CqlParser parser = new CqlParser(tokenStream);
// Parse the query string to a statement instance
ParsedStatement statement = parser.query();
// The lexer and parser queue up any errors they may have encountered
// along the way, if necessary, we turn them into exceptions here.
lexer.throwLastRecognitionError();
parser.throwLastRecognitionError();
return statement;
}
catch (RuntimeException re)
{
throw new SyntaxException(String.format("Failed parsing statement: [%s] reason: %s %s",
queryStr,
re.getClass().getSimpleName(),
re.getMessage()));
}
catch (RecognitionException e)
{
throw new SyntaxException("Invalid or malformed CQL query string: " + e.getMessage());
}
}
}