| /* |
| * 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.nio.ByteBuffer; |
| import java.util.*; |
| |
| import org.apache.cassandra.auth.Permission; |
| import org.apache.cassandra.config.*; |
| import org.apache.cassandra.cql3.*; |
| import org.apache.cassandra.db.marshal.*; |
| import org.apache.cassandra.exceptions.*; |
| import org.apache.cassandra.schema.KeyspaceMetadata; |
| import org.apache.cassandra.service.ClientState; |
| import org.apache.cassandra.service.MigrationManager; |
| import org.apache.cassandra.transport.Event; |
| |
| public abstract class AlterTypeStatement extends SchemaAlteringStatement |
| { |
| protected final UTName name; |
| |
| protected AlterTypeStatement(UTName name) |
| { |
| this.name = name; |
| } |
| |
| @Override |
| public void prepareKeyspace(ClientState state) throws InvalidRequestException |
| { |
| if (!name.hasKeyspace()) |
| name.setKeyspace(state.getKeyspace()); |
| |
| if (name.getKeyspace() == null) |
| throw new InvalidRequestException("You need to be logged in a keyspace or use a fully qualified user type name"); |
| } |
| |
| protected abstract UserType makeUpdatedType(UserType toUpdate, KeyspaceMetadata ksm) throws InvalidRequestException; |
| |
| public static AlterTypeStatement addition(UTName name, ColumnIdentifier fieldName, CQL3Type.Raw type) |
| { |
| return new AddOrAlter(name, true, fieldName, type); |
| } |
| |
| public static AlterTypeStatement alter(UTName name, ColumnIdentifier fieldName, CQL3Type.Raw type) |
| { |
| return new AddOrAlter(name, false, fieldName, type); |
| } |
| |
| public static AlterTypeStatement renames(UTName name, Map<ColumnIdentifier, ColumnIdentifier> renames) |
| { |
| return new Renames(name, renames); |
| } |
| |
| public void checkAccess(ClientState state) throws UnauthorizedException, InvalidRequestException |
| { |
| state.hasKeyspaceAccess(keyspace(), Permission.ALTER); |
| } |
| |
| public void validate(ClientState state) throws RequestValidationException |
| { |
| // Validation is left to announceMigration as it's easier to do it while constructing the updated type. |
| // It doesn't really change anything anyway. |
| } |
| |
| @Override |
| public String keyspace() |
| { |
| return name.getKeyspace(); |
| } |
| |
| public Event.SchemaChange announceMigration(boolean isLocalOnly) throws InvalidRequestException, ConfigurationException |
| { |
| KeyspaceMetadata ksm = Schema.instance.getKSMetaData(name.getKeyspace()); |
| if (ksm == null) |
| throw new InvalidRequestException(String.format("Cannot alter type in unknown keyspace %s", name.getKeyspace())); |
| |
| UserType toUpdate = |
| ksm.types.get(name.getUserTypeName()) |
| .orElseThrow(() -> new InvalidRequestException(String.format("No user type named %s exists.", name))); |
| |
| UserType updated = makeUpdatedType(toUpdate, ksm); |
| |
| // Now, we need to announce the type update to basically change it for new tables using this type, |
| // but we also need to find all existing user types and CF using it and change them. |
| MigrationManager.announceTypeUpdate(updated, isLocalOnly); |
| |
| for (CFMetaData cfm : ksm.tables) |
| { |
| CFMetaData copy = cfm.copy(); |
| boolean modified = false; |
| for (ColumnDefinition def : copy.allColumns()) |
| modified |= updateDefinition(copy, def, toUpdate.keyspace, toUpdate.name, updated); |
| if (modified) |
| MigrationManager.announceColumnFamilyUpdate(copy, isLocalOnly); |
| } |
| |
| for (ViewDefinition view : ksm.views) |
| { |
| ViewDefinition copy = view.copy(); |
| boolean modified = false; |
| for (ColumnDefinition def : copy.metadata.allColumns()) |
| modified |= updateDefinition(copy.metadata, def, toUpdate.keyspace, toUpdate.name, updated); |
| if (modified) |
| MigrationManager.announceViewUpdate(copy, isLocalOnly); |
| } |
| |
| // Other user types potentially using the updated type |
| for (UserType ut : ksm.types) |
| { |
| // Re-updating the type we've just updated would be harmless but useless so we avoid it. |
| // Besides, we use the occasion to drop the old version of the type if it's a type rename |
| if (ut.keyspace.equals(toUpdate.keyspace) && ut.name.equals(toUpdate.name)) |
| { |
| if (!ut.keyspace.equals(updated.keyspace) || !ut.name.equals(updated.name)) |
| MigrationManager.announceTypeDrop(ut); |
| continue; |
| } |
| AbstractType<?> upd = updateWith(ut, toUpdate.keyspace, toUpdate.name, updated); |
| if (upd != null) |
| MigrationManager.announceTypeUpdate((UserType) upd, isLocalOnly); |
| } |
| return new Event.SchemaChange(Event.SchemaChange.Change.UPDATED, Event.SchemaChange.Target.TYPE, keyspace(), name.getStringTypeName()); |
| } |
| |
| private static int getIdxOfField(UserType type, ColumnIdentifier field) |
| { |
| for (int i = 0; i < type.size(); i++) |
| if (field.bytes.equals(type.fieldName(i))) |
| return i; |
| return -1; |
| } |
| |
| private boolean updateDefinition(CFMetaData cfm, ColumnDefinition def, String keyspace, ByteBuffer toReplace, UserType updated) |
| { |
| AbstractType<?> t = updateWith(def.type, keyspace, toReplace, updated); |
| if (t == null) |
| return false; |
| |
| // We need to update this validator ... |
| cfm.addOrReplaceColumnDefinition(def.withNewType(t)); |
| return true; |
| } |
| |
| // Update the provided type were all instance of a given userType is replaced by a new version |
| // Note that this methods reaches inside other UserType, CompositeType and CollectionType. |
| private static AbstractType<?> updateWith(AbstractType<?> type, String keyspace, ByteBuffer toReplace, UserType updated) |
| { |
| if (type instanceof UserType) |
| { |
| UserType ut = (UserType)type; |
| |
| // If it's directly the type we've updated, then just use the new one. |
| if (keyspace.equals(ut.keyspace) && toReplace.equals(ut.name)) |
| return updated; |
| |
| // Otherwise, check for nesting |
| List<AbstractType<?>> updatedTypes = updateTypes(ut.fieldTypes(), keyspace, toReplace, updated); |
| return updatedTypes == null ? null : new UserType(ut.keyspace, ut.name, new ArrayList<>(ut.fieldNames()), updatedTypes); |
| } |
| else if (type instanceof TupleType) |
| { |
| TupleType tt = (TupleType)type; |
| List<AbstractType<?>> updatedTypes = updateTypes(tt.allTypes(), keyspace, toReplace, updated); |
| return updatedTypes == null ? null : new TupleType(updatedTypes); |
| } |
| else if (type instanceof CompositeType) |
| { |
| CompositeType ct = (CompositeType)type; |
| List<AbstractType<?>> updatedTypes = updateTypes(ct.types, keyspace, toReplace, updated); |
| return updatedTypes == null ? null : CompositeType.getInstance(updatedTypes); |
| } |
| else if (type instanceof CollectionType) |
| { |
| if (type instanceof ListType) |
| { |
| AbstractType<?> t = updateWith(((ListType)type).getElementsType(), keyspace, toReplace, updated); |
| if (t == null) |
| return null; |
| return ListType.getInstance(t, type.isMultiCell()); |
| } |
| else if (type instanceof SetType) |
| { |
| AbstractType<?> t = updateWith(((SetType)type).getElementsType(), keyspace, toReplace, updated); |
| if (t == null) |
| return null; |
| return SetType.getInstance(t, type.isMultiCell()); |
| } |
| else |
| { |
| assert type instanceof MapType; |
| MapType mt = (MapType)type; |
| AbstractType<?> k = updateWith(mt.getKeysType(), keyspace, toReplace, updated); |
| AbstractType<?> v = updateWith(mt.getValuesType(), keyspace, toReplace, updated); |
| if (k == null && v == null) |
| return null; |
| return MapType.getInstance(k == null ? mt.getKeysType() : k, v == null ? mt.getValuesType() : v, type.isMultiCell()); |
| } |
| } |
| else |
| { |
| return null; |
| } |
| } |
| |
| private static List<AbstractType<?>> updateTypes(List<AbstractType<?>> toUpdate, String keyspace, ByteBuffer toReplace, UserType updated) |
| { |
| // But this can also be nested. |
| List<AbstractType<?>> updatedTypes = null; |
| for (int i = 0; i < toUpdate.size(); i++) |
| { |
| AbstractType<?> t = updateWith(toUpdate.get(i), keyspace, toReplace, updated); |
| if (t == null) |
| continue; |
| |
| if (updatedTypes == null) |
| updatedTypes = new ArrayList<>(toUpdate); |
| |
| updatedTypes.set(i, t); |
| } |
| return updatedTypes; |
| } |
| |
| protected void checkTypeNotUsedByAggregate(KeyspaceMetadata ksm) |
| { |
| ksm.functions.udas().filter(aggregate -> aggregate.initialCondition() != null && aggregate.stateType().referencesUserType(name.getStringTypeName())) |
| .findAny() |
| .ifPresent((aggregate) -> { |
| throw new InvalidRequestException(String.format("Cannot alter user type %s as it is still used as an INITCOND by aggregate %s", name, aggregate)); |
| }); |
| } |
| |
| private static class AddOrAlter extends AlterTypeStatement |
| { |
| private final boolean isAdd; |
| private final ColumnIdentifier fieldName; |
| private final CQL3Type.Raw type; |
| |
| public AddOrAlter(UTName name, boolean isAdd, ColumnIdentifier fieldName, CQL3Type.Raw type) |
| { |
| super(name); |
| this.isAdd = isAdd; |
| this.fieldName = fieldName; |
| this.type = type; |
| } |
| |
| private UserType doAdd(UserType toUpdate) throws InvalidRequestException |
| { |
| if (getIdxOfField(toUpdate, fieldName) >= 0) |
| throw new InvalidRequestException(String.format("Cannot add new field %s to type %s: a field of the same name already exists", fieldName, name)); |
| |
| List<ByteBuffer> newNames = new ArrayList<>(toUpdate.size() + 1); |
| newNames.addAll(toUpdate.fieldNames()); |
| newNames.add(fieldName.bytes); |
| |
| AbstractType<?> addType = type.prepare(keyspace()).getType(); |
| if (addType.referencesUserType(toUpdate.getNameAsString())) |
| throw new InvalidRequestException(String.format("Cannot add new field %s of type %s to type %s as this would create a circular reference", fieldName, type, name)); |
| |
| List<AbstractType<?>> newTypes = new ArrayList<>(toUpdate.size() + 1); |
| newTypes.addAll(toUpdate.fieldTypes()); |
| newTypes.add(addType); |
| |
| return new UserType(toUpdate.keyspace, toUpdate.name, newNames, newTypes); |
| } |
| |
| private UserType doAlter(UserType toUpdate, KeyspaceMetadata ksm) throws InvalidRequestException |
| { |
| checkTypeNotUsedByAggregate(ksm); |
| |
| int idx = getIdxOfField(toUpdate, fieldName); |
| if (idx < 0) |
| throw new InvalidRequestException(String.format("Unknown field %s in type %s", fieldName, name)); |
| |
| AbstractType<?> previous = toUpdate.fieldType(idx); |
| if (!type.prepare(keyspace()).getType().isCompatibleWith(previous)) |
| throw new InvalidRequestException(String.format("Type %s is incompatible with previous type %s of field %s in user type %s", type, previous.asCQL3Type(), fieldName, name)); |
| |
| List<ByteBuffer> newNames = new ArrayList<>(toUpdate.fieldNames()); |
| List<AbstractType<?>> newTypes = new ArrayList<>(toUpdate.fieldTypes()); |
| newTypes.set(idx, type.prepare(keyspace()).getType()); |
| |
| return new UserType(toUpdate.keyspace, toUpdate.name, newNames, newTypes); |
| } |
| |
| protected UserType makeUpdatedType(UserType toUpdate, KeyspaceMetadata ksm) throws InvalidRequestException |
| { |
| return isAdd ? doAdd(toUpdate) : doAlter(toUpdate, ksm); |
| } |
| } |
| |
| private static class Renames extends AlterTypeStatement |
| { |
| private final Map<ColumnIdentifier, ColumnIdentifier> renames; |
| |
| public Renames(UTName name, Map<ColumnIdentifier, ColumnIdentifier> renames) |
| { |
| super(name); |
| this.renames = renames; |
| } |
| |
| protected UserType makeUpdatedType(UserType toUpdate, KeyspaceMetadata ksm) throws InvalidRequestException |
| { |
| checkTypeNotUsedByAggregate(ksm); |
| |
| List<ByteBuffer> newNames = new ArrayList<>(toUpdate.fieldNames()); |
| List<AbstractType<?>> newTypes = new ArrayList<>(toUpdate.fieldTypes()); |
| |
| for (Map.Entry<ColumnIdentifier, ColumnIdentifier> entry : renames.entrySet()) |
| { |
| ColumnIdentifier from = entry.getKey(); |
| ColumnIdentifier to = entry.getValue(); |
| int idx = getIdxOfField(toUpdate, from); |
| if (idx < 0) |
| throw new InvalidRequestException(String.format("Unknown field %s in type %s", from, name)); |
| newNames.set(idx, to.bytes); |
| } |
| |
| UserType updated = new UserType(toUpdate.keyspace, toUpdate.name, newNames, newTypes); |
| CreateTypeStatement.checkForDuplicateNames(updated); |
| return updated; |
| } |
| |
| } |
| } |