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:
Mark Paluch
2018-04-16 16:39:40 +02:00
parent d3126cadd0
commit 7cba0e2260
4 changed files with 260 additions and 83 deletions

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

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

View File

@@ -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;
}
}