blob: 5d83a2beaf16e07f25389edb04efffb43f5c18a3 [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.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import com.google.common.annotations.VisibleForTesting;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.db.marshal.UserType;
import org.apache.cassandra.schema.*;
/**
* Helper methods to represent TableMetadata and related objects in CQL format
*/
public class SchemaCQLHelper
{
private static final Pattern EMPTY_TYPE_REGEX = Pattern.compile("empty", Pattern.LITERAL);
private static final String EMPTY_TYPE_QUOTED = Matcher.quoteReplacement("'org.apache.cassandra.db.marshal.EmptyType'");
/**
* Generates the DDL statement for a {@code schema.cql} snapshot file.
*/
public static Stream<String> reCreateStatementsForSchemaCql(TableMetadata metadata, Types types)
{
// Types come first, as table can't be created without them
Stream<String> udts = SchemaCQLHelper.getUserTypesAsCQL(metadata, types, true);
return Stream.concat(udts,
reCreateStatements(metadata,
true,
true,
true,
true));
}
public static Stream<String> reCreateStatements(TableMetadata metadata,
boolean includeDroppedColumns,
boolean internals,
boolean ifNotExists,
boolean includeIndexes)
{
// Record re-create schema statements
Stream<String> r = Stream.of(metadata)
.map((tm) -> SchemaCQLHelper.getTableMetadataAsCQL(tm,
includeDroppedColumns,
internals,
ifNotExists));
if (includeIndexes)
{
// Indexes applied as last, since otherwise they may interfere with column drops / re-additions
r = Stream.concat(r, SchemaCQLHelper.getIndexesAsCQL(metadata, ifNotExists));
}
return r;
}
/**
* Build a CQL String representation of Column Family Metadata.
*
* *Note*: this is _only_ visible for testing; you generally shouldn't re-create a single table in isolation as
* that will not contain everything needed for user types.
*/
@VisibleForTesting
public static String getTableMetadataAsCQL(TableMetadata metadata,
boolean includeDroppedColumns,
boolean internals,
boolean ifNotExists)
{
if (metadata.isView())
{
KeyspaceMetadata keyspaceMetadata = Schema.instance.getKeyspaceMetadata(metadata.keyspace);
ViewMetadata viewMetadata = keyspaceMetadata.views.get(metadata.name).orElse(null);
assert viewMetadata != null;
return viewMetadata.toCqlString(internals, ifNotExists);
}
return metadata.toCqlString(includeDroppedColumns, internals, ifNotExists);
}
/**
* Build a CQL String representation of User Types used in the given table.
*
* Type order is ensured as types are built incrementally: from the innermost (most nested)
* to the outermost.
*
* @param metadata the table for which to extract the user types CQL statements.
* @param types the user types defined in the keyspace of the dumped table (which will thus contain any user type
* used by {@code metadata}).
* @param ifNotExists set to true if IF NOT EXISTS should be appended after CREATE TYPE string.
* @return a list of {@code CREATE TYPE} statements corresponding to all the types used in {@code metadata}.
*/
@VisibleForTesting
public static Stream<String> getUserTypesAsCQL(TableMetadata metadata, Types types, boolean ifNotExists)
{
/*
* Implementation note: at first approximation, it may seem like we don't need the Types argument and instead
* directly extract the user types from the provided TableMetadata. Indeed, full user types definitions are
* contained in UserType instances.
*
* However, the UserType instance found within the TableMetadata may have been frozen in such a way that makes
* it challenging.
*
* Consider the user has created:
* CREATE TYPE inner (a set<int>);
* CREATE TYPE outer (b inner);
* CREATE TABLE t (k int PRIMARY KEY, c1 frozen<outer>, c2 set<frozen<inner>>)
* The corresponding TableMetadata would have, as types (where 'mc=true' means that the type has his isMultiCell
* set to true):
* c1: UserType(mc=false, "outer", b->UserType(mc=false, "inner", a->SetType(mc=fase, Int32Type)))
* c2: SetType(mc=true, UserType(mc=false, "inner", a->SetType(mc=fase, Int32Type)))
* From which, it's impossible to decide if we should dump the types above, or instead:
* CREATE TYPE inner (a frozen<set<int>>);
* CREATE TYPE outer (b frozen<inner>);
* or anything in-between.
*
* And while, as of the current limitation around multi-cell types (that are only support non-frozen at
* top-level), any of the generated definition would kind of "work", 1) this could confuse users and 2) this
* would break if we do lift the limitation, which wouldn't be future proof.
*/
return metadata.getReferencedUserTypes()
.stream()
.map(name -> getType(metadata, types, name).toCqlString(false, ifNotExists));
}
/**
* Build a CQL String representation of Indexes on columns in the given Column Family
*
* @param metadata the table for which to extract the index CQL statements.
* @param ifNotExists set to true if IF NOT EXISTS should be appended after CREATE INDEX string.
* @return a list of {@code CREATE INDEX} statements corresponding to table {@code metadata}.
*/
@VisibleForTesting
public static Stream<String> getIndexesAsCQL(TableMetadata metadata, boolean ifNotExists)
{
return metadata.indexes
.stream()
.map(indexMetadata -> indexMetadata.toCqlString(metadata, ifNotExists));
}
private static UserType getType(TableMetadata metadata, Types types, ByteBuffer name)
{
return types.get(name)
.orElseThrow(() -> new IllegalStateException(String.format("user type %s is part of table %s definition but its definition was missing",
UTF8Type.instance.getString(name),
metadata)));
}
/**
* Converts the type to a CQL type. This method special cases empty and UDTs so the string can be used in a create
* statement.
*
* Special cases
* <ul>
* <li>empty - replaces with 'org.apache.cassandra.db.marshal.EmptyType'. empty is the tostring of the type in
* CQL but not allowed to create as empty, but fully qualified name is allowed</li>
* <li>UserType - replaces with TupleType</li>
* </ul>
*/
public static String toCqlType(AbstractType<?> type)
{
return EMPTY_TYPE_REGEX.matcher(type.expandUserTypes().asCQL3Type().toString()).replaceAll(EMPTY_TYPE_QUOTED);
}
}