DATACASS-546 - Consider user-type dependencies during schema drop.
We now consider user-type dependencies while dropping user-defined types. User types can utilize other user-defined types and dropping a type that is in use causes the drop operation to fail. We're introspecting user types reported by Cassandra and construct a dependency graph that is used to determine the proper drop order.
This commit is contained in:
@@ -15,15 +15,28 @@
|
||||
*/
|
||||
package org.springframework.data.cassandra.core;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.data.cassandra.core.cql.CqlIdentifier;
|
||||
import org.springframework.data.cassandra.core.mapping.CassandraMappingContext;
|
||||
import org.springframework.data.cassandra.core.mapping.CassandraPersistentEntity;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
import com.datastax.driver.core.AbstractTableMetadata;
|
||||
import com.datastax.driver.core.DataType;
|
||||
import com.datastax.driver.core.TupleType;
|
||||
import com.datastax.driver.core.UserType;
|
||||
import com.datastax.driver.core.UserType.Field;
|
||||
|
||||
/**
|
||||
* Schema drop support for Cassandra based on {@link CassandraMappingContext} and {@link CassandraPersistentEntity}.
|
||||
@@ -85,15 +98,164 @@ public class CassandraPersistentEntitySchemaDropper {
|
||||
Set<CqlIdentifier> canRecreate = this.mappingContext.getUserDefinedTypeEntities().stream()
|
||||
.map(CassandraPersistentEntity::getTableName).collect(Collectors.toSet());
|
||||
|
||||
this.cassandraAdminOperations.getKeyspaceMetadata().getUserTypes().forEach(userType -> {
|
||||
Collection<UserType> userTypes = this.cassandraAdminOperations.getKeyspaceMetadata().getUserTypes();
|
||||
|
||||
getUserTypesToDrop(userTypes) //
|
||||
.stream() //
|
||||
.filter(it -> canRecreate.contains(it) || (dropUnused && !mappingContext.usesUserType(it))) //
|
||||
.forEach(this.cassandraAdminOperations::dropUserType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create {@link List} of {@link CqlIdentifier} with User-Defined type names to drop considering dependencies between
|
||||
* UDTs.
|
||||
*
|
||||
* @return {@link List} of {@link CqlIdentifier}.
|
||||
*/
|
||||
private List<CqlIdentifier> getUserTypesToDrop(Collection<UserType> knownUserTypes) {
|
||||
|
||||
List<CqlIdentifier> toDrop = new ArrayList<>();
|
||||
|
||||
UserTypeDependencyGraphBuilder builder = new UserTypeDependencyGraphBuilder();
|
||||
knownUserTypes.forEach(builder::addUserType);
|
||||
|
||||
UserTypeDependencyGraph dependencyGraph = builder.build();
|
||||
|
||||
Set<CqlIdentifier> globalSeen = new LinkedHashSet<>();
|
||||
|
||||
knownUserTypes.forEach(userType -> {
|
||||
|
||||
CqlIdentifier typeName = CqlIdentifier.of(userType.getTypeName());
|
||||
toDrop.addAll(dependencyGraph.getDropOrder(typeName, globalSeen::add));
|
||||
});
|
||||
|
||||
return toDrop;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for {@link UserTypeDependencyGraph}. Introspects {@link UserType} for dependencies to other user types to
|
||||
* build a dependency graph between user types.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0.7
|
||||
*/
|
||||
static class UserTypeDependencyGraphBuilder {
|
||||
|
||||
// Maps user types to other types they are referenced in.
|
||||
private final MultiValueMap<CqlIdentifier, CqlIdentifier> dependencies = new LinkedMultiValueMap<>();
|
||||
|
||||
/**
|
||||
* Add {@link UserType} to the builder and inspect its dependencies.
|
||||
*
|
||||
* @param userType must not be {@literal null.}
|
||||
*/
|
||||
void addUserType(UserType userType) {
|
||||
|
||||
Set<CqlIdentifier> seen = new LinkedHashSet<>();
|
||||
visitTypes(userType, seen::add);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the {@link UserTypeDependencyGraph}.
|
||||
*
|
||||
* @return the {@link UserTypeDependencyGraph}.
|
||||
*/
|
||||
UserTypeDependencyGraph build() {
|
||||
return new UserTypeDependencyGraph(new LinkedMultiValueMap<>(dependencies));
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit a {@link UserType} and its fields.
|
||||
*
|
||||
* @param userType
|
||||
* @param typeFilter
|
||||
*/
|
||||
private void visitTypes(UserType userType, Predicate<CqlIdentifier> typeFilter) {
|
||||
|
||||
CqlIdentifier typeName = CqlIdentifier.of(userType.getTypeName());
|
||||
|
||||
if (canRecreate.contains(typeName)) {
|
||||
this.cassandraAdminOperations.dropUserType(typeName);
|
||||
} else if (dropUnused && !mappingContext.usesUserType(typeName)) {
|
||||
this.cassandraAdminOperations.dropUserType(typeName);
|
||||
if (!typeFilter.test(typeName)) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
for (Field field : userType) {
|
||||
|
||||
if (field.getType() instanceof UserType) {
|
||||
|
||||
addDependency((UserType) field.getType(), typeName, typeFilter);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
doWithTypeArguments(field.getType(), it -> {
|
||||
|
||||
if (it instanceof UserType) {
|
||||
addDependency((UserType) it, typeName, typeFilter);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void addDependency(UserType userType, CqlIdentifier requiredBy, Predicate<CqlIdentifier> typeFilter) {
|
||||
|
||||
dependencies.add(CqlIdentifier.of(userType.getTypeName()), requiredBy);
|
||||
|
||||
visitTypes(userType, typeFilter);
|
||||
}
|
||||
|
||||
private static void doWithTypeArguments(DataType type, Consumer<DataType> callback) {
|
||||
|
||||
type.getTypeArguments().forEach(nested -> {
|
||||
callback.accept(nested);
|
||||
doWithTypeArguments(nested, callback);
|
||||
});
|
||||
|
||||
if (type instanceof TupleType) {
|
||||
|
||||
TupleType tupleType = (TupleType) type;
|
||||
|
||||
tupleType.getComponentTypes().forEach(nested -> {
|
||||
callback.accept(nested);
|
||||
doWithTypeArguments(nested, callback);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dependency graph representing user type field dependencies to other user types.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.0.7
|
||||
*/
|
||||
static class UserTypeDependencyGraph {
|
||||
|
||||
private final MultiValueMap<CqlIdentifier, CqlIdentifier> dependencies;
|
||||
|
||||
UserTypeDependencyGraph(MultiValueMap<CqlIdentifier, CqlIdentifier> dependencies) {
|
||||
this.dependencies = dependencies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the names of user types in the order they need to be dropped including type {@code typeName}.
|
||||
*
|
||||
* @param typeName
|
||||
* @param typeFilter
|
||||
* @return
|
||||
*/
|
||||
List<CqlIdentifier> getDropOrder(CqlIdentifier typeName, Predicate<CqlIdentifier> typeFilter) {
|
||||
|
||||
List<CqlIdentifier> toDrop = new ArrayList<>();
|
||||
|
||||
if (typeFilter.test(typeName)) {
|
||||
|
||||
List<CqlIdentifier> dependants = dependencies.getOrDefault(typeName, Collections.emptyList());
|
||||
dependants.stream().map(dependant -> getDropOrder(dependant, typeFilter)).forEach(toDrop::addAll);
|
||||
|
||||
toDrop.add(typeName);
|
||||
}
|
||||
|
||||
return toDrop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,11 +16,7 @@
|
||||
package org.springframework.data.cassandra.core;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.matches;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
@@ -29,13 +25,8 @@ import org.mockito.InOrder;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.cassandra.core.cql.CqlOperations;
|
||||
import org.springframework.data.cassandra.core.mapping.CassandraMappingContext;
|
||||
import org.springframework.data.cassandra.core.mapping.Indexed;
|
||||
import org.springframework.data.cassandra.core.mapping.Table;
|
||||
import org.springframework.data.cassandra.core.mapping.UserDefinedType;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link CassandraPersistentEntitySchemaCreator}.
|
||||
@@ -44,7 +35,7 @@ import org.springframework.data.cassandra.core.mapping.UserDefinedType;
|
||||
* @author Jens Schauder
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class CassandraPersistentEntitySchemaCreatorUnitTests {
|
||||
public class CassandraPersistentEntitySchemaCreatorUnitTests extends CassandraPersistentEntitySchemaTestSupport {
|
||||
|
||||
@Mock CassandraAdminOperations adminOperations;
|
||||
@Mock CqlOperations operations;
|
||||
@@ -141,38 +132,4 @@ public class CassandraPersistentEntitySchemaCreatorUnitTests {
|
||||
inOrder.verify(operations).execute(Mockito.contains("CREATE TYPE " + typename));
|
||||
}
|
||||
}
|
||||
|
||||
@UserDefinedType
|
||||
static class UniverseType {
|
||||
String name;
|
||||
}
|
||||
|
||||
@UserDefinedType
|
||||
static class MoonType {
|
||||
UniverseType universeType;
|
||||
}
|
||||
|
||||
@UserDefinedType
|
||||
static class PlanetType {
|
||||
|
||||
Set<MoonType> moons;
|
||||
UniverseType universeType;
|
||||
}
|
||||
|
||||
@UserDefinedType
|
||||
static class AstronautType {
|
||||
String name;
|
||||
}
|
||||
|
||||
@UserDefinedType
|
||||
static class SpaceAgencyType {
|
||||
List<AstronautType> astronauts;
|
||||
}
|
||||
|
||||
@Table
|
||||
static class IndexedEntity {
|
||||
|
||||
@Id String id;
|
||||
@Indexed String firstName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,6 @@ package org.springframework.data.cassandra.core;
|
||||
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
@@ -26,14 +24,15 @@ import java.util.HashSet;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.InOrder;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.cassandra.core.cql.CqlIdentifier;
|
||||
import org.springframework.data.cassandra.core.mapping.CassandraMappingContext;
|
||||
import org.springframework.data.cassandra.core.mapping.Table;
|
||||
import org.springframework.data.cassandra.core.mapping.UserDefinedType;
|
||||
import org.springframework.data.cassandra.support.UserTypeBuilder;
|
||||
|
||||
import com.datastax.driver.core.DataType;
|
||||
import com.datastax.driver.core.KeyspaceMetadata;
|
||||
import com.datastax.driver.core.TableMetadata;
|
||||
import com.datastax.driver.core.UserType;
|
||||
@@ -45,13 +44,14 @@ import com.datastax.driver.core.UserType;
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class CassandraPersistentEntitySchemaDropperUnitTests {
|
||||
public class CassandraPersistentEntitySchemaDropperUnitTests extends CassandraPersistentEntitySchemaTestSupport {
|
||||
|
||||
@Mock CassandraAdminOperations operations;
|
||||
@Mock KeyspaceMetadata metadata;
|
||||
@Mock UserType universetype;
|
||||
@Mock UserType moontype;
|
||||
@Mock UserType planettype;
|
||||
UserType universetype = UserTypeBuilder.forName("universetype").withField("name", DataType.varchar()).build();
|
||||
UserType moontype = UserTypeBuilder.forName("moontype").withField("universeType", universetype).build();
|
||||
UserType planettype = UserTypeBuilder.forName("planettype").withField("moonType", DataType.set(moontype))
|
||||
.withField("universeType", universetype).build();
|
||||
@Mock TableMetadata person;
|
||||
@Mock TableMetadata contact;
|
||||
|
||||
@@ -64,18 +64,12 @@ public class CassandraPersistentEntitySchemaDropperUnitTests {
|
||||
context.setUserTypeResolver(typeName -> metadata.getUserType(typeName.toCql()));
|
||||
|
||||
when(operations.getKeyspaceMetadata()).thenReturn(metadata);
|
||||
when(universetype.getTypeName()).thenReturn("universetype");
|
||||
when(moontype.getTypeName()).thenReturn("moontype");
|
||||
when(planettype.getTypeName()).thenReturn("planettype");
|
||||
when(person.getName()).thenReturn("person");
|
||||
when(contact.getName()).thenReturn("contact");
|
||||
}
|
||||
|
||||
@Test // DATACASS-355
|
||||
public void shouldDropTypes() throws Exception {
|
||||
|
||||
context.setInitialEntitySet(new HashSet<>(Arrays.asList(MoonType.class, UniverseType.class)));
|
||||
context.afterPropertiesSet();
|
||||
@Test // DATACASS-355, DATACASS-546
|
||||
public void shouldDropTypesInOrderOfDependencies() throws Exception {
|
||||
|
||||
when(metadata.getUserTypes()).thenReturn(Arrays.asList(universetype, moontype, planettype));
|
||||
|
||||
@@ -84,11 +78,7 @@ public class CassandraPersistentEntitySchemaDropperUnitTests {
|
||||
|
||||
schemaDropper.dropUserTypes(true);
|
||||
|
||||
verify(operations).dropUserType(CqlIdentifier.of("universetype"));
|
||||
verify(operations).dropUserType(CqlIdentifier.of("moontype"));
|
||||
verify(operations).dropUserType(CqlIdentifier.of("planettype"));
|
||||
verify(operations).getKeyspaceMetadata();
|
||||
verifyNoMoreInteractions(operations);
|
||||
verifyTypesGetDroppedInOrderFor("planettype", "moontype", "universetype");
|
||||
}
|
||||
|
||||
@Test // DATACASS-355
|
||||
@@ -147,18 +137,15 @@ public class CassandraPersistentEntitySchemaDropperUnitTests {
|
||||
verifyNoMoreInteractions(operations);
|
||||
}
|
||||
|
||||
@UserDefinedType
|
||||
@Data
|
||||
static class UniverseType {}
|
||||
private void verifyTypesGetDroppedInOrderFor(String... typenames) {
|
||||
|
||||
@UserDefinedType
|
||||
static class MoonType {}
|
||||
InOrder inOrder = Mockito.inOrder(operations);
|
||||
|
||||
@UserDefinedType
|
||||
static class PlanetType {}
|
||||
for (String typename : typenames) {
|
||||
inOrder.verify(operations).dropUserType(CqlIdentifier.of(typename));
|
||||
}
|
||||
|
||||
@Table
|
||||
static class Person {
|
||||
@Id String id;
|
||||
inOrder.verifyNoMoreInteractions();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2018 the original author or authors.
|
||||
*
|
||||
* Licensed 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.springframework.data.cassandra.core;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.cassandra.core.mapping.Indexed;
|
||||
import org.springframework.data.cassandra.core.mapping.Table;
|
||||
import org.springframework.data.cassandra.core.mapping.UserDefinedType;
|
||||
|
||||
/**
|
||||
* Support class for schema creation/drop tests.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public abstract class CassandraPersistentEntitySchemaTestSupport {
|
||||
|
||||
@UserDefinedType
|
||||
static class UniverseType {
|
||||
String name;
|
||||
}
|
||||
|
||||
@UserDefinedType
|
||||
static class MoonType {
|
||||
UniverseType universeType;
|
||||
}
|
||||
|
||||
@UserDefinedType
|
||||
static class PlanetType {
|
||||
|
||||
Set<MoonType> moons;
|
||||
UniverseType universeType;
|
||||
}
|
||||
|
||||
@UserDefinedType
|
||||
static class AstronautType {
|
||||
String name;
|
||||
}
|
||||
|
||||
@UserDefinedType
|
||||
static class SpaceAgencyType {
|
||||
List<AstronautType> astronauts;
|
||||
}
|
||||
|
||||
@Table
|
||||
static class IndexedEntity {
|
||||
|
||||
@Id String id;
|
||||
@Indexed String firstName;
|
||||
}
|
||||
|
||||
@Table
|
||||
static class Person {
|
||||
@Id String id;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user