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

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

Go to download

The Apache Cassandra Project develops a highly scalable second-generation distributed database, bringing together Dynamo's fully distributed design and Bigtable's ColumnFamily-based data model.

There is a newer version: 5.0.5
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;
    public final CQL3Type.Raw validator;
    public final ColumnIdentifier.Raw rawColumnName;
    private final TableAttributes attrs;
    private final Map renames;
    private final boolean isStatic; // Only for ALTER ADD

    public AlterTableStatement(CFName name,
                               Type type,
                               ColumnIdentifier.Raw columnName,
                               CQL3Type.Raw validator,
                               TableAttributes attrs,
                               Map renames,
                               boolean isStatic)
    {
        super(name);
        this.oType = type;
        this.rawColumnName = columnName;
        this.validator = validator; // used only for ADD/ALTER commands
        this.attrs = attrs;
        this.renames = renames;
        this.isStatic = isStatic;
    }

    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();

        CQL3Type validator = this.validator == null ? null : this.validator.prepare(keyspace());
        ColumnIdentifier columnName = null;
        ColumnDefinition def = null;
        if (rawColumnName != null)
        {
            columnName = rawColumnName.prepare(cfm);
            def = cfm.getColumnDefinition(columnName);
        }

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

        switch (oType)
        {
            case ADD:
                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:
                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:
                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().prepare(cfm);
                    ColumnIdentifier to = entry.getValue().prepare(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().prepare(viewCopy.metadata);
                        ColumnIdentifier viewTo = entry.getValue().prepare(viewCopy.metadata);
                        viewCopy.renameColumn(viewFrom, viewTo);

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

        MigrationManager.announceColumnFamilyUpdate(cfm, false, 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;
        }
    }

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




© 2015 - 2025 Weber Informatics LLC | Privacy Policy