/* * Copyright (C) 2012-2015 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; import com.datastax.driver.core.utils.Bytes; import com.datastax.driver.core.utils.MoreObjects; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.nio.ByteBuffer; import java.util.Collections; import java.util.List; /** * Describes a CQL aggregate function (created with {@code CREATE AGGREGATE...}). */ public class AggregateMetadata { private static final Logger LOGGER = LoggerFactory.getLogger(AggregateMetadata.class); private final KeyspaceMetadata keyspace; private final String simpleName; private final List<DataType> argumentTypes; private final String finalFuncSimpleName; private final String finalFuncFullName; private final Object initCond; private final DataType returnType; private final String stateFuncSimpleName; private final String stateFuncFullName; private final DataType stateType; private final TypeCodec<Object> stateTypeCodec; private AggregateMetadata(KeyspaceMetadata keyspace, String simpleName, List<DataType> argumentTypes, String finalFuncSimpleName, String finalFuncFullName, Object initCond, DataType returnType, String stateFuncSimpleName, String stateFuncFullName, DataType stateType, TypeCodec<Object> stateTypeCodec) { this.keyspace = keyspace; this.simpleName = simpleName; this.argumentTypes = argumentTypes; this.finalFuncSimpleName = finalFuncSimpleName; this.finalFuncFullName = finalFuncFullName; this.initCond = initCond; this.returnType = returnType; this.stateFuncSimpleName = stateFuncSimpleName; this.stateFuncFullName = stateFuncFullName; this.stateType = stateType; this.stateTypeCodec = stateTypeCodec; } // Cassandra < 3.0: // CREATE TABLE system.schema_aggregates ( // keyspace_name text, // aggregate_name text, // signature frozen<list<text>>, // argument_types list<text>, // final_func text, // initcond blob, // return_type text, // state_func text, // state_type text, // PRIMARY KEY (keyspace_name, aggregate_name, signature) // ) WITH CLUSTERING ORDER BY (aggregate_name ASC, signature ASC) // // Cassandra >= 3.0: // CREATE TABLE system.schema_aggregates ( // keyspace_name text, // aggregate_name text, // argument_types frozen<list<text>>, // final_func text, // initcond text, // return_type text, // state_func text, // state_type text, // PRIMARY KEY (keyspace_name, aggregate_name, argument_types) // ) WITH CLUSTERING ORDER BY (aggregate_name ASC, argument_types ASC) static AggregateMetadata build(KeyspaceMetadata ksm, Row row, VersionNumber version, Cluster cluster) { CodecRegistry codecRegistry = cluster.getConfiguration().getCodecRegistry(); ProtocolVersion protocolVersion = cluster.getConfiguration().getProtocolOptions().getProtocolVersion(); String simpleName = row.getString("aggregate_name"); List<DataType> argumentTypes = parseTypes(ksm, row.getList("argument_types", String.class), version, cluster); String finalFuncSimpleName = row.getString("final_func"); DataType returnType; if (version.getMajor() >= 3) { returnType = DataTypeCqlNameParser.parse(row.getString("return_type"), cluster, ksm.getName(), ksm.userTypes, null, false, false); } else { returnType = DataTypeClassNameParser.parseOne(row.getString("return_type"), protocolVersion, codecRegistry); } String stateFuncSimpleName = row.getString("state_func"); String stateTypeName = row.getString("state_type"); DataType stateType; Object initCond; if (version.getMajor() >= 3) { stateType = DataTypeCqlNameParser.parse(stateTypeName, cluster, ksm.getName(), ksm.userTypes, null, false, false); String rawInitCond = row.getString("initcond"); if (rawInitCond == null) { initCond = null; } else { try { initCond = codecRegistry.codecFor(stateType).parse(rawInitCond); } catch (RuntimeException e) { LOGGER.warn("Failed to parse INITCOND literal: {}; getInitCond() will return the text literal instead.", rawInitCond); initCond = rawInitCond; } } } else { stateType = DataTypeClassNameParser.parseOne(stateTypeName, protocolVersion, codecRegistry); ByteBuffer rawInitCond = row.getBytes("initcond"); if (rawInitCond == null) { initCond = null; } else { try { initCond = codecRegistry.codecFor(stateType).deserialize(rawInitCond, protocolVersion); } catch (RuntimeException e) { LOGGER.warn("Failed to deserialize INITCOND value: {}; getInitCond() will return the raw bytes instead.", Bytes.toHexString(rawInitCond)); initCond = rawInitCond; } } } String finalFuncFullName = finalFuncSimpleName == null ? null : Metadata.fullFunctionName(finalFuncSimpleName, Collections.singletonList(stateType)); String stateFuncFullName = makeStateFuncFullName(stateFuncSimpleName, stateType, argumentTypes); return new AggregateMetadata(ksm, simpleName, argumentTypes, finalFuncSimpleName, finalFuncFullName, initCond, returnType, stateFuncSimpleName, stateFuncFullName, stateType, codecRegistry.codecFor(stateType)); } private static String makeStateFuncFullName(String stateFuncSimpleName, DataType stateType, List<DataType> argumentTypes) { List<DataType> args = Lists.newArrayList(stateType); args.addAll(argumentTypes); return Metadata.fullFunctionName(stateFuncSimpleName, args); } private static List<DataType> parseTypes(KeyspaceMetadata ksm, List<String> types, VersionNumber version, Cluster cluster) { if (types.isEmpty()) return Collections.emptyList(); CodecRegistry codecRegistry = cluster.getConfiguration().getCodecRegistry(); ProtocolVersion protocolVersion = cluster.getConfiguration().getProtocolOptions().getProtocolVersion(); ImmutableList.Builder<DataType> builder = ImmutableList.builder(); for (String name : types) { DataType type; if (version.getMajor() >= 3) { type = DataTypeCqlNameParser.parse(name, cluster, ksm.getName(), ksm.userTypes, null, false, false); } else { type = DataTypeClassNameParser.parseOne(name, protocolVersion, codecRegistry); } builder.add(type); } return builder.build(); } /** * Returns a CQL query representing this function in human readable form. * <p/> * This method is equivalent to {@link #asCQLQuery} but the output is formatted. * * @return the CQL query representing this function. */ public String exportAsString() { return asCQLQuery(true); } /** * Returns a CQL query representing this function. * <p/> * This method returns a single 'CREATE FUNCTION' query corresponding to * this function definition. * * @return the 'CREATE FUNCTION' query corresponding to this function. */ public String asCQLQuery() { return asCQLQuery(false); } @Override public String toString() { return asCQLQuery(false); } private String asCQLQuery(boolean formatted) { StringBuilder sb = new StringBuilder("CREATE AGGREGATE ") .append(Metadata.quoteIfNecessary(keyspace.getName())) .append('.'); appendSignature(sb); TableMetadata.spaceOrNewLine(sb, formatted) .append("SFUNC ") .append(Metadata.quoteIfNecessary(stateFuncSimpleName)) .append(" STYPE ") .append(stateType.asFunctionParameterString()); if (finalFuncSimpleName != null) TableMetadata.spaceOrNewLine(sb, formatted) .append("FINALFUNC ") .append(Metadata.quoteIfNecessary(finalFuncSimpleName)); if (initCond != null) TableMetadata.spaceOrNewLine(sb, formatted) .append("INITCOND ") .append(formatInitCond()); sb.append(';'); return sb.toString(); } private String formatInitCond() { if (stateTypeCodec.accepts(initCond)) { try { return stateTypeCodec.format(initCond); } catch (RuntimeException e) { LOGGER.info("Failed to format INITCOND literal: {}", initCond); } } return initCond.toString(); } private void appendSignature(StringBuilder sb) { sb .append(Metadata.quoteIfNecessary(simpleName)) .append('('); boolean first = true; for (DataType type : argumentTypes) { if (first) first = false; else sb.append(','); sb.append(type.asFunctionParameterString()); } sb.append(')'); } /** * Returns the keyspace this aggregate belongs to. * * @return the keyspace metadata of the keyspace this aggregate belongs to. */ public KeyspaceMetadata getKeyspace() { return keyspace; } /** * Returns the CQL signature of this aggregate. * <p/> * This is the name of the aggregate, followed by the names of the argument types between parentheses, * like it was specified in the {@code CREATE AGGREGATE...} statement, for example {@code sum(int)}. * <p/> * Note that the returned signature is not qualified with the keyspace name. * * @return the signature of this aggregate. */ public String getSignature() { StringBuilder sb = new StringBuilder(); appendSignature(sb); return sb.toString(); } /** * Returns the simple name of this aggregate. * <p/> * This is the name of the aggregate, without arguments. Note that aggregates can be overloaded with * different argument lists, therefore the simple name may not be unique. For example, * {@code sum(int)} and {@code sum(int,int)} both have the simple name {@code sum}. * * @return the simple name of this aggregate. * @see #getSignature() */ public String getSimpleName() { return simpleName; } /** * Returns the types of this aggregate's arguments. * * @return the types. */ public List<DataType> getArgumentTypes() { return argumentTypes; } /** * Returns the final function of this aggregate. * <p/> * This is the function specified with {@code FINALFUNC} in the {@code CREATE AGGREGATE...} * statement. It transforms the final value after the aggregation is complete. * * @return the metadata of the final function, or {@code null} if there is none. */ public FunctionMetadata getFinalFunc() { return (finalFuncFullName == null) ? null : keyspace.functions.get(finalFuncFullName); } /** * Returns the initial state value of this aggregate. * <p/> * This is the value specified with {@code INITCOND} in the {@code CREATE AGGREGATE...} * statement. It's passed to the initial invocation of the state function (if that function * does not accept null arguments). * <p/> * The actual type of the returned object depends on the aggregate's * {@link #getStateType() state type} and on the {@link TypeCodec codec} used * to {@link TypeCodec#parse(String) parse} the {@code INITCOND} literal. * <p/> * If, for some reason, the {@code INITCOND} literal cannot be parsed, * a warning will be logged and the returned object will be the original * {@code INITCOND} literal in its textual, non-parsed form. * * @return the initial state, or {@code null} if there is none. */ public Object getInitCond() { return initCond; } /** * Returns the return type of this aggregate. * <p/> * This is the final type of the value computed by this aggregate; in other words, the return * type of the final function if it is defined, or the state type otherwise. * * @return the return type. */ public DataType getReturnType() { return returnType; } /** * Returns the state function of this aggregate. * <p/> * This is the function specified with {@code SFUNC} in the {@code CREATE AGGREGATE...} * statement. It aggregates the current state with each row to produce a new state. * * @return the metadata of the state function. */ public FunctionMetadata getStateFunc() { return keyspace.functions.get(stateFuncFullName); } /** * Returns the state type of this aggregate. * <p/> * This is the type specified with {@code STYPE} in the {@code CREATE AGGREGATE...} * statement. It defines the type of the value that is accumulated as the aggregate * iterates through the rows. * * @return the state type. */ public DataType getStateType() { return stateType; } @Override public boolean equals(Object other) { if (other == this) return true; if (other instanceof AggregateMetadata) { AggregateMetadata that = (AggregateMetadata) other; return this.keyspace.getName().equals(that.keyspace.getName()) && this.argumentTypes.equals(that.argumentTypes) && MoreObjects.equal(this.finalFuncFullName, that.finalFuncFullName) && // Note: this might be a problem if a custom codec has been registered for the initCond's type, with a target Java type that // does not properly implement equals. We don't have any control over this, at worst this would lead to spurious change // notifications. MoreObjects.equal(this.initCond, that.initCond) && this.returnType.equals(that.returnType) && this.stateFuncFullName.equals(that.stateFuncFullName) && this.stateType.equals(that.stateType); } return false; } @Override public int hashCode() { return MoreObjects.hashCode(this.keyspace.getName(), this.argumentTypes, this.finalFuncFullName, this.initCond, this.returnType, this.stateFuncFullName, this.stateType); } }