All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.cassandra.cql3.statements.AlterTableStatement Maven / Gradle / Ivy

There is a newer version: 4.3.1.0
Show newest version
/*
 * 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 java.util.stream.Collectors;

import com.google.common.collect.Iterables;

import org.apache.cassandra.auth.Permission;
import org.apache.cassandra.config.*;
import org.apache.cassandra.cql3.CFName;
import org.apache.cassandra.cql3.CQL3Type;
import org.apache.cassandra.cql3.ColumnIdentifier;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.CollectionType;
import org.apache.cassandra.db.marshal.CounterColumnType;
import org.apache.cassandra.db.marshal.ReversedType;
import org.apache.cassandra.db.view.View;
import org.apache.cassandra.exceptions.*;
import org.apache.cassandra.schema.IndexMetadata;
import org.apache.cassandra.schema.Indexes;
import org.apache.cassandra.schema.TableParams;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.service.MigrationManager;
import org.apache.cassandra.transport.Event;

import static org.apache.cassandra.thrift.ThriftValidation.validateColumnFamily;

public class AlterTableStatement extends SchemaAlteringStatement
{
    public enum Type
    {
        ADD, ALTER, DROP, OPTS, RENAME
    }

    public final Type oType;
    private final TableAttributes attrs;
    private final Map renames;
    private final List colNameList;

    public AlterTableStatement(CFName name,
                               Type type,
                               List colDataList,
                               TableAttributes attrs,
                               Map renames)
    {
        super(name);
        this.oType = type;
        this.colNameList = colDataList;
        this.attrs = attrs;
        this.renames = renames;
    }

    public void checkAccess(ClientState state) throws UnauthorizedException, InvalidRequestException
    {
        state.hasColumnFamilyAccess(keyspace(), columnFamily(), Permission.ALTER);
    }

    public void validate(ClientState state)
    {
        // validated in announceMigration()
    }

    public Event.SchemaChange announceMigration(boolean isLocalOnly) throws RequestValidationException
    {
        CFMetaData meta = validateColumnFamily(keyspace(), columnFamily());
        if (meta.isView())
            throw new InvalidRequestException("Cannot use ALTER TABLE on Materialized View");

        CFMetaData cfm = meta.copy();
        ColumnIdentifier columnName = null;
        ColumnDefinition def = null;
        CQL3Type.Raw dataType = null;
        boolean isStatic = false;
        CQL3Type validator = null;
        ColumnDefinition.Raw rawColumnName = null;

        List viewUpdates = null;
        Iterable views = View.findAll(keyspace(), columnFamily());

        switch (oType)
        {
            case ADD:
                for (AlterTableStatementColumn colData : colNameList)
                {
                    rawColumnName = colData.getColumnName();
                    if (rawColumnName != null)
                    {
                        columnName = rawColumnName.getIdentifier(cfm);
                        def =  cfm.getColumnDefinition(columnName);
                        dataType = colData.getColumnType();
                        isStatic = colData.getStaticType();
                        validator = dataType == null ? null : dataType.prepare(keyspace());
                    }

                    assert columnName != null;
                    if (cfm.isDense())
                        throw new InvalidRequestException("Cannot add new column to a COMPACT STORAGE table");

                    if (isStatic)
                    {
                        if (!cfm.isCompound())
                            throw new InvalidRequestException("Static columns are not allowed in COMPACT STORAGE tables");
                        if (cfm.clusteringColumns().isEmpty())
                            throw new InvalidRequestException("Static columns are only useful (and thus allowed) if the table has at least one clustering column");
                    }

                    if (def != null)
                    {
                        switch (def.kind)
                        {
                            case PARTITION_KEY:
                            case CLUSTERING:
                                throw new InvalidRequestException(String.format("Invalid column name %s because it conflicts with a PRIMARY KEY part", columnName));
                            default:
                                throw new InvalidRequestException(String.format("Invalid column name %s because it conflicts with an existing column", columnName));
                        }
                    }

                    // Cannot re-add a dropped counter column. See #7831.
                    if (meta.isCounter() && meta.getDroppedColumns().containsKey(columnName.bytes))
                        throw new InvalidRequestException(String.format("Cannot re-add previously dropped counter column %s", columnName));

                    AbstractType type = validator.getType();
                    if (type.isCollection() && type.isMultiCell())
                    {
                        if (!cfm.isCompound())
                            throw new InvalidRequestException("Cannot use non-frozen collections in COMPACT STORAGE tables");
                        if (cfm.isSuper())
                            throw new InvalidRequestException("Cannot use non-frozen collections with super column families");

                        // If there used to be a non-frozen collection column with the same name (that has been dropped),
                        // we could still have some data using the old type, and so we can't allow adding a collection
                        // with the same name unless the types are compatible (see #6276).
                        CFMetaData.DroppedColumn dropped = cfm.getDroppedColumns().get(columnName.bytes);
                        if (dropped != null && dropped.type instanceof CollectionType
                            && dropped.type.isMultiCell() && !type.isCompatibleWith(dropped.type))
                        {
                            String message =
                                String.format("Cannot add a collection with the name %s because a collection with the same name"
                                              + " and a different type (%s) has already been used in the past",
                                              columnName,
                                              dropped.type.asCQL3Type());
                            throw new InvalidRequestException(message);
                        }
                    }

                    cfm.addColumnDefinition(isStatic
                                            ? ColumnDefinition.staticDef(cfm, columnName.bytes, type)
                                            : ColumnDefinition.regularDef(cfm, columnName.bytes, type));

                    // Adding a column to a table which has an include all view requires the column to be added to the view
                    // as well
                    if (!isStatic)
                    {
                        for (ViewDefinition view : views)
                        {
                            if (view.includeAllColumns)
                            {
                                ViewDefinition viewCopy = view.copy();
                                viewCopy.metadata.addColumnDefinition(ColumnDefinition.regularDef(viewCopy.metadata, columnName.bytes, type));
                                if (viewUpdates == null)
                                    viewUpdates = new ArrayList<>();
                                viewUpdates.add(viewCopy);
                            }
                        }
                    }
                }
                break;

            case ALTER:
                rawColumnName = colNameList.get(0).getColumnName();
                if (rawColumnName != null)
                {
                    columnName = rawColumnName.getIdentifier(cfm);
                    def = cfm.getColumnDefinition(columnName);
                    dataType = colNameList.get(0).getColumnType();
                    validator = dataType == null ? null : dataType.prepare(keyspace());
                }

                assert columnName != null;
                if (def == null)
                    throw new InvalidRequestException(String.format("Column %s was not found in table %s", columnName, columnFamily()));

                AbstractType validatorType = def.isReversedType() && !validator.getType().isReversed()
                                                ? ReversedType.getInstance(validator.getType())
                                                : validator.getType();
                validateAlter(cfm, def, validatorType);
                // In any case, we update the column definition
                cfm.addOrReplaceColumnDefinition(def.withNewType(validatorType));

                // We also have to validate the view types here. If we have a view which includes a column as part of
                // the clustering key, we need to make sure that it is indeed compatible.
                for (ViewDefinition view : views)
                {
                    if (!view.includes(columnName)) continue;
                    ViewDefinition viewCopy = view.copy();
                    ColumnDefinition viewDef = view.metadata.getColumnDefinition(columnName);
                    AbstractType viewType = viewDef.isReversedType() && !validator.getType().isReversed()
                                            ? ReversedType.getInstance(validator.getType())
                                            : validator.getType();
                    validateAlter(view.metadata, viewDef, viewType);
                    viewCopy.metadata.addOrReplaceColumnDefinition(viewDef.withNewType(viewType));

                    if (viewUpdates == null)
                        viewUpdates = new ArrayList<>();
                    viewUpdates.add(viewCopy);
                }
                break;

            case DROP:
                for (AlterTableStatementColumn colData : colNameList)
                {
                    columnName = null;
                    rawColumnName = colData.getColumnName();
                    if (rawColumnName != null)
                    {
                        columnName = rawColumnName.getIdentifier(cfm);
                        def = cfm.getColumnDefinition(columnName);
                    }
                    assert columnName != null;
                    if (!cfm.isCQLTable())
                        throw new InvalidRequestException("Cannot drop columns from a non-CQL3 table");
                    if (def == null)
                        throw new InvalidRequestException(String.format("Column %s was not found in table %s", columnName, columnFamily()));

                    switch (def.kind)
                    {
                         case PARTITION_KEY:
                         case CLUSTERING:
                              throw new InvalidRequestException(String.format("Cannot drop PRIMARY KEY part %s", columnName));
                         case REGULAR:
                         case STATIC:
                              ColumnDefinition toDelete = null;
                              for (ColumnDefinition columnDef : cfm.partitionColumns())
                              {
                                   if (columnDef.name.equals(columnName))
                                      {
                                        toDelete = columnDef;
                                        break;
                                      }
                               }
                        assert toDelete != null;
                        cfm.removeColumnDefinition(toDelete);
                        cfm.recordColumnDrop(toDelete);
                        break;
                    }

                    // If the dropped column is required by any secondary indexes
                    // we reject the operation, as the indexes must be dropped first
                    Indexes allIndexes = cfm.getIndexes();
                    if (!allIndexes.isEmpty())
                    {
                        ColumnFamilyStore store = Keyspace.openAndGetStore(cfm);
                        Set dependentIndexes = store.indexManager.getDependentIndexes(def);
                        if (!dependentIndexes.isEmpty())
                            throw new InvalidRequestException(String.format("Cannot drop column %s because it has " +
                                                                            "dependent secondary indexes (%s)",
                                                                            def,
                                                                            dependentIndexes.stream()
                                                                                            .map(i -> i.name)
                                                                                            .collect(Collectors.joining(","))));
                    }

                    // If a column is dropped which is included in a view, we don't allow the drop to take place.
                    boolean rejectAlter = false;
                    StringBuilder builder = new StringBuilder();
                    for (ViewDefinition view : views)
                    {
                        if (!view.includes(columnName)) continue;
                        if (rejectAlter)
                            builder.append(',');
                        rejectAlter = true;
                        builder.append(view.viewName);
                    }
                    if (rejectAlter)
                        throw new InvalidRequestException(String.format("Cannot drop column %s, depended on by materialized views (%s.{%s})",
                                                                        columnName.toString(),
                                                                        keyspace(),
                                                                        builder.toString()));
                }
                break;
            case OPTS:
                if (attrs == null)
                    throw new InvalidRequestException("ALTER TABLE WITH invoked, but no parameters found");
                attrs.validate();

                TableParams params = attrs.asAlteredTableParams(cfm.params);

                if (!Iterables.isEmpty(views) && params.gcGraceSeconds == 0)
                {
                    throw new InvalidRequestException("Cannot alter gc_grace_seconds of the base table of a " +
                                                      "materialized view to 0, since this value is used to TTL " +
                                                      "undelivered updates. Setting gc_grace_seconds too low might " +
                                                      "cause undelivered updates to expire " +
                                                      "before being replayed.");
                }

                if (meta.isCounter() && params.defaultTimeToLive > 0)
                    throw new InvalidRequestException("Cannot set default_time_to_live on a table with counters");

                cfm.params(params);

                break;
            case RENAME:
                for (Map.Entry entry : renames.entrySet())
                {
                    ColumnIdentifier from = entry.getKey().getIdentifier(cfm);
                    ColumnIdentifier to = entry.getValue().getIdentifier(cfm);
                    cfm.renameColumn(from, to);

                    // If the view includes a renamed column, it must be renamed in the view table and the definition.
                    for (ViewDefinition view : views)
                    {
                        if (!view.includes(from)) continue;

                        ViewDefinition viewCopy = view.copy();
                        ColumnIdentifier viewFrom = entry.getKey().getIdentifier(viewCopy.metadata);
                        ColumnIdentifier viewTo = entry.getValue().getIdentifier(viewCopy.metadata);
                        viewCopy.renameColumn(viewFrom, viewTo);

                        if (viewUpdates == null)
                            viewUpdates = new ArrayList<>();
                        viewUpdates.add(viewCopy);
                    }
                }
                break;
        }

        MigrationManager.announceColumnFamilyUpdate(cfm, isLocalOnly);

        if (viewUpdates != null)
        {
            for (ViewDefinition viewUpdate : viewUpdates)
                MigrationManager.announceViewUpdate(viewUpdate, isLocalOnly);
        }
        return new Event.SchemaChange(Event.SchemaChange.Change.UPDATED, Event.SchemaChange.Target.TABLE, keyspace(), columnFamily());
    }

    private static void validateAlter(CFMetaData cfm, ColumnDefinition def, AbstractType validatorType)
    {
        switch (def.kind)
        {
            case PARTITION_KEY:
                if (validatorType instanceof CounterColumnType)
                    throw new InvalidRequestException(String.format("counter type is not supported for PRIMARY KEY part %s", def.name));

                AbstractType currentType = cfm.getKeyValidatorAsClusteringComparator().subtype(def.position());
                if (!validatorType.isValueCompatibleWith(currentType))
                    throw new ConfigurationException(String.format("Cannot change %s from type %s to type %s: types are incompatible.",
                                                                   def.name,
                                                                   currentType.asCQL3Type(),
                                                                   validatorType.asCQL3Type()));
                break;
            case CLUSTERING:
                if (!cfm.isCQLTable())
                    throw new InvalidRequestException(String.format("Cannot alter clustering column %s in a non-CQL3 table", def.name));

                AbstractType oldType = cfm.comparator.subtype(def.position());
                // Note that CFMetaData.validateCompatibility already validate the change we're about to do. However, the error message it
                // sends is a bit cryptic for a CQL3 user, so validating here for a sake of returning a better error message
                // Do note that we need isCompatibleWith here, not just isValueCompatibleWith.
                if (!validatorType.isCompatibleWith(oldType))
                {
                    throw new ConfigurationException(String.format("Cannot change %s from type %s to type %s: types are not order-compatible.",
                                                                   def.name,
                                                                   oldType.asCQL3Type(),
                                                                   validatorType.asCQL3Type()));
                }
                break;
            case REGULAR:
            case STATIC:
                // Thrift allows to change a column validator so CFMetaData.validateCompatibility will let it slide
                // if we change to an incompatible type (contrarily to the comparator case). But we don't want to
                // allow it for CQL3 (see #5882) so validating it explicitly here. We only care about value compatibility
                // though since we won't compare values (except when there is an index, but that is validated by
                // ColumnDefinition already).
                if (!validatorType.isValueCompatibleWith(def.type))
                    throw new ConfigurationException(String.format("Cannot change %s from type %s to type %s: types are incompatible.",
                                                                   def.name,
                                                                   def.type.asCQL3Type(),
                                                                   validatorType.asCQL3Type()));
                break;
        }
    }

    @Override
    public String toString()
    {
        return String.format("AlterTableStatement(name=%s, type=%s)",
                             cfName,
                             oType);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy