diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaDropper.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaDropper.java index 187874c45..26e35a1fe 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaDropper.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaDropper.java @@ -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 canRecreate = this.mappingContext.getUserDefinedTypeEntities().stream() .map(CassandraPersistentEntity::getTableName).collect(Collectors.toSet()); - this.cassandraAdminOperations.getKeyspaceMetadata().getUserTypes().forEach(userType -> { + Collection 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 getUserTypesToDrop(Collection knownUserTypes) { + + List toDrop = new ArrayList<>(); + + UserTypeDependencyGraphBuilder builder = new UserTypeDependencyGraphBuilder(); + knownUserTypes.forEach(builder::addUserType); + + UserTypeDependencyGraph dependencyGraph = builder.build(); + + Set 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 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 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 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 typeFilter) { + + dependencies.add(CqlIdentifier.of(userType.getTypeName()), requiredBy); + + visitTypes(userType, typeFilter); + } + + private static void doWithTypeArguments(DataType type, Consumer 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 dependencies; + + UserTypeDependencyGraph(MultiValueMap 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 getDropOrder(CqlIdentifier typeName, Predicate typeFilter) { + + List toDrop = new ArrayList<>(); + + if (typeFilter.test(typeName)) { + + List dependants = dependencies.getOrDefault(typeName, Collections.emptyList()); + dependants.stream().map(dependant -> getDropOrder(dependant, typeFilter)).forEach(toDrop::addAll); + + toDrop.add(typeName); + } + + return toDrop; + } } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaCreatorUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaCreatorUnitTests.java index 872a00121..58e0d8289 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaCreatorUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaCreatorUnitTests.java @@ -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 moons; - UniverseType universeType; - } - - @UserDefinedType - static class AstronautType { - String name; - } - - @UserDefinedType - static class SpaceAgencyType { - List astronauts; - } - - @Table - static class IndexedEntity { - - @Id String id; - @Indexed String firstName; - } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaDropperUnitTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaDropperUnitTests.java index 50b2ad402..e4dede07e 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaDropperUnitTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaDropperUnitTests.java @@ -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(); } + } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaTestSupport.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaTestSupport.java new file mode 100644 index 000000000..0325afb83 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/CassandraPersistentEntitySchemaTestSupport.java @@ -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 moons; + UniverseType universeType; + } + + @UserDefinedType + static class AstronautType { + String name; + } + + @UserDefinedType + static class SpaceAgencyType { + List astronauts; + } + + @Table + static class IndexedEntity { + + @Id String id; + @Indexed String firstName; + } + + @Table + static class Person { + @Id String id; + } +}