blob: 54c811788e644f68f0ec706644c08ad2611a4b25 [file] [log] [blame]
/*
* 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.db;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.concurrent.atomic.*;
import java.util.function.*;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Iterables;
import org.apache.cassandra.config.*;
import org.apache.cassandra.cql3.*;
import org.apache.cassandra.cql3.statements.*;
import org.apache.cassandra.db.marshal.*;
import org.apache.cassandra.schema.*;
import org.apache.cassandra.utils.*;
/**
* Helper methods to represent CFMetadata and related objects in CQL format
*/
public class ColumnFamilyStoreCQLHelper
{
public static List<String> dumpReCreateStatements(CFMetaData metadata)
{
List<String> l = new ArrayList<>();
// Types come first, as table can't be created without them
l.addAll(ColumnFamilyStoreCQLHelper.getUserTypesAsCQL(metadata));
// Record re-create schema statements
l.add(ColumnFamilyStoreCQLHelper.getCFMetadataAsCQL(metadata, true));
// Dropped columns (and re-additions)
l.addAll(ColumnFamilyStoreCQLHelper.getDroppedColumnsAsCQL(metadata));
// Indexes applied as last, since otherwise they may interfere with column drops / re-additions
l.addAll(ColumnFamilyStoreCQLHelper.getIndexesAsCQL(metadata));
return l;
}
private static List<ColumnDefinition> getClusteringColumns(CFMetaData metadata)
{
List<ColumnDefinition> cds = new ArrayList<>(metadata.clusteringColumns().size());
if (!metadata.isStaticCompactTable())
for (ColumnDefinition cd : metadata.clusteringColumns())
cds.add(cd);
return cds;
}
private static List<ColumnDefinition> getPartitionColumns(CFMetaData metadata)
{
List<ColumnDefinition> cds = new ArrayList<>(metadata.partitionColumns().size());
for (ColumnDefinition cd : metadata.partitionColumns().statics)
cds.add(cd);
if (metadata.isDense())
{
// remove an empty type
for (ColumnDefinition cd : metadata.partitionColumns().withoutStatics())
if (!cd.type.equals(EmptyType.instance))
cds.add(cd);
}
// "regular" columns are not exposed for static compact tables
else if (!metadata.isStaticCompactTable())
{
for (ColumnDefinition cd : metadata.partitionColumns().withoutStatics())
cds.add(cd);
}
return cds;
}
/**
* Build a CQL String representation of Column Family Metadata
*/
@VisibleForTesting
public static String getCFMetadataAsCQL(CFMetaData metadata, boolean includeDroppedColumns)
{
StringBuilder sb = new StringBuilder();
if (!isCqlCompatible(metadata))
{
sb.append(String.format("/*\nWarning: Table %s.%s omitted because it has constructs not compatible with CQL (was created via legacy API).\n",
metadata.ksName,
metadata.cfName));
sb.append("\nApproximate structure, for reference:");
sb.append("\n(this should not be used to reproduce this schema)\n\n");
}
sb.append("CREATE TABLE IF NOT EXISTS ");
sb.append(quoteIdentifier(metadata.ksName)).append('.').append(quoteIdentifier(metadata.cfName)).append(" (");
List<ColumnDefinition> partitionKeyColumns = metadata.partitionKeyColumns();
List<ColumnDefinition> clusteringColumns = getClusteringColumns(metadata);
List<ColumnDefinition> partitionColumns = getPartitionColumns(metadata);
Consumer<StringBuilder> cdCommaAppender = commaAppender("\n\t");
sb.append("\n\t");
for (ColumnDefinition cfd: partitionKeyColumns)
{
cdCommaAppender.accept(sb);
sb.append(toCQL(cfd));
if (partitionKeyColumns.size() == 1 && clusteringColumns.size() == 0)
sb.append(" PRIMARY KEY");
}
for (ColumnDefinition cfd: clusteringColumns)
{
cdCommaAppender.accept(sb);
sb.append(toCQL(cfd));
}
for (ColumnDefinition cfd: partitionColumns)
{
cdCommaAppender.accept(sb);
sb.append(toCQL(cfd, metadata.isStaticCompactTable()));
}
if (includeDroppedColumns)
{
for (Map.Entry<ByteBuffer, CFMetaData.DroppedColumn> entry: metadata.getDroppedColumns().entrySet())
{
if (metadata.getColumnDefinition(entry.getKey()) != null)
continue;
CFMetaData.DroppedColumn droppedColumn = entry.getValue();
cdCommaAppender.accept(sb);
sb.append(quoteIdentifier(droppedColumn.name));
sb.append(' ');
sb.append(droppedColumn.type.asCQL3Type().toString());
}
}
if (clusteringColumns.size() > 0 || partitionKeyColumns.size() > 1)
{
sb.append(",\n\tPRIMARY KEY (");
if (partitionKeyColumns.size() > 1)
{
sb.append("(");
Consumer<StringBuilder> pkCommaAppender = commaAppender(" ");
for (ColumnDefinition cfd : partitionKeyColumns)
{
pkCommaAppender.accept(sb);
sb.append(quoteIdentifier(cfd.name.toString()));
}
sb.append(")");
}
else
{
sb.append(quoteIdentifier(partitionKeyColumns.get(0).name.toString()));
}
for (ColumnDefinition cfd : metadata.clusteringColumns())
sb.append(", ").append(quoteIdentifier(cfd.name.toString()));
sb.append(')');
}
sb.append(")\n\t");
sb.append("WITH ");
sb.append("ID = ").append(metadata.cfId).append("\n\tAND ");
if (metadata.isCompactTable())
sb.append("COMPACT STORAGE\n\tAND ");
if (clusteringColumns.size() > 0)
{
sb.append("CLUSTERING ORDER BY (");
Consumer<StringBuilder> cOrderCommaAppender = commaAppender(" ");
for (ColumnDefinition cd : clusteringColumns)
{
cOrderCommaAppender.accept(sb);
sb.append(quoteIdentifier(cd.name.toString())).append(' ').append(cd.clusteringOrder().toString());
}
sb.append(")\n\tAND ");
}
sb.append(toCQL(metadata.params));
sb.append(";");
if (!isCqlCompatible(metadata))
{
sb.append("\n*/");
}
return sb.toString();
}
/**
* Build a CQL String representation of User Types used in the given Column Family.
*
* Type order is ensured as types are built incrementally: from the innermost (most nested)
* to the outermost.
*/
@VisibleForTesting
public static List<String> getUserTypesAsCQL(CFMetaData metadata)
{
List<AbstractType> types = new ArrayList<>();
Set<AbstractType> typeSet = new HashSet<>();
for (ColumnDefinition cd: Iterables.concat(metadata.partitionKeyColumns(), metadata.clusteringColumns(), metadata.partitionColumns()))
{
AbstractType type = cd.type;
if (type.isUDT())
resolveUserType((UserType) type, typeSet, types);
}
List<String> typeStrings = new ArrayList<>();
for (AbstractType type: types)
typeStrings.add(toCQL((UserType) type));
return typeStrings;
}
/**
* Build a CQL String representation of Dropped Columns in the given Column Family.
*
* If the column was dropped once, but is now re-created `ADD` will be appended accordingly.
*/
@VisibleForTesting
public static List<String> getDroppedColumnsAsCQL(CFMetaData metadata)
{
List<String> droppedColumns = new ArrayList<>();
for (Map.Entry<ByteBuffer, CFMetaData.DroppedColumn> entry: metadata.getDroppedColumns().entrySet())
{
CFMetaData.DroppedColumn column = entry.getValue();
droppedColumns.add(toCQLDrop(metadata.ksName, metadata.cfName, column));
if (metadata.getColumnDefinition(entry.getKey()) != null)
droppedColumns.add(toCQLAdd(metadata.ksName, metadata.cfName, metadata.getColumnDefinition(entry.getKey())));
}
return droppedColumns;
}
/**
* Build a CQL String representation of Indexes on columns in the given Column Family
*/
@VisibleForTesting
public static List<String> getIndexesAsCQL(CFMetaData metadata)
{
List<String> indexes = new ArrayList<>();
for (IndexMetadata indexMetadata: metadata.getIndexes())
indexes.add(toCQL(metadata.ksName, metadata.cfName, indexMetadata));
return indexes;
}
private static String toCQL(String keyspace, String cf, IndexMetadata indexMetadata)
{
if (indexMetadata.isCustom())
{
Map<String, String> options = new HashMap<>();
indexMetadata.options.forEach((k, v) -> {
if (!k.equals(IndexTarget.TARGET_OPTION_NAME) && !k.equals(IndexTarget.CUSTOM_INDEX_OPTION_NAME))
options.put(k, v);
});
return String.format("CREATE CUSTOM INDEX %s ON %s.%s (%s) USING '%s'%s;",
quoteIdentifier(indexMetadata.name),
quoteIdentifier(keyspace),
quoteIdentifier(cf),
indexMetadata.options.get(IndexTarget.TARGET_OPTION_NAME),
indexMetadata.options.get(IndexTarget.CUSTOM_INDEX_OPTION_NAME),
options.isEmpty() ? "" : " WITH OPTIONS " + toCQL(options));
}
else
{
return String.format("CREATE INDEX %s ON %s.%s (%s);",
quoteIdentifier(indexMetadata.name),
quoteIdentifier(keyspace),
quoteIdentifier(cf),
indexMetadata.options.get(IndexTarget.TARGET_OPTION_NAME));
}
}
private static String toCQL(UserType userType)
{
StringBuilder sb = new StringBuilder();
sb.append(String.format("CREATE TYPE %s.%s(",
quoteIdentifier(userType.keyspace),
quoteIdentifier(userType.getNameAsString())));
Consumer<StringBuilder> commaAppender = commaAppender(" ");
for (int i = 0; i < userType.size(); i++)
{
commaAppender.accept(sb);
sb.append(String.format("%s %s",
userType.fieldNameAsString(i),
userType.fieldType(i).asCQL3Type()));
}
sb.append(");");
return sb.toString();
}
private static String toCQL(TableParams tableParams)
{
StringBuilder builder = new StringBuilder();
builder.append("bloom_filter_fp_chance = ").append(tableParams.bloomFilterFpChance);
builder.append("\n\tAND dclocal_read_repair_chance = ").append(tableParams.dcLocalReadRepairChance);
builder.append("\n\tAND crc_check_chance = ").append(tableParams.crcCheckChance);
builder.append("\n\tAND default_time_to_live = ").append(tableParams.defaultTimeToLive);
builder.append("\n\tAND gc_grace_seconds = ").append(tableParams.gcGraceSeconds);
builder.append("\n\tAND min_index_interval = ").append(tableParams.minIndexInterval);
builder.append("\n\tAND max_index_interval = ").append(tableParams.maxIndexInterval);
builder.append("\n\tAND memtable_flush_period_in_ms = ").append(tableParams.memtableFlushPeriodInMs);
builder.append("\n\tAND read_repair_chance = ").append(tableParams.readRepairChance);
builder.append("\n\tAND speculative_retry = '").append(tableParams.speculativeRetry).append("'");
builder.append("\n\tAND comment = ").append(singleQuote(tableParams.comment));
builder.append("\n\tAND caching = ").append(toCQL(tableParams.caching.asMap()));
builder.append("\n\tAND compaction = ").append(toCQL(tableParams.compaction.asMap()));
builder.append("\n\tAND compression = ").append(toCQL(tableParams.compression.asMap()));
builder.append("\n\tAND cdc = ").append(tableParams.cdc);
builder.append("\n\tAND extensions = { ");
for (Map.Entry<String, ByteBuffer> entry : tableParams.extensions.entrySet())
{
builder.append(singleQuote(entry.getKey()));
builder.append(": ");
builder.append("0x").append(ByteBufferUtil.bytesToHex(entry.getValue()));
}
builder.append(" }");
return builder.toString();
}
private static String toCQL(Map<?, ?> map)
{
StringBuilder builder = new StringBuilder("{ ");
boolean isFirst = true;
for (Map.Entry entry: map.entrySet())
{
if (isFirst)
isFirst = false;
else
builder.append(", ");
builder.append(singleQuote(entry.getKey().toString()));
builder.append(": ");
builder.append(singleQuote(entry.getValue().toString()));
}
builder.append(" }");
return builder.toString();
}
private static String toCQL(ColumnDefinition cd)
{
return toCQL(cd, false);
}
private static String toCQL(ColumnDefinition cd, boolean isStaticCompactTable)
{
return String.format("%s %s%s",
quoteIdentifier(cd.name.toString()),
cd.type.asCQL3Type().toString(),
cd.isStatic() && !isStaticCompactTable ? " static" : "");
}
private static String toCQLAdd(String keyspace, String cf, ColumnDefinition cd)
{
return String.format("ALTER TABLE %s.%s ADD %s %s%s;",
quoteIdentifier(keyspace),
quoteIdentifier(cf),
quoteIdentifier(cd.name.toString()),
cd.type.asCQL3Type().toString(),
cd.isStatic() ? " static" : "");
}
private static String toCQLDrop(String keyspace, String cf, CFMetaData.DroppedColumn droppedColumn)
{
return String.format("ALTER TABLE %s.%s DROP %s USING TIMESTAMP %s;",
quoteIdentifier(keyspace),
quoteIdentifier(cf),
quoteIdentifier(droppedColumn.name),
droppedColumn.droppedTime);
}
private static void resolveUserType(UserType type, Set<AbstractType> typeSet, List<AbstractType> types)
{
for (AbstractType subType: type.fieldTypes())
if (!typeSet.contains(subType) && subType.isUDT())
resolveUserType((UserType) subType, typeSet, types);
if (!typeSet.contains(type))
{
typeSet.add(type);
types.add(type);
}
}
private static String singleQuote(String s)
{
return String.format("'%s'", s.replaceAll("'", "''"));
}
private static Consumer<StringBuilder> commaAppender(String afterComma)
{
AtomicBoolean isFirst = new AtomicBoolean(true);
return new Consumer<StringBuilder>()
{
public void accept(StringBuilder stringBuilder)
{
if (!isFirst.getAndSet(false))
stringBuilder.append(',').append(afterComma);
}
};
}
private static String quoteIdentifier(String id)
{
return ColumnIdentifier.maybeQuote(id);
}
/**
* Whether or not the given metadata is compatible / representable with CQL Language
*/
public static boolean isCqlCompatible(CFMetaData metaData)
{
if (metaData.isSuper())
return false;
if (metaData.isCompactTable()
&& metaData.partitionColumns().withoutStatics().size() > 1
&& metaData.clusteringColumns().size() >= 1)
return false;
return true;
}
}