/*
 * 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
/** * 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
/** * 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.
/** * 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.
/** * 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
/** * 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
/** * 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; } }