blob: f51ad444daddc36e4b1ba39b186d848eb54bacee [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.cql3.statements;
import java.util.*;
import com.google.common.base.Optional;
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.auth.Permission;
import org.apache.cassandra.config.CFMetaData;
import org.apache.cassandra.config.ColumnDefinition;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.config.Schema;
import org.apache.cassandra.cql3.CFName;
import org.apache.cassandra.cql3.ColumnIdentifier;
import org.apache.cassandra.cql3.IndexName;
import org.apache.cassandra.db.marshal.MapType;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.exceptions.RequestValidationException;
import org.apache.cassandra.exceptions.UnauthorizedException;
import org.apache.cassandra.index.sasi.SASIIndex;
import org.apache.cassandra.schema.IndexMetadata;
import org.apache.cassandra.schema.Indexes;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.service.ClientWarn;
import org.apache.cassandra.service.MigrationManager;
import org.apache.cassandra.service.QueryState;
import org.apache.cassandra.thrift.ThriftValidation;
import org.apache.cassandra.transport.Event;
import static org.apache.cassandra.cql3.statements.RequestValidations.checkFalse;
import static org.apache.cassandra.cql3.statements.RequestValidations.invalidRequest;
/** A <code>CREATE INDEX</code> statement parsed from a CQL query. */
public class CreateIndexStatement extends SchemaAlteringStatement
{
private static final Logger logger = LoggerFactory.getLogger(CreateIndexStatement.class);
private final String indexName;
private final List<IndexTarget.Raw> rawTargets;
private final IndexPropDefs properties;
private final boolean ifNotExists;
public CreateIndexStatement(CFName name,
IndexName indexName,
List<IndexTarget.Raw> targets,
IndexPropDefs properties,
boolean ifNotExists)
{
super(name);
this.indexName = indexName.getIdx();
this.rawTargets = targets;
this.properties = properties;
this.ifNotExists = ifNotExists;
}
public void checkAccess(ClientState state) throws UnauthorizedException, InvalidRequestException
{
state.hasColumnFamilyAccess(keyspace(), columnFamily(), Permission.ALTER);
}
public void validate(ClientState state) throws RequestValidationException
{
CFMetaData cfm = ThriftValidation.validateColumnFamily(keyspace(), columnFamily());
if (cfm.isCounter())
throw new InvalidRequestException("Secondary indexes are not supported on counter tables");
if (cfm.isView())
throw new InvalidRequestException("Secondary indexes are not supported on materialized views");
if (cfm.isCompactTable() && !cfm.isStaticCompactTable())
throw new InvalidRequestException("Secondary indexes are not supported on COMPACT STORAGE tables that have clustering columns");
List<IndexTarget> targets = new ArrayList<>(rawTargets.size());
for (IndexTarget.Raw rawTarget : rawTargets)
targets.add(rawTarget.prepare(cfm));
if (targets.isEmpty() && !properties.isCustom)
throw new InvalidRequestException("Only CUSTOM indexes can be created without specifying a target column");
if (targets.size() > 1)
validateTargetsForMultiColumnIndex(targets);
for (IndexTarget target : targets)
{
ColumnDefinition cd = cfm.getColumnDefinitionForCQL(target.column);
if (cd == null)
throw new InvalidRequestException("No column definition found for column " + target.column);
if (cd.type.referencesDuration())
{
checkFalse(cd.type.isCollection(), "Secondary indexes are not supported on collections containing durations");
checkFalse(cd.type.isTuple(), "Secondary indexes are not supported on tuples containing durations");
checkFalse(cd.type.isUDT(), "Secondary indexes are not supported on UDTs containing durations");
throw invalidRequest("Secondary indexes are not supported on duration columns");
}
// TODO: we could lift that limitation
if (cfm.isCompactTable())
{
if (cd.isPrimaryKeyColumn())
throw new InvalidRequestException("Secondary indexes are not supported on PRIMARY KEY columns in COMPACT STORAGE tables");
if (cfm.compactValueColumn().equals(cd))
throw new InvalidRequestException("Secondary indexes are not supported on compact value column of COMPACT STORAGE tables");
}
if (cd.kind == ColumnDefinition.Kind.PARTITION_KEY && cfm.getKeyValidatorAsClusteringComparator().size() == 1)
throw new InvalidRequestException(String.format("Cannot create secondary index on partition key column %s", target.column));
boolean isMap = cd.type instanceof MapType;
boolean isFrozenCollection = cd.type.isCollection() && !cd.type.isMultiCell();
if (isFrozenCollection)
{
validateForFrozenCollection(target);
}
else
{
validateNotFullIndex(target);
validateIsSimpleIndexIfTargetColumnNotCollection(cd, target);
validateTargetColumnIsMapIfIndexInvolvesKeys(isMap, target);
}
checkFalse(cd.type.isUDT() && cd.type.isMultiCell(), "Secondary indexes are not supported on non-frozen UDTs");
}
if (!Strings.isNullOrEmpty(indexName))
{
if (Schema.instance.getKSMetaData(keyspace()).existingIndexNames(null).contains(indexName))
{
if (ifNotExists)
return;
else
throw new InvalidRequestException(String.format("Index %s already exists", indexName));
}
}
properties.validate();
}
private void validateForFrozenCollection(IndexTarget target) throws InvalidRequestException
{
if (target.type != IndexTarget.Type.FULL)
throw new InvalidRequestException(String.format("Cannot create %s() index on frozen column %s. " +
"Frozen collections only support full() indexes",
target.type, target.column));
}
private void validateNotFullIndex(IndexTarget target) throws InvalidRequestException
{
if (target.type == IndexTarget.Type.FULL)
throw new InvalidRequestException("full() indexes can only be created on frozen collections");
}
private void validateIsSimpleIndexIfTargetColumnNotCollection(ColumnDefinition cd, IndexTarget target) throws InvalidRequestException
{
if (!cd.type.isCollection() && target.type != IndexTarget.Type.SIMPLE)
throw new InvalidRequestException(String.format("Cannot create %s() index on %s. " +
"Non-collection columns support only simple indexes",
target.type.toString(), target.column));
}
private void validateTargetColumnIsMapIfIndexInvolvesKeys(boolean isMap, IndexTarget target) throws InvalidRequestException
{
if (target.type == IndexTarget.Type.KEYS || target.type == IndexTarget.Type.KEYS_AND_VALUES)
{
if (!isMap)
throw new InvalidRequestException(String.format("Cannot create index on %s of column %s with non-map type",
target.type, target.column));
}
}
private void validateTargetsForMultiColumnIndex(List<IndexTarget> targets)
{
if (!properties.isCustom)
throw new InvalidRequestException("Only CUSTOM indexes support multiple columns");
Set<ColumnIdentifier> columns = Sets.newHashSetWithExpectedSize(targets.size());
for (IndexTarget target : targets)
if (!columns.add(target.column))
throw new InvalidRequestException("Duplicate column " + target.column + " in index target list");
}
public Event.SchemaChange announceMigration(QueryState queryState, boolean isLocalOnly) throws RequestValidationException
{
CFMetaData cfm = Schema.instance.getCFMetaData(keyspace(), columnFamily()).copy();
List<IndexTarget> targets = new ArrayList<>(rawTargets.size());
for (IndexTarget.Raw rawTarget : rawTargets)
targets.add(rawTarget.prepare(cfm));
String acceptedName = indexName;
if (Strings.isNullOrEmpty(acceptedName))
{
acceptedName = Indexes.getAvailableIndexName(keyspace(),
columnFamily(),
targets.size() == 1 ? targets.get(0).column.toString() : null);
}
if (Schema.instance.getKSMetaData(keyspace()).existingIndexNames(null).contains(acceptedName))
{
if (ifNotExists)
return null;
else
throw new InvalidRequestException(String.format("Index %s already exists", acceptedName));
}
IndexMetadata.Kind kind;
Map<String, String> indexOptions;
if (properties.isCustom)
{
kind = IndexMetadata.Kind.CUSTOM;
indexOptions = properties.getOptions();
if (properties.customClass.equals(SASIIndex.class.getName()))
{
if (!DatabaseDescriptor.getEnableSASIIndexes())
throw new InvalidRequestException("SASI indexes are disabled. Enable in cassandra.yaml to use.");
logger.warn("Creating SASI index {} for {}.{}. {}",
acceptedName, cfm.ksName, cfm.cfName, SASIIndex.USAGE_WARNING);
ClientWarn.instance.warn(SASIIndex.USAGE_WARNING);
}
}
else
{
indexOptions = Collections.emptyMap();
kind = cfm.isCompound() ? IndexMetadata.Kind.COMPOSITES : IndexMetadata.Kind.KEYS;
}
IndexMetadata index = IndexMetadata.fromIndexTargets(cfm, targets, acceptedName, kind, indexOptions);
// check to disallow creation of an index which duplicates an existing one in all but name
Optional<IndexMetadata> existingIndex = Iterables.tryFind(cfm.getIndexes(), existing -> existing.equalsWithoutName(index));
if (existingIndex.isPresent())
{
if (ifNotExists)
return null;
else
throw new InvalidRequestException(String.format("Index %s is a duplicate of existing index %s",
index.name,
existingIndex.get().name));
}
logger.trace("Updating index definition for {}", indexName);
cfm.indexes(cfm.getIndexes().with(index));
MigrationManager.announceColumnFamilyUpdate(cfm, isLocalOnly);
// Creating an index is akin to updating the CF
return new Event.SchemaChange(Event.SchemaChange.Change.UPDATED, Event.SchemaChange.Target.TABLE, keyspace(), columnFamily());
}
}