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

com.datastax.driver.core.querybuilder.BuiltStatement Maven / Gradle / Ivy

/*
 * Copyright DataStax, Inc.
 *
 * Licensed 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 com.datastax.driver.core.querybuilder;

import com.datastax.driver.core.CodecRegistry;
import com.datastax.driver.core.ColumnMetadata;
import com.datastax.driver.core.ConsistencyLevel;
import com.datastax.driver.core.Metadata;
import com.datastax.driver.core.ProtocolVersion;
import com.datastax.driver.core.RegularStatement;
import com.datastax.driver.core.Statement;
import com.datastax.driver.core.TypeCodec;
import com.datastax.driver.core.exceptions.CodecNotFoundException;
import com.datastax.driver.core.policies.RetryPolicy;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * Common ancestor to statements generated with the {@link QueryBuilder}.
 *
 * 

The actual query string will be generated and cached the first time it is requested, which is * either when the driver tries to execute the query, or when you call certain public methods (for * example {@link RegularStatement#getQueryString(CodecRegistry)}, {@link #getObject(int, * CodecRegistry)}). * *

Whenever possible (and unless you call {@link #setForceNoValues(boolean)}, the builder will * try to handle values passed to its methods as standalone values bound to the query string with * placeholders. For instance: * *

 *     select().all().from("foo").where(eq("k", "the key"));
 *     // Is equivalent to:
 *     new SimpleStatement("SELECT * FROM foo WHERE k=?", "the key");
 * 
* * There are a few exceptions to this rule: * *
    *
  • for fixed-size number types, the builder can't guess what the actual CQL type is. * Standalone values are sent to Cassandra in their serialized form, and number types aren't * all serialized in the same way, so picking the wrong type could lead to a query error; *
  • if the value is a "special" element like a function call, it can't be serialized. This also * applies to collections mixing special elements and regular objects. *
* * In these cases, the builder will inline the value in the query string: * *
 *     select().all().from("foo").where(eq("k", 1));
 *     // Is equivalent to:
 *     new SimpleStatement("SELECT * FROM foo WHERE k=1");
 * 
* * One final thing to consider is {@link CodecRegistry custom codecs}. If you've registered codecs * to handle your own Java types against the cluster, then you can pass instances of those types to * query builder methods. But should the builder have to inline those values, it needs your codecs * to {@link TypeCodec#format(Object) convert them to string form}. That is why some of the public * methods of this class take a {@code CodecRegistry} as a parameter: * *
 *     BuiltStatement s = select().all().from("foo").where(eq("k", myCustomObject));
 *     // if we do this codecs will definitely be needed:
 *     s.forceNoValues(true);
 *     s.getQueryString(myCodecRegistry);
 * 
* * For convenience, there are no-arg versions of those methods that use {@link * CodecRegistry#DEFAULT_INSTANCE}. But you should only use them if you are sure that no custom * values will need to be inlined while building the statement, or if you have registered your * custom codecs with the default registry instance. Otherwise, you will get a {@link * CodecNotFoundException}. */ public abstract class BuiltStatement extends RegularStatement { private final List partitionKey; private final List routingKeyValues; final String keyspace; private boolean dirty; private String cache; private List values; Boolean isCounterOp; boolean hasNonIdempotentOps; // Whether the user has inputted bind markers. If that's the case, we never generate values as // it means the user meant for the statement to be prepared and we shouldn't add our own markers. boolean hasBindMarkers; private boolean forceNoValues; BuiltStatement( String keyspace, List partitionKey, List routingKeyValues) { this.partitionKey = partitionKey; this.routingKeyValues = routingKeyValues; this.keyspace = keyspace; } /** * @deprecated preserved for backward compatibility, use {@link Metadata#quoteIfNecessary(String)} * instead. */ @Deprecated protected static String escapeId(String ident) { return Metadata.quoteIfNecessary(ident); } @Override public String getQueryString(CodecRegistry codecRegistry) { maybeRebuildCache(codecRegistry); return cache; } /** * Returns the {@code i}th value as the Java type matching its CQL type. * * @param i the index to retrieve. * @param codecRegistry the codec registry that will be used if the statement must be rebuilt in * order to determine if it has values, and Java objects must be inlined in the process (see * {@link BuiltStatement} for more explanations on why this is so). * @return the value of the {@code i}th value of this statement. * @throws IllegalStateException if this statement does not have values. * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. * @see #getObject(int) */ public Object getObject(int i, CodecRegistry codecRegistry) { maybeRebuildCache(codecRegistry); if (values == null || values.isEmpty()) throw new IllegalStateException("This statement does not have values"); if (i < 0 || i >= values.size()) throw new ArrayIndexOutOfBoundsException(i); return values.get(i); } /** * Returns the {@code i}th value as the Java type matching its CQL type. * *

This method calls {@link #getObject(int, CodecRegistry)} with {@link * CodecRegistry#DEFAULT_INSTANCE}. It's safe to use if you don't use any custom codecs, or if * your custom codecs are in the default registry; otherwise, use the other method and provide the * registry that contains your codecs. * * @param i the index to retrieve. * @return the value of the {@code i}th value of this statement. * @throws IllegalStateException if this statement does not have values. * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. */ public Object getObject(int i) { return getObject(i, CodecRegistry.DEFAULT_INSTANCE); } private void maybeRebuildCache(CodecRegistry codecRegistry) { if (!dirty && cache != null) return; StringBuilder sb; values = null; if (hasBindMarkers || forceNoValues) { sb = buildQueryString(null, codecRegistry); } else { values = new ArrayList(); sb = buildQueryString(values, codecRegistry); if (values.size() > 65535) throw new IllegalArgumentException( "Too many values for built statement, the maximum allowed is 65535"); if (values.isEmpty()) values = null; } maybeAddSemicolon(sb); cache = sb.toString(); dirty = false; } static StringBuilder maybeAddSemicolon(StringBuilder sb) { // Use the same test that String#trim() uses to determine // if a character is a whitespace character. int l = sb.length(); while (l > 0 && sb.charAt(l - 1) <= ' ') l -= 1; if (l != sb.length()) sb.setLength(l); if (l == 0 || sb.charAt(l - 1) != ';') sb.append(';'); return sb; } abstract StringBuilder buildQueryString(List variables, CodecRegistry codecRegistry); boolean isCounterOp() { return isCounterOp == null ? false : isCounterOp; } void setCounterOp(boolean isCounterOp) { this.isCounterOp = isCounterOp; } boolean hasNonIdempotentOps() { return hasNonIdempotentOps; } void setNonIdempotentOps() { hasNonIdempotentOps = true; } void setDirty() { dirty = true; } void checkForBindMarkers(Object value) { dirty = true; if (Utils.containsBindMarker(value)) hasBindMarkers = true; } void checkForBindMarkers(Utils.Appendeable value) { dirty = true; if (value != null && value.containsBindMarker()) hasBindMarkers = true; } // TODO: Correctly document the InvalidTypeException void maybeAddRoutingKey(String name, Object value) { if (routingKeyValues == null || name == null || value == null || Utils.containsSpecialValue(value)) return; for (int i = 0; i < partitionKey.size(); i++) { if (Utils.handleId(name).equals(partitionKey.get(i).getName())) { routingKeyValues.set(i, value); return; } } } @Override public ByteBuffer getRoutingKey(ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { if (routingKeyValues == null) return null; ByteBuffer[] routingKeyParts = new ByteBuffer[partitionKey.size()]; for (int i = 0; i < partitionKey.size(); i++) { Object value = routingKeyValues.get(i); if (value == null) return null; TypeCodec codec = codecRegistry.codecFor(partitionKey.get(i).getType(), value); routingKeyParts[i] = codec.serialize(value, protocolVersion); } return routingKeyParts.length == 1 ? routingKeyParts[0] : Utils.compose(routingKeyParts); } @Override public String getKeyspace() { return keyspace; } @Override public ByteBuffer[] getValues(ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { maybeRebuildCache(codecRegistry); return values == null ? null : Utils.convert(values.toArray(), protocolVersion, codecRegistry); } @Override public boolean hasValues(CodecRegistry codecRegistry) { maybeRebuildCache(codecRegistry); return values != null; } @Override public Map getNamedValues( ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { // Built statements never return named values return null; } @Override public boolean usesNamedValues() { return false; } @Override public Boolean isIdempotent() { // If a value was forced with setIdempotent, it takes priority if (idempotent != null) return idempotent; // Otherwise return the computed value return !hasNonIdempotentOps(); } @Override public String toString() { try { if (forceNoValues) return getQueryString(); // 1) try first with all values inlined (will not work if some values require custom codecs, // or if the required codecs are registered in a different CodecRegistry instance than the // default one) return maybeAddSemicolon(buildQueryString(null, CodecRegistry.DEFAULT_INSTANCE)).toString(); } catch (RuntimeException e1) { // 2) try next with bind markers for all values to avoid usage of custom codecs try { return maybeAddSemicolon( buildQueryString(new ArrayList(), CodecRegistry.DEFAULT_INSTANCE)) .toString(); } catch (RuntimeException e2) { // Ugly but we have absolutely no context to get the registry from return String.format( "built query (could not generate with default codec registry: %s)", e2.getMessage()); } } } /** * Allows to force this builder to not generate values (through its {@code getValues()} method). * *

By default (and unless the protocol version 1 is in use, see below) and for performance * reasons, the query builder will not serialize all values provided to strings. This means that * {@link #getQueryString(CodecRegistry)} may return a query string with bind markers (where and * when is at the discretion of the builder) and {@link #getValues} will return the binary values * for those markers. This method allows to force the builder to not generate binary values but * rather to inline them all in the query string. In practice, this means that if you call {@code * setForceNoValues(true)}, you are guaranteed that {@code getValues()} will return {@code null} * and that the string returned by {@code getQueryString()} will contain no other bind markers * than the ones specified by the user. * *

If the native protocol version 1 is in use, the driver will default to not generating values * since those are not supported by that version of the protocol. In practice, the driver will * automatically call this method with {@code true} as argument prior to execution. Hence, calling * this method when the protocol version 1 is in use is basically a no-op. * *

Note that this method is mainly useful for debugging purpose. In general, the default * behavior should be the correct and most efficient one. * * @param forceNoValues whether or not this builder may generate values. * @return this statement. */ public RegularStatement setForceNoValues(boolean forceNoValues) { this.forceNoValues = forceNoValues; this.dirty = true; return this; } /** An utility class to create a BuiltStatement that encapsulate another one. */ abstract static class ForwardingStatement extends BuiltStatement { T statement; ForwardingStatement(T statement) { super(null, null, null); this.statement = statement; } @Override public String getQueryString(CodecRegistry codecRegistry) { return statement.getQueryString(codecRegistry); } @Override StringBuilder buildQueryString(List values, CodecRegistry codecRegistry) { return statement.buildQueryString(values, codecRegistry); } @Override public ByteBuffer getRoutingKey(ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { return statement.getRoutingKey(protocolVersion, codecRegistry); } @Override public String getKeyspace() { return statement.getKeyspace(); } @Override boolean isCounterOp() { return statement.isCounterOp(); } @Override boolean hasNonIdempotentOps() { return statement.hasNonIdempotentOps(); } @Override public RegularStatement setForceNoValues(boolean forceNoValues) { statement.setForceNoValues(forceNoValues); return this; } @Override public Statement setConsistencyLevel(ConsistencyLevel consistency) { statement.setConsistencyLevel(consistency); return this; } @Override public ConsistencyLevel getConsistencyLevel() { return statement.getConsistencyLevel(); } @Override public Statement enableTracing() { statement.enableTracing(); return this; } @Override public Statement disableTracing() { statement.disableTracing(); return this; } @Override public boolean isTracing() { return statement.isTracing(); } @Override public Statement setRetryPolicy(RetryPolicy policy) { statement.setRetryPolicy(policy); return this; } @Override public RetryPolicy getRetryPolicy() { return statement.getRetryPolicy(); } @Override public ByteBuffer[] getValues(ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { return statement.getValues(protocolVersion, codecRegistry); } @Override public boolean hasValues() { return statement.hasValues(); } @Override void checkForBindMarkers(Object value) { statement.checkForBindMarkers(value); } @Override void checkForBindMarkers(Utils.Appendeable value) { statement.checkForBindMarkers(value); } @Override public String toString() { return statement.toString(); } } }