diff --git a/ci/test.sh b/ci/test.sh index ea820843d..c60e198c2 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -6,7 +6,7 @@ mkdir -p /tmp/jenkins-home export JENKINS_USER=${JENKINS_USER_NAME} export SDN_FORCE_REUSE_OF_CONTAINERS=true -export SDN_NEO4J_VERSION=5.26.2 +export SDN_NEO4J_VERSION=5.26.12 MAVEN_OPTS="-Duser.name=${JENKINS_USER} -Duser.home=/tmp/jenkins-home -Dscan=false" \ ./mvnw -s settings.xml -P${PROFILE} clean dependency:list verify -Dsort -U -B -Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-neo4j -Ddevelocity.storage.directory=/tmp/jenkins-home/.develocity-root diff --git a/pom.xml b/pom.xml index 923e8fe67..2013e7a9a 100644 --- a/pom.xml +++ b/pom.xml @@ -75,7 +75,7 @@ 1.0.8.RELEASE ${skipTests} 8.40 - 2024.5.1 + 2025.2.2 spring-data-neo4j SDNEO4J 1.7.0 @@ -91,7 +91,7 @@ ${java.version} 5.28.10 2.17.3 - 4.4.41 + 5.26.12 3.0.1 ${project.build.directory}/docs UTF-8 diff --git a/src/main/java/org/springframework/data/neo4j/core/DynamicLabels.java b/src/main/java/org/springframework/data/neo4j/core/DynamicLabels.java index b9db66e19..549cd76b1 100644 --- a/src/main/java/org/springframework/data/neo4j/core/DynamicLabels.java +++ b/src/main/java/org/springframework/data/neo4j/core/DynamicLabels.java @@ -52,11 +52,13 @@ final class DynamicLabels implements UnaryOperator { public OngoingMatchAndUpdate apply(OngoingMatchAndUpdate ongoingMatchAndUpdate) { OngoingMatchAndUpdate decoratedMatchAndUpdate = ongoingMatchAndUpdate; - if (!oldLabels.isEmpty()) { - decoratedMatchAndUpdate = decoratedMatchAndUpdate.remove(rootNode, oldLabels.toArray(new String[0])); + if (!this.oldLabels.isEmpty()) { + decoratedMatchAndUpdate = decoratedMatchAndUpdate.remove(this.rootNode, + Cypher.allLabels(Cypher.anonParameter(this.oldLabels))); } - if (!newLabels.isEmpty()) { - decoratedMatchAndUpdate = decoratedMatchAndUpdate.set(rootNode, newLabels.toArray(new String[0])); + if (!this.newLabels.isEmpty()) { + decoratedMatchAndUpdate = decoratedMatchAndUpdate.set(this.rootNode, + Cypher.allLabels(Cypher.anonParameter(this.newLabels))); } return decoratedMatchAndUpdate; } diff --git a/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java b/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java index 992e36662..d64305b62 100644 --- a/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java +++ b/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java @@ -452,10 +452,12 @@ private T saveImpl(T instance, @Nullable Collection) entityToBeSaved.getClass()) ); + Statement statement = cypherGenerator.prepareSaveOf(entityMetaData, dynamicLabels, TemplateSupport.rendererRendersElementId(renderer)); Optional newOrUpdatedNode = neo4jClient - .query(() -> renderer.render(cypherGenerator.prepareSaveOf(entityMetaData, dynamicLabels, TemplateSupport.rendererRendersElementId(renderer)))) + .query(() -> renderer.render(statement)) .bind(entityToBeSaved) .with(binderFunction) + .bindAll(statement.getCatalog().getParameters()) .fetchAs(Entity.class) .one(); @@ -496,11 +498,13 @@ private DynamicLabels determineDynamicLabels(T entityToBeSaved, Neo4jPersist PersistentPropertyAccessor propertyAccessor = entityMetaData.getPropertyAccessor(entityToBeSaved); Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty(); + var statementReturningDynamicLabels = cypherGenerator.createStatementReturningDynamicLabels(entityMetaData); Neo4jClient.RunnableSpec runnableQuery = neo4jClient - .query(() -> renderer.render(cypherGenerator.createStatementReturningDynamicLabels(entityMetaData))) + .query(() -> renderer.render(statementReturningDynamicLabels)) .bind(convertIdValues(idProperty, propertyAccessor.getProperty(idProperty))) .to(Constants.NAME_OF_ID).bind(entityMetaData.getStaticLabels()) - .to(Constants.NAME_OF_STATIC_LABELS_PARAM); + .to(Constants.NAME_OF_STATIC_LABELS_PARAM) + .bindAll(statementReturningDynamicLabels.getCatalog().getParameters()); if (entityMetaData.hasVersionProperty()) { runnableQuery = runnableQuery @@ -579,9 +583,11 @@ class Tuple3 { binderFunction = TemplateSupport.createAndApplyPropertyFilter(pps, entityMetaData, binderFunction); List> entityList = entitiesToBeSaved.stream().map(h -> h.modifiedInstance).map(binderFunction) .collect(Collectors.toList()); + var statement = cypherGenerator.prepareSaveOfMultipleInstancesOf(entityMetaData); Map idToInternalIdMapping = neo4jClient - .query(() -> renderer.render(cypherGenerator.prepareSaveOfMultipleInstancesOf(entityMetaData))) + .query(() -> renderer.render(statement)) .bind(entityList).to(Constants.NAME_OF_ENTITY_LIST_PARAM) + .bindAll(statement.getCatalog().getParameters()) .fetchAs(Map.Entry.class) .mappedBy((t, r) -> new AbstractMap.SimpleEntry<>(r.get(Constants.NAME_OF_ID), TemplateSupport.convertIdOrElementIdToString(r.get(Constants.NAME_OF_ELEMENT_ID)))) .all() @@ -673,7 +679,9 @@ public void deleteById(Object id, Class domainType) { Statement statement = cypherGenerator.prepareDeleteOf(entityMetaData, condition); ResultSummary summary = this.neo4jClient.query(renderer.render(statement)) .bind(convertIdValues(entityMetaData.getRequiredIdProperty(), id)) - .to(nameOfParameter).run(); + .to(nameOfParameter) + .bindAll(statement.getCatalog().getParameters()) + .run(); log.debug(() -> String.format("Deleted %d nodes and %d relationships.", summary.counters().nodesDeleted(), summary.counters().relationshipsDeleted())); @@ -724,7 +732,9 @@ public void deleteAllById(Iterable ids, Class domainType) { Statement statement = cypherGenerator.prepareDeleteOf(entityMetaData, condition); ResultSummary summary = this.neo4jClient.query(renderer.render(statement)) .bind(convertIdValues(entityMetaData.getRequiredIdProperty(), ids)) - .to(nameOfParameter).run(); + .to(nameOfParameter) + .bindAll(statement.getCatalog().getParameters()) + .run(); log.debug(() -> String.format("Deleted %d nodes and %d relationships.", summary.counters().nodesDeleted(), summary.counters().relationshipsDeleted())); @@ -873,6 +883,7 @@ private T processNestedRelations( .to(Constants.FROM_ID_PARAMETER_NAME) // .bind(knownRelationshipsIds) // .to(Constants.NAME_OF_KNOWN_RELATIONSHIPS_PARAM) // + .bindAll(relationshipRemoveQuery.getCatalog().getParameters()) .run(); } @@ -1023,6 +1034,7 @@ private T processNestedRelations( statementHolder = statementHolder.addProperty(Constants.NAME_OF_RELATIONSHIP_LIST_PARAM, plainRelationshipRows); neo4jClient.query(renderer.render(statementHolder.getStatement())) .bindAll(statementHolder.getProperties()) + .bindAll(statementHolder.getStatement().getCatalog().getParameters()) .run(); } else if (relationshipDescription.hasRelationshipProperties()) { if (!relationshipPropertiesRows.isEmpty()) { @@ -1032,6 +1044,7 @@ private T processNestedRelations( neo4jClient.query(renderer.render(statementHolder.getStatement())) .bindAll(statementHolder.getProperties()) + .bindAll(statementHolder.getStatement().getCatalog().getParameters()) .run(); } if (!newRelationshipPropertiesToStore.isEmpty()) { @@ -1039,6 +1052,7 @@ private T processNestedRelations( sourceEntity, relationshipDescription, newRelationshipPropertiesToStore, newRelationshipPropertiesRows, canUseElementId); List all = new ArrayList<>(neo4jClient.query(renderer.render(statementHolder.getStatement())) .bindAll(statementHolder.getProperties()) + .bindAll(statementHolder.getStatement().getCatalog().getParameters()) .fetchAs(Object.class) .mappedBy((t, r) -> IdentitySupport.mapperForRelatedIdValues(idProperty).apply(r)) .all()); @@ -1068,6 +1082,7 @@ private Optional getRelationshipId(Statement statement, Neo4jPersistentP .to(Constants.FROM_ID_PARAMETER_NAME) // .bind(toId) // .to(Constants.TO_ID_PARAMETER_NAME) // + .bindAll(statement.getCatalog().getParameters()) .fetchAs(Object.class) .mappedBy((t, r) -> IdentitySupport.mapperForRelatedIdValues(idProperty).apply(r)) .one(); @@ -1126,9 +1141,11 @@ private Entity saveRelatedNode(Object entity, NodeDescription targetNodeDescr } return tree; }); + var statement = cypherGenerator.prepareSaveOf(targetNodeDescription, dynamicLabels, TemplateSupport.rendererRendersElementId(renderer)); Optional optionalSavedNode = neo4jClient - .query(() -> renderer.render(cypherGenerator.prepareSaveOf(targetNodeDescription, dynamicLabels, TemplateSupport.rendererRendersElementId(renderer)))) + .query(() -> renderer.render(statement)) .bind(entity).with(binderFunction) + .bindAll(statement.getCatalog().getParameters()) .fetchAs(Entity.class) .one(); @@ -1424,6 +1441,7 @@ private void iterateNextLevel(Collection nodeIds, RelationshipDescriptio neo4jClient.query(renderer.render(statement)) .bindAll(Collections.singletonMap(Constants.NAME_OF_IDS, TemplateSupport.convertToLongIdOrStringElementId(nodeIds))) + .bindAll(statement.getCatalog().getParameters()) .fetch() .one() .ifPresent(iterateAndMapNextLevel(relationshipsToRelatedNodes, relationshipDescription, nextPathStep)); diff --git a/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java b/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java index 7d93f646d..650ecf4ec 100644 --- a/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java +++ b/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java @@ -464,9 +464,11 @@ private Mono saveImpl(T instance, @Nullable Collection) entityToBeSaved.getClass())); boolean canUseElementId = TemplateSupport.rendererRendersElementId(renderer); - Mono idMono = this.neo4jClient.query(() -> renderer.render(cypherGenerator.prepareSaveOf(entityMetaData, dynamicLabels, canUseElementId))) + var statement = cypherGenerator.prepareSaveOf(entityMetaData, dynamicLabels, canUseElementId); + Mono idMono = this.neo4jClient.query(() -> renderer.render(statement)) .bind(entityToBeSaved) .with(binderFunction) + .bindAll(statement.getCatalog().getParameters()) .fetchAs(Entity.class) .one() .switchIfEmpty(Mono.defer(() -> { @@ -498,10 +500,12 @@ private Mono> determineDynamicLabels(T entityToBeSa PersistentPropertyAccessor propertyAccessor = entityMetaData.getPropertyAccessor(entityToBeSaved); Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty(); + var statementReturningDynamicLabels = cypherGenerator.createStatementReturningDynamicLabels(entityMetaData); ReactiveNeo4jClient.RunnableSpec runnableQuery = neo4jClient - .query(() -> renderer.render(cypherGenerator.createStatementReturningDynamicLabels(entityMetaData))) + .query(() -> renderer.render(statementReturningDynamicLabels)) .bind(convertIdValues(idProperty, propertyAccessor.getProperty(idProperty))) - .to(Constants.NAME_OF_ID).bind(entityMetaData.getStaticLabels()).to(Constants.NAME_OF_STATIC_LABELS_PARAM); + .to(Constants.NAME_OF_ID).bind(entityMetaData.getStaticLabels()).to(Constants.NAME_OF_STATIC_LABELS_PARAM) + .bindAll(statementReturningDynamicLabels.getCatalog().getParameters()); if (entityMetaData.hasVersionProperty()) { runnableQuery = runnableQuery @@ -613,9 +617,11 @@ private Flux saveAllImpl(Iterable instances, @Nullable Collection> boundedEntityList = entitiesToBeSaved.stream() .map(Tuple3::getT3) // extract PotentiallyModified .map(binderFunction).collect(Collectors.toList()); - return neo4jClient - .query(() -> renderer.render(cypherGenerator.prepareSaveOfMultipleInstancesOf(entityMetaData))) + var statement = cypherGenerator.prepareSaveOfMultipleInstancesOf(entityMetaData); + return neo4jClient + .query(() -> renderer.render(statement)) .bind(boundedEntityList).to(Constants.NAME_OF_ENTITY_LIST_PARAM) + .bindAll(statement.getCatalog().getParameters()) .fetchAs(Tuple2.class) .mappedBy((t, r) -> Tuples.of(r.get(Constants.NAME_OF_ID), TemplateSupport.convertIdOrElementIdToString(r.get(Constants.NAME_OF_ELEMENT_ID)))) .all() @@ -623,7 +629,6 @@ private Flux saveAllImpl(Iterable instances, @Nullable Collection Flux.fromIterable(entitiesToBeSaved) .concatMap(t -> { PersistentPropertyAccessor propertyAccessor = entityMetaData.getPropertyAccessor(t.getT3()); - Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty(); return processRelations(entityMetaData, propertyAccessor, t.getT2(), ctx.get("stateMachine"), ctx.get("knownRelIds"), @@ -648,7 +653,9 @@ public Mono deleteAllById(Iterable ids, Class domainType) { return transactionalOperator.transactional(Mono.defer(() -> this.neo4jClient.query(() -> renderer.render(statement)) .bind(convertIdValues(entityMetaData.getRequiredIdProperty(), ids)) - .to(nameOfParameter).run().then())); + .to(nameOfParameter) + .bindAll(statement.getCatalog().getParameters()) + .run().then())); } @Override @@ -664,7 +671,9 @@ public Mono deleteById(Object id, Class domainType) { return transactionalOperator.transactional(Mono.defer(() -> this.neo4jClient.query(() -> renderer.render(statement)) .bind(convertIdValues(entityMetaData.getRequiredIdProperty(), id)) - .to(nameOfParameter).run().then())); + .to(nameOfParameter) + .bindAll(statement.getCatalog().getParameters()) + .run().then())); } @Override @@ -970,7 +979,8 @@ private Mono processNestedRelations(Neo4jPersistentEntity sourceEntity .bind(convertIdValues(sourceEntity.getIdProperty(), fromId)) // .to(Constants.FROM_ID_PARAMETER_NAME) // .bind(knownRelationshipsIds) // - .to(Constants.NAME_OF_KNOWN_RELATIONSHIPS_PARAM) // + .to(Constants.NAME_OF_KNOWN_RELATIONSHIPS_PARAM) + .bindAll(relationshipRemoveQuery.getCatalog().getParameters()) .run().checkpoint("delete relationships").then()); } @@ -1077,6 +1087,7 @@ private Mono processNestedRelations(Neo4jPersistentEntity sourceEntity .bind(idValue) // .to(Constants.NAME_OF_KNOWN_RELATIONSHIP_PARAM) // .bindAll(statementHolder.getProperties()) + .bindAll(statementHolder.getStatement().getCatalog().getParameters()) .fetchAs(Object.class) .mappedBy((t, r) -> IdentitySupport.mapperForRelatedIdValues(idProperty).apply(r)) .one() @@ -1143,6 +1154,7 @@ private Mono getRelationshipId(Statement statement, Neo4jPersistentPrope .to(Constants.FROM_ID_PARAMETER_NAME) // .bind(toId) // .to(Constants.TO_ID_PARAMETER_NAME) // + .bindAll(statement.getCatalog().getParameters()) .fetchAs(Object.class) .mappedBy((t, r) -> IdentitySupport.mapperForRelatedIdValues(idProperty).apply(r)) .one(); @@ -1156,11 +1168,13 @@ private Mono loadRelatedNode(NodeDescription targetNodeDescription, O var queryFragmentsAndParameters = QueryFragmentsAndParameters.forFindById(targetPersistentEntity, convertIdValues(targetPersistentEntity.getRequiredIdProperty(), relatedInternalId)); var nodeName = Constants.NAME_OF_TYPED_ROOT_NODE.apply(targetNodeDescription).getValue(); + var statement = cypherGenerator.prepareFindOf(targetNodeDescription, queryFragmentsAndParameters.getQueryFragments().getMatchOn(), + queryFragmentsAndParameters.getQueryFragments().getCondition()).returning(nodeName).build(); return neo4jClient .query(() -> renderer.render( - cypherGenerator.prepareFindOf(targetNodeDescription, queryFragmentsAndParameters.getQueryFragments().getMatchOn(), - queryFragmentsAndParameters.getQueryFragments().getCondition()).returning(nodeName).build())) + statement)) .bindAll(queryFragmentsAndParameters.getParameters()) + .bindAll(statement.getCatalog().getParameters()) .fetchAs(Entity.class).mappedBy((t, r) -> r.get(nodeName).asNode()) .one(); } @@ -1191,9 +1205,11 @@ private Mono saveRelatedNode(Object relatedNode, Neo4jPersistentEntity renderer.render(cypherGenerator.prepareSaveOf(targetNodeDescription, dynamicLabels, TemplateSupport.rendererRendersElementId(renderer)))) + .query(() -> renderer.render(statement)) .bind(entity).with(binderFunction) + .bindAll(statement.getCatalog().getParameters()) .fetchAs(Entity.class) .one(); }).switchIfEmpty(Mono.defer(() -> { diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java b/src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java index b378c7572..3bfed8a94 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/CypherGenerator.java @@ -21,7 +21,6 @@ import static org.neo4j.cypherdsl.core.Cypher.listBasedOn; import static org.neo4j.cypherdsl.core.Cypher.literalOf; import static org.neo4j.cypherdsl.core.Cypher.match; -import static org.neo4j.cypherdsl.core.Cypher.node; import static org.neo4j.cypherdsl.core.Cypher.optionalMatch; import static org.neo4j.cypherdsl.core.Cypher.parameter; @@ -94,6 +93,14 @@ public enum CypherGenerator { } }; + private static Node node(String primaryLabel, List additionalLabels) { + var labels = Cypher.exactlyLabel(primaryLabel); + if (!additionalLabels.isEmpty()) { + labels = labels.conjunctionWith(Cypher.allLabels(Cypher.anonParameter(additionalLabels))); + } + return Cypher.node(labels); + } + /** * Set function to be used to query either elementId or id. * diff --git a/src/test/java/org/springframework/data/neo4j/core/mapping/CypherGeneratorTest.java b/src/test/java/org/springframework/data/neo4j/core/mapping/CypherGeneratorTest.java index c4dd3330e..a0976b629 100644 --- a/src/test/java/org/springframework/data/neo4j/core/mapping/CypherGeneratorTest.java +++ b/src/test/java/org/springframework/data/neo4j/core/mapping/CypherGeneratorTest.java @@ -27,6 +27,7 @@ import java.util.stream.Stream; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -34,6 +35,7 @@ import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mockito; import org.neo4j.cypherdsl.core.Cypher; +import org.neo4j.cypherdsl.core.FunctionInvocation; import org.neo4j.cypherdsl.core.Statement; import org.neo4j.cypherdsl.core.renderer.Configuration; import org.neo4j.cypherdsl.core.renderer.Dialect; @@ -49,6 +51,12 @@ */ class CypherGeneratorTest { + @BeforeAll + static void fixTheAbusOfASingleton() { + CypherGenerator.INSTANCE + .setElementIdOrIdFunction(n -> FunctionInvocation.create(() -> "elementId", n.getRequiredSymbolicName())); + } + @Test void shouldCreateRelationshipCreationQueryWithLabelIfPresent() { Neo4jPersistentEntity persistentEntity = new Neo4jMappingContext().getPersistentEntity(Entity1.class); diff --git a/src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/MovieRepository.java b/src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/MovieRepository.java index 3920080e6..70a9bc8ec 100644 --- a/src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/MovieRepository.java +++ b/src/test/java/org/springframework/data/neo4j/documentation/repositories/custom_queries/MovieRepository.java @@ -21,7 +21,7 @@ import static org.neo4j.cypherdsl.core.Cypher.name; import static org.neo4j.cypherdsl.core.Cypher.node; import static org.neo4j.cypherdsl.core.Cypher.parameter; -import static org.neo4j.cypherdsl.core.Cypher.shortestPath; +import static org.neo4j.cypherdsl.core.Cypher.shortestK; // end::domain-results-impl[] import java.util.Collection; @@ -82,7 +82,7 @@ public List findMoviesAlongShortestPath(PersonEntity from, PersonEn var p1 = node("Person").withProperties("name", parameter("person1")); var p2 = node("Person").withProperties("name", parameter("person2")); - var shortestPath = shortestPath("p").definedBy( + var shortestPath = shortestK(1).named("p").definedBy( p1.relationshipBetween(p2).unbounded() ); var p = shortestPath.getRequiredSymbolicName(); diff --git a/src/test/java/org/springframework/data/neo4j/integration/cascading/AbstractCascadingTestBase.java b/src/test/java/org/springframework/data/neo4j/integration/cascading/AbstractCascadingTestBase.java index 2488fa93a..ca1b9be71 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/cascading/AbstractCascadingTestBase.java +++ b/src/test/java/org/springframework/data/neo4j/integration/cascading/AbstractCascadingTestBase.java @@ -22,10 +22,13 @@ import java.util.Map; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; import org.neo4j.driver.Driver; import org.neo4j.driver.types.TypeSystem; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.neo4j.test.Neo4jExtension; +@Tag(Neo4jExtension.NEEDS_VERSION_SUPPORTING_ELEMENT_ID) abstract class AbstractCascadingTestBase { @Autowired diff --git a/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/TypeConversionIT.java b/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/TypeConversionIT.java index f99b61d51..37a95bdb4 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/TypeConversionIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/TypeConversionIT.java @@ -39,6 +39,7 @@ import org.junit.jupiter.api.DynamicContainer; import org.junit.jupiter.api.DynamicNode; import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestFactory; import org.neo4j.driver.Driver; @@ -70,6 +71,7 @@ import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; import org.springframework.data.neo4j.test.BookmarkCapture; +import org.springframework.data.neo4j.test.Neo4jExtension; import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; import org.springframework.test.util.ReflectionTestUtils; @@ -77,11 +79,14 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; /** + * Tag due to the requirements on db.create.setNodeVectorProperty + * * @author Michael J. Simons * @author Dennis Crissman * @soundtrack Tool - Fear Inoculum */ @Neo4jIntegrationTest +@Tag(Neo4jExtension.NEEDS_VECTOR_INDEX) class TypeConversionIT extends Neo4jConversionsITBase { private final CypherTypesRepository cypherTypesRepository; diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/DynamicLabelsIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/DynamicLabelsIT.java index 7fe213a2a..f98ce2c51 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/DynamicLabelsIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/DynamicLabelsIT.java @@ -15,9 +15,6 @@ */ package org.springframework.data.neo4j.integration.imperative; -import static org.assertj.core.api.Assertions.assertThat; -import static org.neo4j.cypherdsl.core.Cypher.parameter; - import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -32,19 +29,18 @@ import org.neo4j.cypherdsl.core.Condition; import org.neo4j.cypherdsl.core.Cypher; import org.neo4j.cypherdsl.core.Node; +import org.neo4j.cypherdsl.core.renderer.Dialect; import org.neo4j.cypherdsl.core.renderer.Renderer; import org.neo4j.driver.Driver; import org.neo4j.driver.Record; import org.neo4j.driver.Session; import org.neo4j.driver.TransactionContext; import org.neo4j.driver.Value; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.neo4j.integration.shared.common.Port; -import org.springframework.data.neo4j.repository.Neo4jRepository; -import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; -import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; +import org.springframework.context.annotation.Primary; import org.springframework.data.neo4j.config.Neo4jEntityScanner; import org.springframework.data.neo4j.core.DatabaseSelectionProvider; import org.springframework.data.neo4j.core.Neo4jTemplate; @@ -64,8 +60,12 @@ import org.springframework.data.neo4j.integration.shared.common.EntitiesWithDynamicLabels.SimpleDynamicLabelsWithVersion; import org.springframework.data.neo4j.integration.shared.common.EntitiesWithDynamicLabels.SuperNode; import org.springframework.data.neo4j.integration.shared.common.EntityWithDynamicLabelsAndIdThatNeedsToBeConverted; +import org.springframework.data.neo4j.integration.shared.common.Port; +import org.springframework.data.neo4j.repository.Neo4jRepository; +import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories; import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; +import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -73,29 +73,170 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.support.TransactionTemplate; +import static org.assertj.core.api.Assertions.assertThat; +import static org.neo4j.cypherdsl.core.Cypher.parameter; + /** * @author Michael J. Simons - * @soundtrack Samy Deluxe - Samy Deluxe */ @ExtendWith(Neo4jExtension.class) -public class DynamicLabelsIT { +final class DynamicLabelsIT { + + private static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + + private DynamicLabelsIT() { + } + + interface PortRepository extends Neo4jRepository { + + List findByLabelsContaining(String label); + + } + + interface AbstractBaseEntityWithDynamicLabelsRepository + extends Neo4jRepository { + + } + + public static class DialectConfig extends SpringTestBase.Config { + + @Bean + @Primary + public org.neo4j.cypherdsl.core.renderer.Configuration getConfiguration() { + if (neo4jConnectionSupport.isCypher5SyntaxCompatible()) { + return org.neo4j.cypherdsl.core.renderer.Configuration.newConfig().withDialect(Dialect.NEO4J_5_DEFAULT_CYPHER).build(); + } + + return org.neo4j.cypherdsl.core.renderer.Configuration.newConfig().withDialect(Dialect.NEO4J_4).build(); + } + + } + + @ExtendWith(SpringExtension.class) + @ContextConfiguration(classes = DialectConfig.class) + @DirtiesContext + abstract static class SpringTestBase { + + @Autowired + protected Driver driver; + + @Autowired + protected TransactionTemplate transactionTemplate; + + @Autowired + protected BookmarkCapture bookmarkCapture; + + protected Long existingEntityId; + + abstract Long createTestEntity(TransactionContext ctx); + + T executeInTransaction(Callable runnable) { + return this.transactionTemplate.execute(tx -> { + try { + return runnable.call(); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + }); + } + + @BeforeEach + void setupData() { + try (Session session = this.driver.session()) { + session.executeWrite(tx -> tx.run("MATCH (n) DETACH DELETE n").consume()); + this.existingEntityId = session.executeWrite(this::createTestEntity); + this.bookmarkCapture.seedWith(session.lastBookmarks()); + } + } + + protected final List getLabels(Long id) { + return getLabels(Cypher.anyNode().named("n").internalId().isEqualTo(parameter("id")), id); + } + + protected final List getLabels(Condition idCondition, Object id) { + + Node n = Cypher.anyNode("n"); + String cypher = Renderer.getDefaultRenderer() + .render(Cypher.match(n) + .where(idCondition) + .and(n.property("moreLabels").isNull()) + .returning(n.labels().as("labels")) + .build()); + + try (Session session = this.driver.session(this.bookmarkCapture.createSessionConfig())) { + return session.executeRead(tx -> tx.run(cypher, Collections.singletonMap("id", id)) + .single() + .get("labels") + .asList(Value::asString)); + } + } + + @Configuration + @EnableTransactionManagement + @EnableNeo4jRepositories(considerNestedRepositories = true) + static class Config extends Neo4jImperativeTestConfiguration { + + @Bean + @Override + public Driver driver() { + return neo4jConnectionSupport.getDriver(); + } + + @Bean + BookmarkCapture bookmarkCapture() { + return new BookmarkCapture(); + } + + @Override + public PlatformTransactionManager transactionManager(Driver driver, + DatabaseSelectionProvider databaseNameProvider) { + + BookmarkCapture bookmarkCapture = bookmarkCapture(); + return new Neo4jTransactionManager(driver, databaseNameProvider, + Neo4jBookmarkManager.create(bookmarkCapture)); + } + + @Bean + TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) { + return new TransactionTemplate(transactionManager); + } + + @Bean + @Override + public Neo4jMappingContext neo4jMappingContext(Neo4jConversions neo4JConversions) + throws ClassNotFoundException { + + Neo4jMappingContext mappingContext = new Neo4jMappingContext(neo4JConversions); + mappingContext.setInitialEntitySet( + Neo4jEntityScanner.get().scan(EntitiesWithDynamicLabels.class.getPackage().getName())); + return mappingContext; + } + + @Override + public boolean isCypher5Compatible() { + return neo4jConnectionSupport.isCypher5SyntaxCompatible(); + } + + } - protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + } @Nested class EntityWithSingleStaticLabelAndGeneratedId extends SpringTestBase { @Override Long createTestEntity(TransactionContext transaction) { - Record r = transaction - .run("CREATE (e:InheritedSimpleDynamicLabels:SimpleDynamicLabels:Foo:Bar:Baz:Foobar) RETURN id(e) as existingEntityId").single(); + Record r = transaction.run( + "CREATE (e:InheritedSimpleDynamicLabels:SimpleDynamicLabels:Foo:Bar:Baz:Foobar) RETURN id(e) as existingEntityId") + .single(); return r.get("existingEntityId").asLong(); } @Test void shouldReadDynamicLabels(@Autowired Neo4jTemplate template) { - Optional optionalEntity = template.findById(existingEntityId, SimpleDynamicLabels.class); + Optional optionalEntity = template.findById(this.existingEntityId, + SimpleDynamicLabels.class); assertThat(optionalEntity).hasValueSatisfying( entity -> assertThat(entity.moreLabels).containsExactlyInAnyOrder("Foo", "Bar", "Baz", "Foobar")); } @@ -104,14 +245,15 @@ void shouldReadDynamicLabels(@Autowired Neo4jTemplate template) { void shouldUpdateDynamicLabels(@Autowired Neo4jTemplate template) { executeInTransaction(() -> { - SimpleDynamicLabels entity = template.findById(existingEntityId, SimpleDynamicLabels.class).get(); + SimpleDynamicLabels entity = template.findById(this.existingEntityId, SimpleDynamicLabels.class).get(); entity.moreLabels.remove("Foo"); entity.moreLabels.add("Fizz"); return template.save(entity); }); - List labels = getLabels(existingEntityId); - assertThat(labels).containsExactlyInAnyOrder("SimpleDynamicLabels", "InheritedSimpleDynamicLabels", "Fizz", "Bar", "Baz", "Foobar"); + List labels = getLabels(this.existingEntityId); + assertThat(labels).containsExactlyInAnyOrder("SimpleDynamicLabels", "InheritedSimpleDynamicLabels", "Fizz", + "Bar", "Baz", "Foobar"); } @Test @@ -146,6 +288,7 @@ void shouldWriteDynamicLabelsFromRelatedNodes(@Autowired Neo4jTemplate template) List labels = getLabels(id); assertThat(labels).containsExactlyInAnyOrder("SimpleDynamicLabels", "A", "B", "C"); } + } @Nested @@ -153,16 +296,16 @@ class EntityWithInheritedDynamicLabels extends SpringTestBase { @Override Long createTestEntity(TransactionContext transaction) { - Record r = transaction - .run("CREATE (e:InheritedSimpleDynamicLabels:SimpleDynamicLabels:Foo:Bar:Baz:Foobar) RETURN id(e) as existingEntityId") - .single(); + Record r = transaction.run( + "CREATE (e:InheritedSimpleDynamicLabels:SimpleDynamicLabels:Foo:Bar:Baz:Foobar) RETURN id(e) as existingEntityId") + .single(); return r.get("existingEntityId").asLong(); } @Test void shouldReadDynamicLabels(@Autowired Neo4jTemplate template) { - Optional optionalEntity = template.findById(existingEntityId, + Optional optionalEntity = template.findById(this.existingEntityId, InheritedSimpleDynamicLabels.class); assertThat(optionalEntity).hasValueSatisfying( entity -> assertThat(entity.moreLabels).containsExactlyInAnyOrder("Foo", "Bar", "Baz", "Foobar")); @@ -172,15 +315,17 @@ void shouldReadDynamicLabels(@Autowired Neo4jTemplate template) { void shouldUpdateDynamicLabels(@Autowired Neo4jTemplate template) { executeInTransaction(() -> { - InheritedSimpleDynamicLabels entity = template.findById(existingEntityId, InheritedSimpleDynamicLabels.class) - .get(); + InheritedSimpleDynamicLabels entity = template + .findById(this.existingEntityId, InheritedSimpleDynamicLabels.class) + .get(); entity.moreLabels.remove("Foo"); entity.moreLabels.add("Fizz"); return template.save(entity); }); - List labels = getLabels(existingEntityId); - assertThat(labels).containsExactlyInAnyOrder("SimpleDynamicLabels", "InheritedSimpleDynamicLabels", "Fizz", "Bar", "Baz", "Foobar"); + List labels = getLabels(this.existingEntityId); + assertThat(labels).containsExactlyInAnyOrder("SimpleDynamicLabels", "InheritedSimpleDynamicLabels", "Fizz", + "Bar", "Baz", "Foobar"); } @Test @@ -196,8 +341,10 @@ void shouldWriteDynamicLabels(@Autowired Neo4jTemplate template) { }); List labels = getLabels(id); - assertThat(labels).containsExactlyInAnyOrder("SimpleDynamicLabels", "InheritedSimpleDynamicLabels", "A", "B", "C"); + assertThat(labels).containsExactlyInAnyOrder("SimpleDynamicLabels", "InheritedSimpleDynamicLabels", "A", + "B", "C"); } + } @Nested @@ -206,9 +353,9 @@ class EntityWithSingleStaticLabelAndAssignedId extends SpringTestBase { @Override Long createTestEntity(TransactionContext transaction) { Record r = transaction.run(""" - CREATE (e:SimpleDynamicLabelsWithBusinessId:Foo:Bar:Baz:Foobar {id: 'E1'}) - RETURN id(e) as existingEntityId - """).single(); + CREATE (e:SimpleDynamicLabelsWithBusinessId:Foo:Bar:Baz:Foobar {id: 'E1'}) + RETURN id(e) as existingEntityId + """).single(); return r.get("existingEntityId").asLong(); } @@ -216,15 +363,17 @@ RETURN id(e) as existingEntityId void shouldUpdateDynamicLabels(@Autowired Neo4jTemplate template) { executeInTransaction(() -> { - SimpleDynamicLabelsWithBusinessId entity = template.findById("E1", SimpleDynamicLabelsWithBusinessId.class) - .get(); + SimpleDynamicLabelsWithBusinessId entity = template + .findById("E1", SimpleDynamicLabelsWithBusinessId.class) + .get(); entity.moreLabels.remove("Foo"); entity.moreLabels.add("Fizz"); return template.save(entity); }); - List labels = getLabels(existingEntityId); - assertThat(labels).containsExactlyInAnyOrder("SimpleDynamicLabelsWithBusinessId", "Fizz", "Bar", "Baz", "Foobar"); + List labels = getLabels(this.existingEntityId); + assertThat(labels).containsExactlyInAnyOrder("SimpleDynamicLabelsWithBusinessId", "Fizz", "Bar", "Baz", + "Foobar"); } @Test @@ -243,6 +392,35 @@ void shouldWriteDynamicLabels(@Autowired Neo4jTemplate template) { List labels = getLabels(Cypher.anyNode("n").property("id").isEqualTo(parameter("id")), result.id); assertThat(labels).containsExactlyInAnyOrder("SimpleDynamicLabelsWithBusinessId", "A", "B", "C"); } + + @Test + void saveAllShouldWork(@Autowired Neo4jTemplate template) { + var newId = executeInTransaction(() -> { + SimpleDynamicLabelsWithBusinessId entity = template + .findById("E1", SimpleDynamicLabelsWithBusinessId.class) + .orElseThrow(); + entity.moreLabels.remove("Foo"); + entity.moreLabels.add("Fizz"); + + SimpleDynamicLabelsWithBusinessId entity2 = new SimpleDynamicLabelsWithBusinessId(); + entity2.id = UUID.randomUUID().toString(); + entity2.moreLabels = new HashSet<>(); + entity2.moreLabels.add("A"); + entity2.moreLabels.add("B"); + entity2.moreLabels.add("C"); + + template.saveAll(List.of(entity, entity2)); + + return entity2.id; + }); + + assertThat(getLabels(this.existingEntityId)).containsExactlyInAnyOrder("SimpleDynamicLabelsWithBusinessId", + "Fizz", "Bar", "Baz", "Foobar"); + + assertThat(getLabels(Cypher.anyNode("n").property("id").isEqualTo(parameter("id")), newId)) + .containsExactlyInAnyOrder("SimpleDynamicLabelsWithBusinessId", "A", "B", "C"); + } + } @Nested @@ -250,8 +428,10 @@ class EntityWithSingleStaticLabelGeneratedIdAndVersion extends SpringTestBase { @Override Long createTestEntity(TransactionContext transaction) { - Record r = transaction.run("CREATE (e:SimpleDynamicLabelsWithVersion:Foo:Bar:Baz:Foobar {myVersion: 0}) " - + "RETURN id(e) as existingEntityId").single(); + Record r = transaction + .run("CREATE (e:SimpleDynamicLabelsWithVersion:Foo:Bar:Baz:Foobar {myVersion: 0}) " + + "RETURN id(e) as existingEntityId") + .single(); return r.get("existingEntityId").asLong(); } @@ -260,34 +440,36 @@ void shouldUpdateDynamicLabels(@Autowired Neo4jTemplate template) { SimpleDynamicLabelsWithVersion result = executeInTransaction(() -> { SimpleDynamicLabelsWithVersion entity = template - .findById(existingEntityId, SimpleDynamicLabelsWithVersion.class) - .get(); + .findById(this.existingEntityId, SimpleDynamicLabelsWithVersion.class) + .get(); entity.moreLabels.remove("Foo"); entity.moreLabels.add("Fizz"); return template.save(entity); }); assertThat(result.myVersion).isNotNull().isEqualTo(1); - List labels = getLabels(existingEntityId); - assertThat(labels).containsExactlyInAnyOrder("SimpleDynamicLabelsWithVersion", "Fizz", "Bar", "Baz", "Foobar"); + List labels = getLabels(this.existingEntityId); + assertThat(labels).containsExactlyInAnyOrder("SimpleDynamicLabelsWithVersion", "Fizz", "Bar", "Baz", + "Foobar"); } @Test void shouldWriteDynamicLabels(@Autowired Neo4jTemplate template) { SimpleDynamicLabelsWithVersion result = executeInTransaction(() -> { - SimpleDynamicLabelsWithVersion entity = new SimpleDynamicLabelsWithVersion(); - entity.moreLabels = new HashSet<>(); - entity.moreLabels.add("A"); - entity.moreLabels.add("B"); - entity.moreLabels.add("C"); - return template.save(entity); + SimpleDynamicLabelsWithVersion entity = new SimpleDynamicLabelsWithVersion(); + entity.moreLabels = new HashSet<>(); + entity.moreLabels.add("A"); + entity.moreLabels.add("B"); + entity.moreLabels.add("C"); + return template.save(entity); }); assertThat(result.myVersion).isNotNull().isEqualTo(0); List labels = getLabels(result.id); assertThat(labels).containsExactlyInAnyOrder("SimpleDynamicLabelsWithVersion", "A", "B", "C"); } + } @Nested @@ -295,8 +477,9 @@ class EntityWithSingleStaticLabelAssignedIdAndVersion extends SpringTestBase { @Override Long createTestEntity(TransactionContext transaction) { - Record r = transaction.run("CREATE (e:SimpleDynamicLabelsWithBusinessIdAndVersion:Foo:Bar:Baz:Foobar {id: 'E2', myVersion: 0}) RETURN id(e) as existingEntityId") - .single(); + Record r = transaction.run( + "CREATE (e:SimpleDynamicLabelsWithBusinessIdAndVersion:Foo:Bar:Baz:Foobar {id: 'E2', myVersion: 0}) RETURN id(e) as existingEntityId") + .single(); return r.get("existingEntityId").asLong(); } @@ -305,17 +488,17 @@ void shouldUpdateDynamicLabels(@Autowired Neo4jTemplate template) { SimpleDynamicLabelsWithBusinessIdAndVersion result = executeInTransaction(() -> { SimpleDynamicLabelsWithBusinessIdAndVersion entity = template - .findById("E2", SimpleDynamicLabelsWithBusinessIdAndVersion.class).get(); + .findById("E2", SimpleDynamicLabelsWithBusinessIdAndVersion.class) + .get(); entity.moreLabels.remove("Foo"); entity.moreLabels.add("Fizz"); return template.save(entity); }); assertThat(result.myVersion).isNotNull().isEqualTo(1); - List labels = getLabels(existingEntityId); - assertThat(labels) - .containsExactlyInAnyOrder("SimpleDynamicLabelsWithBusinessIdAndVersion", "Fizz", "Bar", "Baz", - "Foobar"); + List labels = getLabels(this.existingEntityId); + assertThat(labels).containsExactlyInAnyOrder("SimpleDynamicLabelsWithBusinessIdAndVersion", "Fizz", "Bar", + "Baz", "Foobar"); } @Test @@ -335,6 +518,7 @@ void shouldWriteDynamicLabels(@Autowired Neo4jTemplate template) { List labels = getLabels(Cypher.anyNode("n").property("id").isEqualTo(parameter("id")), result.id); assertThat(labels).containsExactlyInAnyOrder("SimpleDynamicLabelsWithBusinessIdAndVersion", "A", "B", "C"); } + } @Nested @@ -343,19 +527,20 @@ class ConstructorInitializedEntity extends SpringTestBase { @Override Long createTestEntity(TransactionContext transaction) { Record r = transaction - .run("CREATE (e:SimpleDynamicLabelsCtor:Foo:Bar:Baz:Foobar) RETURN id(e) as existingEntityId") - .single(); + .run("CREATE (e:SimpleDynamicLabelsCtor:Foo:Bar:Baz:Foobar) RETURN id(e) as existingEntityId") + .single(); return r.get("existingEntityId").asLong(); } @Test void shouldReadDynamicLabels(@Autowired Neo4jTemplate template) { - Optional optionalEntity = template.findById(existingEntityId, + Optional optionalEntity = template.findById(this.existingEntityId, SimpleDynamicLabelsCtor.class); assertThat(optionalEntity).hasValueSatisfying( entity -> assertThat(entity.moreLabels).containsExactlyInAnyOrder("Foo", "Bar", "Baz", "Foobar")); } + } @Nested @@ -364,25 +549,26 @@ class ClassesWithAdditionalLabels extends SpringTestBase { @Override Long createTestEntity(TransactionContext transaction) { Record r = transaction - .run("CREATE (e:SimpleDynamicLabels:Foo:Bar:Baz:Foobar) RETURN id(e) as existingEntityId").single(); + .run("CREATE (e:SimpleDynamicLabels:Foo:Bar:Baz:Foobar) RETURN id(e) as existingEntityId") + .single(); return r.get("existingEntityId").asLong(); } @Test void shouldReadDynamicLabelsOnClassWithSingleNodeLabel(@Autowired Neo4jTemplate template) { - Optional optionalEntity = template.findById(existingEntityId, + Optional optionalEntity = template.findById(this.existingEntityId, DynamicLabelsWithNodeLabel.class); assertThat(optionalEntity).hasValueSatisfying(entity -> assertThat(entity.moreLabels) - .containsExactlyInAnyOrder("SimpleDynamicLabels", "Foo", "Bar", "Foobar")); + .containsExactlyInAnyOrder("SimpleDynamicLabels", "Foo", "Bar", "Foobar")); } @Test void shouldReadDynamicLabelsOnClassWithMultipleNodeLabel(@Autowired Neo4jTemplate template) { - Optional optionalEntity = template.findById(existingEntityId, + Optional optionalEntity = template.findById(this.existingEntityId, DynamicLabelsWithMultipleNodeLabels.class); - assertThat(optionalEntity).hasValueSatisfying( - entity -> assertThat(entity.moreLabels).containsExactlyInAnyOrder("SimpleDynamicLabels", "Baz", "Foobar")); + assertThat(optionalEntity).hasValueSatisfying(entity -> assertThat(entity.moreLabels) + .containsExactlyInAnyOrder("SimpleDynamicLabels", "Baz", "Foobar")); } @Test // GH-2296 @@ -390,15 +576,16 @@ void shouldConvertIds(@Autowired Neo4jTemplate template) { template.deleteAll(EntityWithDynamicLabelsAndIdThatNeedsToBeConverted.class); EntityWithDynamicLabelsAndIdThatNeedsToBeConverted savedInstance = template - .save(new EntityWithDynamicLabelsAndIdThatNeedsToBeConverted("value_1")); + .save(new EntityWithDynamicLabelsAndIdThatNeedsToBeConverted("value_1")); assertThat(savedInstance.getValue()).isEqualTo("value_1"); assertThat(savedInstance.getExtraLabels()).containsExactlyInAnyOrder("value_1"); - Optional optionalReloadedInstance = - template.findById(savedInstance.getId(), EntityWithDynamicLabelsAndIdThatNeedsToBeConverted.class); + Optional optionalReloadedInstance = template + .findById(savedInstance.getId(), EntityWithDynamicLabelsAndIdThatNeedsToBeConverted.class); assertThat(optionalReloadedInstance).hasValueSatisfying(v -> v.getExtraLabels().contains("value_1")); } + } @Nested @@ -406,18 +593,21 @@ class ClassesWithAdditionalLabelsInInheritanceTree extends SpringTestBase { @Override Long createTestEntity(TransactionContext transaction) { - Record r = transaction.run("CREATE (e:DynamicLabelsBaseClass:ExtendedBaseClass1:D1:D2:D3) RETURN id(e) as existingEntityId") - .single(); + Record r = transaction + .run("CREATE (e:DynamicLabelsBaseClass:ExtendedBaseClass1:D1:D2:D3) RETURN id(e) as existingEntityId") + .single(); return r.get("existingEntityId").asLong(); } @Test void shouldReadDynamicLabelsInInheritance(@Autowired Neo4jTemplate template) { - Optional optionalEntity = template.findById(existingEntityId, ExtendedBaseClass1.class); - assertThat(optionalEntity) - .hasValueSatisfying(entity -> assertThat(entity.moreLabels).containsExactlyInAnyOrder("D1", "D2", "D3")); + Optional optionalEntity = template.findById(this.existingEntityId, + ExtendedBaseClass1.class); + assertThat(optionalEntity).hasValueSatisfying( + entity -> assertThat(entity.moreLabels).containsExactlyInAnyOrder("D1", "D2", "D3")); } + } @Nested @@ -430,16 +620,16 @@ Long createTestEntity(TransactionContext t) { @Test void instantiateConcreteEntityType(@Autowired AbstractBaseEntityWithDynamicLabelsRepository repository) { - EntitiesWithDynamicLabels.EntityWithMultilevelInheritanceAndDynamicLabels entity = - new EntitiesWithDynamicLabels.EntityWithMultilevelInheritanceAndDynamicLabels(); + EntitiesWithDynamicLabels.EntityWithMultilevelInheritanceAndDynamicLabels entity = new EntitiesWithDynamicLabels.EntityWithMultilevelInheritanceAndDynamicLabels(); entity.labels = Collections.singleton("AdditionalLabel"); entity.name = "Name"; entity.id = "ID1"; repository.save(entity); - EntitiesWithDynamicLabels.EntityWithMultilevelInheritanceAndDynamicLabels loadedEntity = - (EntitiesWithDynamicLabels.EntityWithMultilevelInheritanceAndDynamicLabels) repository.findById("ID1").get(); + EntitiesWithDynamicLabels.EntityWithMultilevelInheritanceAndDynamicLabels loadedEntity = (EntitiesWithDynamicLabels.EntityWithMultilevelInheritanceAndDynamicLabels) repository + .findById("ID1") + .get(); assertThat(loadedEntity.labels).contains("AdditionalLabel"); } @@ -464,103 +654,7 @@ void findByDynamicLabelsContainingShouldWork(@Autowired PortRepository portRepos List ports = portRepository.findByLabelsContaining("A"); assertThat(ports).hasSize(2); } - } - interface PortRepository extends Neo4jRepository { - List findByLabelsContaining(String label); } - interface AbstractBaseEntityWithDynamicLabelsRepository extends Neo4jRepository {} - - @ExtendWith(SpringExtension.class) - @ContextConfiguration(classes = SpringTestBase.Config.class) - @DirtiesContext - abstract static class SpringTestBase { - - @Autowired protected Driver driver; - - @Autowired protected TransactionTemplate transactionTemplate; - - @Autowired protected BookmarkCapture bookmarkCapture; - - protected Long existingEntityId; - - abstract Long createTestEntity(TransactionContext ctx); - - T executeInTransaction(Callable runnable) { - return transactionTemplate.execute(tx -> { - try { - return runnable.call(); - } catch (Exception e) { - throw new RuntimeException(e); - } - }); - } - - @BeforeEach - void setupData() { - try (Session session = driver.session()) { - session.executeWrite(tx -> tx.run("MATCH (n) DETACH DELETE n").consume()); - existingEntityId = session.executeWrite(this::createTestEntity); - bookmarkCapture.seedWith(session.lastBookmarks()); - } - } - - protected final List getLabels(Long id) { - return getLabels(Cypher.anyNode().named("n").internalId().isEqualTo(parameter("id")), id); - } - - protected final List getLabels(Condition idCondition, Object id) { - - Node n = Cypher.anyNode("n"); - String cypher = Renderer.getDefaultRenderer().render(Cypher.match(n).where(idCondition) - .and(n.property("moreLabels").isNull()).returning(n.labels().as("labels")).build()); - - try (Session session = driver.session(bookmarkCapture.createSessionConfig())) { - return session.executeRead( - tx -> tx.run(cypher, Collections.singletonMap("id", id)).single().get("labels").asList(Value::asString)); - } - } - - @Configuration - @EnableTransactionManagement - @EnableNeo4jRepositories(considerNestedRepositories = true) - static class Config extends Neo4jImperativeTestConfiguration { - - @Bean - public Driver driver() { - return neo4jConnectionSupport.getDriver(); - } - - @Bean - public BookmarkCapture bookmarkCapture() { - return new BookmarkCapture(); - } - - @Override - public PlatformTransactionManager transactionManager(Driver driver, DatabaseSelectionProvider databaseNameProvider) { - - BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)); - } - - @Bean - public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) { - return new TransactionTemplate(transactionManager); - } - - @Bean - public Neo4jMappingContext neo4jMappingContext(Neo4jConversions neo4JConversions) throws ClassNotFoundException { - - Neo4jMappingContext mappingContext = new Neo4jMappingContext(neo4JConversions); - mappingContext.setInitialEntitySet(Neo4jEntityScanner.get().scan(EntitiesWithDynamicLabels.class.getPackage().getName())); - return mappingContext; - } - - @Override - public boolean isCypher5Compatible() { - return neo4jConnectionSupport.isCypher5SyntaxCompatible(); - } - } - } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/InheritanceMappingIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/InheritanceMappingIT.java index 6d1d9e525..60a44b8e8 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/InheritanceMappingIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/InheritanceMappingIT.java @@ -27,6 +27,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIf; import org.neo4j.driver.Driver; import org.neo4j.driver.Record; import org.neo4j.driver.Session; @@ -52,6 +53,7 @@ import org.springframework.data.neo4j.test.Neo4jExtension; import org.springframework.data.neo4j.test.Neo4jImperativeTestConfiguration; import org.springframework.data.neo4j.test.Neo4jIntegrationTest; +import org.springframework.data.neo4j.test.ServerVersion; import org.springframework.data.repository.query.Param; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; @@ -333,7 +335,12 @@ void mixedInterfaces(@Autowired Neo4jTemplate template) { } } + static boolean isGreaterThanOrEqualNeo4j5() { + return neo4jConnectionSupport.getServerVersion().greaterThanOrEqual(ServerVersion.v5_0_0); + } + @Test // GH-2788 + @EnabledIf("isGreaterThanOrEqualNeo4j5") void detectPropertiesAndRelationshipsOfImplementingEntities(@Autowired Neo4jTemplate template) { String id; try (Session session = driver.session(bookmarkCapture.createSessionConfig()); Transaction transaction = session.beginTransaction()) { diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/IssuesIT.java b/src/test/java/org/springframework/data/neo4j/integration/issues/IssuesIT.java index 321926198..87991905e 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/IssuesIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/IssuesIT.java @@ -47,10 +47,10 @@ import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.condition.EnabledIf; import org.junit.jupiter.api.extension.ExtendWith; import org.neo4j.cypherdsl.core.Condition; import org.neo4j.cypherdsl.core.Cypher; -import org.neo4j.cypherdsl.core.LabelExpression; import org.neo4j.cypherdsl.core.Node; import org.neo4j.cypherdsl.core.Parameter; import org.neo4j.cypherdsl.core.Property; @@ -276,15 +276,13 @@ protected static void setupData(@Autowired BookmarkCapture bookmarkCapture) { @AfterEach void cleanup(@Autowired BookmarkCapture bookmarkCapture) { List labelsToBeRemoved = List.of("BugFromV1", "BugFrom", "BugTargetV1", "BugTarget", "BugTargetBaseV1", "BugTargetBase", "BugTargetContainer"); - var labelExpression = new LabelExpression(labelsToBeRemoved.get(0)); - for (int i = 1; i < labelsToBeRemoved.size(); i++) { - labelExpression = labelExpression.or(new LabelExpression(labelsToBeRemoved.get(i))); - } try (Session session = neo4jConnectionSupport.getDriver().session(bookmarkCapture.createSessionConfig()); Transaction transaction = session.beginTransaction()) { - Node nodes = Cypher.node(labelExpression); - String cypher = Cypher.match(nodes).detachDelete(nodes).build().getCypher(); - transaction.run(cypher).consume(); + for (var label : labelsToBeRemoved) { + Node nodes = Cypher.node(label); + String cypher = Cypher.match(nodes).detachDelete(nodes).build().getCypher(); + transaction.run(cypher).consume(); + } transaction.commit(); bookmarkCapture.seedWith(session.lastBookmarks()); } @@ -1227,8 +1225,13 @@ void inheritanceAndProjectionShouldMapRelatedNodesCorrectly(@Autowired GH2819Rep } + boolean is5OrLater() { + return neo4jConnectionSupport.isCypher5SyntaxCompatible(); + } + @Test @Tag("GH-2858") + @EnabledIf("is5OrLater") void hydrateProjectionReachableViaMultiplePaths(@Autowired GH2858Repository repository) { GH2858 entity = new GH2858(); entity.name = "rootEntity"; @@ -1773,8 +1776,9 @@ void abstractedRelationshipTypesShouldBeMappedCorrectly(@Autowired Gh2973Reposit assertThat(relationshipsCFail.get(0)).isNotExactlyInstanceOf(BaseRelationship.class); } - @Tag("GH-3036") @Test + @Tag("GH-3036") + @EnabledIf("is5OrLater") void asdf(@Autowired VehicleRepository repository) { var vehicleWithoutDynamicLabels = new Vehicle(); var vehicleWithOneDynamicLabel = new Vehicle(); diff --git a/src/test/java/org/springframework/data/neo4j/integration/issues/ReactiveIssuesIT.java b/src/test/java/org/springframework/data/neo4j/integration/issues/ReactiveIssuesIT.java index aeaa24273..dd3bd9cc6 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/issues/ReactiveIssuesIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/issues/ReactiveIssuesIT.java @@ -23,7 +23,6 @@ import org.assertj.core.data.Percentage; import org.junit.jupiter.api.BeforeEach; import org.neo4j.cypherdsl.core.Cypher; -import org.neo4j.cypherdsl.core.LabelExpression; import org.neo4j.cypherdsl.core.Node; import org.neo4j.driver.Value; import org.neo4j.driver.types.Relationship; @@ -131,17 +130,16 @@ protected static void setupData(@Autowired BookmarkCapture bookmarkCapture) { @BeforeEach void setup(@Autowired BookmarkCapture bookmarkCapture) { - List labelsToBeRemoved = List.of("BugFromV1", "BugFrom", "BugTargetV1", "BugTarget", "BugTargetBaseV1", "BugTargetBase", "BugTargetContainer"); - var labelExpression = new LabelExpression(labelsToBeRemoved.get(0)); - for (int i = 1; i < labelsToBeRemoved.size(); i++) { - labelExpression = labelExpression.or(new LabelExpression(labelsToBeRemoved.get(i))); - } + List labelsToBeRemoved = List.of("BugFromV1", "BugFrom", "BugTargetV1", "BugTarget", "BugTargetBaseV1", + "BugTargetBase", "BugTargetContainer"); try (Session session = neo4jConnectionSupport.getDriver().session(bookmarkCapture.createSessionConfig())) { try (Transaction transaction = session.beginTransaction()) { setupGH2289(transaction); - Node nodes = Cypher.node(labelExpression); - String cypher = Cypher.match(nodes).detachDelete(nodes).build().getCypher(); - transaction.run(cypher).consume(); + for (var label : labelsToBeRemoved) { + Node nodes = Cypher.node(label); + String cypher = Cypher.match(nodes).detachDelete(nodes).build().getCypher(); + transaction.run(cypher).consume(); + } transaction.commit(); } bookmarkCapture.seedWith(session.lastBookmarks()); diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveDynamicLabelsIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveDynamicLabelsIT.java index 5da66ec03..bf35b1a47 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveDynamicLabelsIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveDynamicLabelsIT.java @@ -15,24 +15,6 @@ */ package org.springframework.data.neo4j.integration.reactive; -import static org.assertj.core.api.Assertions.assertThat; - -import org.neo4j.driver.TransactionContext; -import org.junit.jupiter.api.RepeatedTest; -import org.neo4j.driver.reactive.ReactiveSession; -import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; -import org.springframework.data.neo4j.integration.shared.common.CounterMetric; -import org.springframework.data.neo4j.integration.shared.common.GaugeMetric; -import org.springframework.data.neo4j.integration.shared.common.HistogramMetric; -import org.springframework.data.neo4j.integration.shared.common.Metric; -import org.springframework.data.neo4j.integration.shared.common.SummaryMetric; -import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; - -import reactor.adapter.JdkFlowAdapter; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -45,23 +27,35 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.neo4j.cypherdsl.core.Condition; import org.neo4j.cypherdsl.core.Cypher; import org.neo4j.cypherdsl.core.Node; +import org.neo4j.cypherdsl.core.renderer.Dialect; import org.neo4j.cypherdsl.core.renderer.Renderer; import org.neo4j.driver.Driver; import org.neo4j.driver.Record; import org.neo4j.driver.Session; +import org.neo4j.driver.TransactionContext; +import org.neo4j.driver.reactive.ReactiveSession; +import reactor.adapter.JdkFlowAdapter; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider; import org.springframework.data.neo4j.core.ReactiveNeo4jTemplate; +import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager; +import org.springframework.data.neo4j.integration.shared.common.CounterMetric; import org.springframework.data.neo4j.integration.shared.common.EntitiesWithDynamicLabels.DynamicLabelsWithMultipleNodeLabels; import org.springframework.data.neo4j.integration.shared.common.EntitiesWithDynamicLabels.DynamicLabelsWithNodeLabel; import org.springframework.data.neo4j.integration.shared.common.EntitiesWithDynamicLabels.ExtendedBaseClass1; @@ -73,8 +67,13 @@ import org.springframework.data.neo4j.integration.shared.common.EntitiesWithDynamicLabels.SimpleDynamicLabelsWithVersion; import org.springframework.data.neo4j.integration.shared.common.EntitiesWithDynamicLabels.SuperNode; import org.springframework.data.neo4j.integration.shared.common.EntityWithDynamicLabelsAndIdThatNeedsToBeConverted; +import org.springframework.data.neo4j.integration.shared.common.GaugeMetric; +import org.springframework.data.neo4j.integration.shared.common.HistogramMetric; +import org.springframework.data.neo4j.integration.shared.common.Metric; +import org.springframework.data.neo4j.integration.shared.common.SummaryMetric; import org.springframework.data.neo4j.test.BookmarkCapture; import org.springframework.data.neo4j.test.Neo4jExtension; +import org.springframework.data.neo4j.test.Neo4jReactiveTestConfiguration; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -82,29 +81,141 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.reactive.TransactionalOperator; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Michael J. Simons */ @Tag(Neo4jExtension.NEEDS_REACTIVE_SUPPORT) @ExtendWith(Neo4jExtension.class) -public class ReactiveDynamicLabelsIT { +final class ReactiveDynamicLabelsIT { protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + private ReactiveDynamicLabelsIT() { + } + + public static class DialectConfig extends SpringTestBase.Config { + + @Bean + @Primary + public org.neo4j.cypherdsl.core.renderer.Configuration getConfiguration() { + if (neo4jConnectionSupport.isCypher5SyntaxCompatible()) { + return org.neo4j.cypherdsl.core.renderer.Configuration.newConfig().withDialect(Dialect.NEO4J_5_DEFAULT_CYPHER).build(); + } + + return org.neo4j.cypherdsl.core.renderer.Configuration.newConfig().withDialect(Dialect.NEO4J_4).build(); + } + + } + + @ExtendWith(SpringExtension.class) + @ContextConfiguration(classes = DialectConfig.class) + @DirtiesContext + abstract static class SpringTestBase { + + @Autowired + protected Driver driver; + + @Autowired + protected TransactionalOperator transactionalOperator; + + @Autowired + protected BookmarkCapture bookmarkCapture; + + protected Long existingEntityId; + + abstract Long createTestEntity(TransactionContext t); + + @BeforeEach + void setupData() { + try (Session session = this.driver.session();) { + session.executeWrite(tx -> tx.run("MATCH (n) DETACH DELETE n").consume()); + this.existingEntityId = session.executeWrite(this::createTestEntity); + this.bookmarkCapture.seedWith(session.lastBookmarks()); + } + } + + @SuppressWarnings("deprecation") + protected final Flux getLabels(Long id) { + return getLabels(Cypher.anyNode().named("n").internalId().isEqualTo(Cypher.parameter("id")), id); + } + + protected final Flux getLabels(Condition idCondition, Object id) { + + Node n = Cypher.anyNode("n"); + String cypher = Renderer.getDefaultRenderer() + .render(Cypher.match(n) + .where(idCondition) + .and(n.property("moreLabels").isNull()) + .unwind(n.labels()) + .as("label") + .returning("label") + .build()); + return Flux.usingWhen(Mono.fromSupplier( + () -> this.driver.session(ReactiveSession.class, this.bookmarkCapture.createSessionConfig())), + s -> JdkFlowAdapter.flowPublisherToFlux(s.run(cypher, Collections.singletonMap("id", id))) + .flatMap(r -> JdkFlowAdapter.flowPublisherToFlux(r.records())), + rs -> JdkFlowAdapter.flowPublisherToFlux(rs.close())) + .map(r -> r.get("label").asString()); + } + + @Configuration + @EnableTransactionManagement + static class Config extends Neo4jReactiveTestConfiguration { + + @Bean + @Override + public Driver driver() { + return neo4jConnectionSupport.getDriver(); + } + + @Bean + BookmarkCapture bookmarkCapture() { + return new BookmarkCapture(); + } + + @Override + public ReactiveTransactionManager reactiveTransactionManager(Driver driver, + ReactiveDatabaseSelectionProvider databaseSelectionProvider) { + + BookmarkCapture bookmarkCapture = bookmarkCapture(); + return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, + Neo4jBookmarkManager.createReactive(bookmarkCapture)); + } + + @Bean + TransactionalOperator transactionalOperator(ReactiveTransactionManager transactionManager) { + return TransactionalOperator.create(transactionManager); + } + + @Override + public boolean isCypher5Compatible() { + return neo4jConnectionSupport.isCypher5SyntaxCompatible(); + } + + } + + } @Nested class DynamicLabelsAndOrderOfClassesBeingLoaded extends SpringTestBase { @Override Long createTestEntity(TransactionContext t) { - return t.run("CREATE (m:Metric:Counter:A:B:C:D {timestamp: datetime()}) RETURN id(m)").single().get(0).asLong(); + return t.run("CREATE (m:Metric:Counter:A:B:C:D {timestamp: datetime()}) RETURN id(m)") + .single() + .get(0) + .asLong(); } @RepeatedTest(100) // GH-2619 - void ownLabelsShouldNotEndUpWithDynamicLabels(@Autowired Neo4jMappingContext mappingContext, @Autowired ReactiveNeo4jTemplate template) { + void ownLabelsShouldNotEndUpWithDynamicLabels(@Autowired Neo4jMappingContext mappingContext, + @Autowired ReactiveNeo4jTemplate template) { - List> metrics = Arrays.asList(GaugeMetric.class, SummaryMetric.class, HistogramMetric.class, CounterMetric.class); + List> metrics = Arrays.asList(GaugeMetric.class, SummaryMetric.class, + HistogramMetric.class, CounterMetric.class); Collections.shuffle(metrics); for (Class type : metrics) { assertThat(mappingContext.getPersistentEntity(type)).isNotNull(); @@ -112,15 +223,18 @@ void ownLabelsShouldNotEndUpWithDynamicLabels(@Autowired Neo4jMappingContext map Map args = new HashMap<>(); args.put("agentIdLabel", "B"); - template.findAll("MATCH (m:Metric) WHERE $agentIdLabel in labels(m) RETURN m ORDER BY m.timestamp DESC", args, Metric.class) - .as(StepVerifier::create) - .assertNext(cm -> { - assertThat(cm).isInstanceOf(CounterMetric.class); - assertThat(cm.getId()).isEqualTo(existingEntityId); - assertThat(cm.getDynamicLabels()).containsExactlyInAnyOrder("A", "B", "C", "D"); - }) - .verifyComplete(); + template + .findAll("MATCH (m:Metric) WHERE $agentIdLabel in labels(m) RETURN m ORDER BY m.timestamp DESC", args, + Metric.class) + .as(StepVerifier::create) + .assertNext(cm -> { + assertThat(cm).isInstanceOf(CounterMetric.class); + assertThat(cm.getId()).isEqualTo(this.existingEntityId); + assertThat(cm.getDynamicLabels()).containsExactlyInAnyOrder("A", "B", "C", "D"); + }) + .verifyComplete(); } + } @Nested @@ -129,28 +243,36 @@ class EntityWithSingleStaticLabelAndGeneratedId extends SpringTestBase { @Override Long createTestEntity(TransactionContext transaction) { Record r = transaction - .run("CREATE (e:SimpleDynamicLabels:Foo:Bar:Baz:Foobar) RETURN id(e) as existingEntityId").single(); + .run("CREATE (e:SimpleDynamicLabels:Foo:Bar:Baz:Foobar) RETURN id(e) as existingEntityId") + .single(); return r.get("existingEntityId").asLong(); } @Test void shouldReadDynamicLabels(@Autowired ReactiveNeo4jTemplate template) { - template.findById(existingEntityId, SimpleDynamicLabels.class) - .flatMapMany(entity -> Flux.fromIterable(entity.moreLabels)).sort().as(StepVerifier::create) - .expectNext("Bar", "Baz", "Foo", "Foobar").verifyComplete(); + template.findById(this.existingEntityId, SimpleDynamicLabels.class) + .flatMapMany(entity -> Flux.fromIterable(entity.moreLabels)) + .sort() + .as(StepVerifier::create) + .expectNext("Bar", "Baz", "Foo", "Foobar") + .verifyComplete(); } @Test void shouldUpdateDynamicLabels(@Autowired ReactiveNeo4jTemplate template) { - template.findById(existingEntityId, SimpleDynamicLabels.class).flatMap(entity -> { + template.findById(this.existingEntityId, SimpleDynamicLabels.class).flatMap(entity -> { entity.moreLabels.remove("Foo"); entity.moreLabels.add("Fizz"); return template.save(entity); - }).as(transactionalOperator::transactional) - .thenMany(getLabels(existingEntityId)).sort().as(StepVerifier::create) - .expectNext("Bar", "Baz", "Fizz", "Foobar", "SimpleDynamicLabels").verifyComplete(); + }) + .as(this.transactionalOperator::transactional) + .thenMany(getLabels(this.existingEntityId)) + .sort() + .as(StepVerifier::create) + .expectNext("Bar", "Baz", "Fizz", "Foobar", "SimpleDynamicLabels") + .verifyComplete(); } @Test @@ -162,10 +284,14 @@ void shouldWriteDynamicLabels(@Autowired ReactiveNeo4jTemplate template) { entity.moreLabels.add("B"); entity.moreLabels.add("C"); - template.save(entity).map(SimpleDynamicLabels::getId) - .as(transactionalOperator::transactional) - .flatMapMany(this::getLabels).sort().as(StepVerifier::create) - .expectNext("A", "B", "C", "SimpleDynamicLabels").verifyComplete(); + template.save(entity) + .map(SimpleDynamicLabels::getId) + .as(this.transactionalOperator::transactional) + .flatMapMany(this::getLabels) + .sort() + .as(StepVerifier::create) + .expectNext("A", "B", "C", "SimpleDynamicLabels") + .verifyComplete(); } @Test @@ -179,11 +305,17 @@ void shouldWriteDynamicLabelsFromRelatedNodes(@Autowired ReactiveNeo4jTemplate t SuperNode superNode = new SuperNode(); superNode.relatedTo = entity; - template.save(superNode).map(SuperNode::getRelatedTo).map(SimpleDynamicLabels::getId) - .as(transactionalOperator::transactional) - .flatMapMany(this::getLabels) - .sort().as(StepVerifier::create).expectNext("A", "B", "C", "SimpleDynamicLabels").verifyComplete(); + template.save(superNode) + .map(SuperNode::getRelatedTo) + .map(SimpleDynamicLabels::getId) + .as(this.transactionalOperator::transactional) + .flatMapMany(this::getLabels) + .sort() + .as(StepVerifier::create) + .expectNext("A", "B", "C", "SimpleDynamicLabels") + .verifyComplete(); } + } @Nested @@ -191,30 +323,37 @@ class EntityWithInheritedDynamicLabels extends SpringTestBase { @Override Long createTestEntity(TransactionContext transaction) { - Record r = transaction - .run("CREATE (e:SimpleDynamicLabels:InheritedSimpleDynamicLabels:Foo:Bar:Baz:Foobar) RETURN id(e) as existingEntityId") - .single(); + Record r = transaction.run( + "CREATE (e:SimpleDynamicLabels:InheritedSimpleDynamicLabels:Foo:Bar:Baz:Foobar) RETURN id(e) as existingEntityId") + .single(); return r.get("existingEntityId").asLong(); } @Test void shouldReadDynamicLabels(@Autowired ReactiveNeo4jTemplate template) { - template.findById(existingEntityId, InheritedSimpleDynamicLabels.class) - .flatMapMany(entity -> Flux.fromIterable(entity.moreLabels)).sort().as(StepVerifier::create) - .expectNext("Bar", "Baz", "Foo", "Foobar").verifyComplete(); + template.findById(this.existingEntityId, InheritedSimpleDynamicLabels.class) + .flatMapMany(entity -> Flux.fromIterable(entity.moreLabels)) + .sort() + .as(StepVerifier::create) + .expectNext("Bar", "Baz", "Foo", "Foobar") + .verifyComplete(); } @Test void shouldUpdateDynamicLabels(@Autowired ReactiveNeo4jTemplate template) { - template.findById(existingEntityId, InheritedSimpleDynamicLabels.class).flatMap(entity -> { + template.findById(this.existingEntityId, InheritedSimpleDynamicLabels.class).flatMap(entity -> { entity.moreLabels.remove("Foo"); entity.moreLabels.add("Fizz"); return template.save(entity); - }).as(transactionalOperator::transactional) - .thenMany(getLabels(existingEntityId)).sort().as(StepVerifier::create) - .expectNext("Bar", "Baz", "Fizz", "Foobar", "InheritedSimpleDynamicLabels", "SimpleDynamicLabels").verifyComplete(); + }) + .as(this.transactionalOperator::transactional) + .thenMany(getLabels(this.existingEntityId)) + .sort() + .as(StepVerifier::create) + .expectNext("Bar", "Baz", "Fizz", "Foobar", "InheritedSimpleDynamicLabels", "SimpleDynamicLabels") + .verifyComplete(); } @Test @@ -226,11 +365,16 @@ void shouldWriteDynamicLabels(@Autowired ReactiveNeo4jTemplate template) { entity.moreLabels.add("B"); entity.moreLabels.add("C"); - template.save(entity).map(SimpleDynamicLabels::getId) - .as(transactionalOperator::transactional) - .flatMapMany(this::getLabels).sort().as(StepVerifier::create) - .expectNext("A", "B", "C", "InheritedSimpleDynamicLabels", "SimpleDynamicLabels").verifyComplete(); + template.save(entity) + .map(SimpleDynamicLabels::getId) + .as(this.transactionalOperator::transactional) + .flatMapMany(this::getLabels) + .sort() + .as(StepVerifier::create) + .expectNext("A", "B", "C", "InheritedSimpleDynamicLabels", "SimpleDynamicLabels") + .verifyComplete(); } + } @Nested @@ -239,9 +383,9 @@ class EntityWithSingleStaticLabelAndAssignedId extends SpringTestBase { @Override Long createTestEntity(TransactionContext transaction) { Record r = transaction.run(""" - CREATE (e:SimpleDynamicLabelsWithBusinessId:Foo:Bar:Baz:Foobar {id: 'E1'}) - RETURN id(e) as existingEntityId - """).single(); + CREATE (e:SimpleDynamicLabelsWithBusinessId:Foo:Bar:Baz:Foobar {id: 'E1'}) + RETURN id(e) as existingEntityId + """).single(); return r.get("existingEntityId").asLong(); } @@ -252,9 +396,13 @@ void shouldUpdateDynamicLabels(@Autowired ReactiveNeo4jTemplate template) { entity.moreLabels.remove("Foo"); entity.moreLabels.add("Fizz"); return template.save(entity); - }).as(transactionalOperator::transactional) - .thenMany(getLabels(existingEntityId)).sort().as(StepVerifier::create) - .expectNext("Bar", "Baz", "Fizz", "Foobar", "SimpleDynamicLabelsWithBusinessId").verifyComplete(); + }) + .as(this.transactionalOperator::transactional) + .thenMany(getLabels(this.existingEntityId)) + .sort() + .as(StepVerifier::create) + .expectNext("Bar", "Baz", "Fizz", "Foobar", "SimpleDynamicLabelsWithBusinessId") + .verifyComplete(); } @Test @@ -267,11 +415,16 @@ void shouldWriteDynamicLabels(@Autowired ReactiveNeo4jTemplate template) { entity.moreLabels.add("B"); entity.moreLabels.add("C"); - template.save(entity).map(SimpleDynamicLabelsWithBusinessId::getId) - .as(transactionalOperator::transactional) - .flatMapMany(id -> getLabels(Cypher.anyNode("n").property("id").isEqualTo(Cypher.parameter("id")), id)).sort() - .as(StepVerifier::create).expectNext("A", "B", "C", "SimpleDynamicLabelsWithBusinessId").verifyComplete(); + template.save(entity) + .map(SimpleDynamicLabelsWithBusinessId::getId) + .as(this.transactionalOperator::transactional) + .flatMapMany(id -> getLabels(Cypher.anyNode("n").property("id").isEqualTo(Cypher.parameter("id")), id)) + .sort() + .as(StepVerifier::create) + .expectNext("A", "B", "C", "SimpleDynamicLabelsWithBusinessId") + .verifyComplete(); } + } @Nested @@ -280,22 +433,26 @@ class EntityWithSingleStaticLabelGeneratedIdAndVersion extends SpringTestBase { @Override Long createTestEntity(TransactionContext transaction) { Record r = transaction.run( - "CREATE (e:SimpleDynamicLabelsWithVersion:Foo:Bar:Baz:Foobar {myVersion: 0}) RETURN id(e) as existingEntityId").single(); + "CREATE (e:SimpleDynamicLabelsWithVersion:Foo:Bar:Baz:Foobar {myVersion: 0}) RETURN id(e) as existingEntityId") + .single(); return r.get("existingEntityId").asLong(); } @Test void shouldUpdateDynamicLabels(@Autowired ReactiveNeo4jTemplate template) { - template.findById(existingEntityId, SimpleDynamicLabelsWithVersion.class).flatMap(entity -> { + template.findById(this.existingEntityId, SimpleDynamicLabelsWithVersion.class).flatMap(entity -> { entity.moreLabels.remove("Foo"); entity.moreLabels.add("Fizz"); return template.save(entity); - }).doOnNext(e -> assertThat(e.myVersion).isNotNull().isEqualTo(1)) - .as(transactionalOperator::transactional) - .thenMany(getLabels(existingEntityId)).sort() - .as(StepVerifier::create).expectNext("Bar", "Baz", "Fizz", "Foobar", "SimpleDynamicLabelsWithVersion") - .verifyComplete(); + }) + .doOnNext(e -> assertThat(e.myVersion).isNotNull().isEqualTo(1)) + .as(this.transactionalOperator::transactional) + .thenMany(getLabels(this.existingEntityId)) + .sort() + .as(StepVerifier::create) + .expectNext("Bar", "Baz", "Fizz", "Foobar", "SimpleDynamicLabelsWithVersion") + .verifyComplete(); } @Test @@ -307,12 +464,17 @@ void shouldWriteDynamicLabels(@Autowired ReactiveNeo4jTemplate template) { entity.moreLabels.add("B"); entity.moreLabels.add("C"); - template.save(entity).doOnNext(e -> assertThat(e.myVersion).isNotNull().isEqualTo(0)) - .map(SimpleDynamicLabelsWithVersion::getId) - .as(transactionalOperator::transactional) - .flatMapMany(this::getLabels).sort().as(StepVerifier::create) - .expectNext("A", "B", "C", "SimpleDynamicLabelsWithVersion").verifyComplete(); + template.save(entity) + .doOnNext(e -> assertThat(e.myVersion).isNotNull().isEqualTo(0)) + .map(SimpleDynamicLabelsWithVersion::getId) + .as(this.transactionalOperator::transactional) + .flatMapMany(this::getLabels) + .sort() + .as(StepVerifier::create) + .expectNext("A", "B", "C", "SimpleDynamicLabelsWithVersion") + .verifyComplete(); } + } @Nested @@ -320,7 +482,9 @@ class EntityWithSingleStaticLabelAssignedIdAndVersion extends SpringTestBase { @Override Long createTestEntity(TransactionContext transaction) { - Record r = transaction.run("CREATE (e:SimpleDynamicLabelsWithBusinessIdAndVersion:Foo:Bar:Baz:Foobar {id: 'E2', myVersion: 0}) RETURN id(e) as existingEntityId").single(); + Record r = transaction.run( + "CREATE (e:SimpleDynamicLabelsWithBusinessIdAndVersion:Foo:Bar:Baz:Foobar {id: 'E2', myVersion: 0}) RETURN id(e) as existingEntityId") + .single(); return r.get("existingEntityId").asLong(); } @@ -331,12 +495,15 @@ void shouldUpdateDynamicLabels(@Autowired ReactiveNeo4jTemplate template) { entity.moreLabels.remove("Foo"); entity.moreLabels.add("Fizz"); return template.save(entity); - }).doOnNext(e -> assertThat(e.myVersion).isNotNull().isEqualTo(1)) - .map(SimpleDynamicLabelsWithBusinessIdAndVersion::getId) - .as(transactionalOperator::transactional) - .flatMapMany(id -> getLabels(Cypher.anyNode("n").property("id").isEqualTo(Cypher.parameter("id")), id)).sort() - .as(StepVerifier::create) - .expectNext("Bar", "Baz", "Fizz", "Foobar", "SimpleDynamicLabelsWithBusinessIdAndVersion").verifyComplete(); + }) + .doOnNext(e -> assertThat(e.myVersion).isNotNull().isEqualTo(1)) + .map(SimpleDynamicLabelsWithBusinessIdAndVersion::getId) + .as(this.transactionalOperator::transactional) + .flatMapMany(id -> getLabels(Cypher.anyNode("n").property("id").isEqualTo(Cypher.parameter("id")), id)) + .sort() + .as(StepVerifier::create) + .expectNext("Bar", "Baz", "Fizz", "Foobar", "SimpleDynamicLabelsWithBusinessIdAndVersion") + .verifyComplete(); } @Test @@ -349,13 +516,17 @@ void shouldWriteDynamicLabels(@Autowired ReactiveNeo4jTemplate template) { entity.moreLabels.add("B"); entity.moreLabels.add("C"); - template.save(entity).doOnNext(e -> assertThat(e.myVersion).isNotNull().isEqualTo(0)) - .map(SimpleDynamicLabelsWithBusinessIdAndVersion::getId) - .as(transactionalOperator::transactional) - .flatMapMany(id -> getLabels(Cypher.anyNode("n").property("id").isEqualTo(Cypher.parameter("id")), id)).sort() - .as(StepVerifier::create).expectNext("A", "B", "C", "SimpleDynamicLabelsWithBusinessIdAndVersion") - .verifyComplete(); + template.save(entity) + .doOnNext(e -> assertThat(e.myVersion).isNotNull().isEqualTo(0)) + .map(SimpleDynamicLabelsWithBusinessIdAndVersion::getId) + .as(this.transactionalOperator::transactional) + .flatMapMany(id -> getLabels(Cypher.anyNode("n").property("id").isEqualTo(Cypher.parameter("id")), id)) + .sort() + .as(StepVerifier::create) + .expectNext("A", "B", "C", "SimpleDynamicLabelsWithBusinessIdAndVersion") + .verifyComplete(); } + } @Nested @@ -364,18 +535,21 @@ class ConstructorInitializedEntity extends SpringTestBase { @Override Long createTestEntity(TransactionContext transaction) { Record r = transaction - .run("CREATE (e:SimpleDynamicLabelsCtor:Foo:Bar:Baz:Foobar) RETURN id(e) as existingEntityId") - .single(); + .run("CREATE (e:SimpleDynamicLabelsCtor:Foo:Bar:Baz:Foobar) RETURN id(e) as existingEntityId") + .single(); return r.get("existingEntityId").asLong(); } @Test void shouldReadDynamicLabels(@Autowired ReactiveNeo4jTemplate template) { - template.findById(existingEntityId, SimpleDynamicLabelsCtor.class) - .flatMapMany(entity -> Flux.fromIterable(entity.moreLabels)).sort().as(StepVerifier::create) - .expectNext("Bar", "Baz", "Foo", "Foobar"); + template.findById(this.existingEntityId, SimpleDynamicLabelsCtor.class) + .flatMapMany(entity -> Flux.fromIterable(entity.moreLabels)) + .sort() + .as(StepVerifier::create) + .expectNext("Bar", "Baz", "Foo", "Foobar"); } + } @Nested @@ -383,48 +557,55 @@ class ClassesWithAdditionalLabels extends SpringTestBase { @Override Long createTestEntity(TransactionContext transaction) { - Record r = transaction.run("CREATE (e:SimpleDynamicLabels:Foo:Bar:Baz:Foobar) RETURN id(e) as existingEntityId").single(); + Record r = transaction + .run("CREATE (e:SimpleDynamicLabels:Foo:Bar:Baz:Foobar) RETURN id(e) as existingEntityId") + .single(); return r.get("existingEntityId").asLong(); } @Test void shouldReadDynamicLabelsOnClassWithSingleNodeLabel(@Autowired ReactiveNeo4jTemplate template) { - template.findById(existingEntityId, DynamicLabelsWithNodeLabel.class) - .flatMapMany(entity -> Flux.fromIterable(entity.moreLabels)).sort().as(StepVerifier::create) - .expectNext("Bar", "Foo", "Foobar", "SimpleDynamicLabels") - .verifyComplete(); + template.findById(this.existingEntityId, DynamicLabelsWithNodeLabel.class) + .flatMapMany(entity -> Flux.fromIterable(entity.moreLabels)) + .sort() + .as(StepVerifier::create) + .expectNext("Bar", "Foo", "Foobar", "SimpleDynamicLabels") + .verifyComplete(); } @Test void shouldReadDynamicLabelsOnClassWithMultipleNodeLabel(@Autowired ReactiveNeo4jTemplate template) { - template.findById(existingEntityId, DynamicLabelsWithMultipleNodeLabels.class) - .flatMapMany(entity -> Flux.fromIterable(entity.moreLabels)).sort().as(StepVerifier::create) - .expectNext("Baz", "Foobar", "SimpleDynamicLabels") - .verifyComplete(); + template.findById(this.existingEntityId, DynamicLabelsWithMultipleNodeLabels.class) + .flatMapMany(entity -> Flux.fromIterable(entity.moreLabels)) + .sort() + .as(StepVerifier::create) + .expectNext("Baz", "Foobar", "SimpleDynamicLabels") + .verifyComplete(); } @Test // GH-2296 void shouldConvertIds(@Autowired ReactiveNeo4jTemplate template) { String label = "value_1"; - Predicate expectatations = savedInstance -> - label.equals(savedInstance.getValue()) && savedInstance.getExtraLabels().contains(label); + Predicate expectatations = savedInstance -> label + .equals(savedInstance.getValue()) && savedInstance.getExtraLabels().contains(label); AtomicReference generatedUUID = new AtomicReference<>(); template.deleteAll(EntityWithDynamicLabelsAndIdThatNeedsToBeConverted.class) - .then(template.save(new EntityWithDynamicLabelsAndIdThatNeedsToBeConverted(label))) - .doOnNext(s -> generatedUUID.set(s.getId())) - .as(StepVerifier::create) - .expectNextMatches(expectatations) - .verifyComplete(); + .then(template.save(new EntityWithDynamicLabelsAndIdThatNeedsToBeConverted(label))) + .doOnNext(s -> generatedUUID.set(s.getId())) + .as(StepVerifier::create) + .expectNextMatches(expectatations) + .verifyComplete(); template.findById(generatedUUID.get(), EntityWithDynamicLabelsAndIdThatNeedsToBeConverted.class) - .as(StepVerifier::create) - .expectNextMatches(expectatations) - .verifyComplete(); + .as(StepVerifier::create) + .expectNextMatches(expectatations) + .verifyComplete(); } + } @Nested @@ -432,91 +613,23 @@ class ClassesWithAdditionalLabelsInInheritanceTree extends SpringTestBase { @Override Long createTestEntity(TransactionContext transaction) { - Record r = transaction.run("CREATE (e:DynamicLabelsBaseClass:ExtendedBaseClass1:D1:D2:D3) RETURN id(e) as existingEntityId").single(); + Record r = transaction + .run("CREATE (e:DynamicLabelsBaseClass:ExtendedBaseClass1:D1:D2:D3) RETURN id(e) as existingEntityId") + .single(); return r.get("existingEntityId").asLong(); } @Test void shouldReadDynamicLabelsInInheritance(@Autowired ReactiveNeo4jTemplate template) { - template.findById(existingEntityId, ExtendedBaseClass1.class) - .flatMapMany(entity -> Flux.fromIterable(entity.moreLabels)).sort().as(StepVerifier::create) - .expectNext("D1", "D2", "D3") - .verifyComplete(); + template.findById(this.existingEntityId, ExtendedBaseClass1.class) + .flatMapMany(entity -> Flux.fromIterable(entity.moreLabels)) + .sort() + .as(StepVerifier::create) + .expectNext("D1", "D2", "D3") + .verifyComplete(); } - } - @ExtendWith(SpringExtension.class) - @ContextConfiguration(classes = SpringTestBase.Config.class) - @DirtiesContext - abstract static class SpringTestBase { - - @Autowired protected Driver driver; - - @Autowired protected TransactionalOperator transactionalOperator; - - @Autowired protected BookmarkCapture bookmarkCapture; - - protected Long existingEntityId; - - abstract Long createTestEntity(TransactionContext t); - - @BeforeEach - void setupData() { - try (Session session = driver.session();) { - session.executeWrite(tx -> tx.run("MATCH (n) DETACH DELETE n").consume()); - existingEntityId = session.executeWrite(this::createTestEntity); - bookmarkCapture.seedWith(session.lastBookmarks()); - } - } - - @SuppressWarnings("deprecation") - protected final Flux getLabels(Long id) { - return getLabels(Cypher.anyNode().named("n").internalId().isEqualTo(Cypher.parameter("id")), id); - } - - protected final Flux getLabels(Condition idCondition, Object id) { - - Node n = Cypher.anyNode("n"); - String cypher = Renderer.getDefaultRenderer().render(Cypher.match(n).where(idCondition) - .and(n.property("moreLabels").isNull()).unwind(n.labels()).as("label").returning("label").build()); - return Flux - .usingWhen(Mono.fromSupplier(() -> driver.session(ReactiveSession.class, bookmarkCapture.createSessionConfig())), - s -> JdkFlowAdapter.flowPublisherToFlux(s.run(cypher, Collections.singletonMap("id", id))).flatMap(r -> JdkFlowAdapter.flowPublisherToFlux(r.records())), rs -> JdkFlowAdapter.flowPublisherToFlux(rs.close())) - .map(r -> r.get("label").asString()); - } - - @Configuration - @EnableTransactionManagement - static class Config extends Neo4jReactiveTestConfiguration { - - @Bean - public Driver driver() { - return neo4jConnectionSupport.getDriver(); - } - - @Bean - public BookmarkCapture bookmarkCapture() { - return new BookmarkCapture(); - } - - @Override - public ReactiveTransactionManager reactiveTransactionManager(Driver driver, ReactiveDatabaseSelectionProvider databaseSelectionProvider) { - - BookmarkCapture bookmarkCapture = bookmarkCapture(); - return new ReactiveNeo4jTransactionManager(driver, databaseSelectionProvider, Neo4jBookmarkManager.createReactive(bookmarkCapture)); - } - - @Bean - public TransactionalOperator transactionalOperator(ReactiveTransactionManager transactionManager) { - return TransactionalOperator.create(transactionManager); - } - - @Override - public boolean isCypher5Compatible() { - return neo4jConnectionSupport.isCypher5SyntaxCompatible(); - } - } } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveNeo4jClientIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveNeo4jClientIT.java index 906cb2f3e..3d1f1f8ae 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveNeo4jClientIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveNeo4jClientIT.java @@ -15,17 +15,10 @@ */ package org.springframework.data.neo4j.integration.reactive; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Collection; import java.util.Collections; -import java.util.List; import java.util.Map; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicLong; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.stream.Stream; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -33,20 +26,16 @@ import org.junit.jupiter.api.Test; import org.neo4j.cypherdsl.core.Cypher; import org.neo4j.cypherdsl.core.Node; -import org.neo4j.cypherdsl.core.Statement; -import org.neo4j.cypherdsl.core.executables.ExecutableResultStatement; import org.neo4j.driver.Driver; -import org.neo4j.driver.Query; -import org.neo4j.driver.Record; import org.neo4j.driver.Session; -import org.neo4j.driver.SimpleQueryRunner; import org.neo4j.driver.Transaction; -import org.neo4j.driver.async.AsyncQueryRunner; -import org.neo4j.driver.reactivestreams.ReactiveQueryRunner; import org.neo4j.driver.reactivestreams.ReactiveResult; import org.neo4j.driver.reactivestreams.ReactiveSession; -import org.neo4j.driver.summary.ResultSummary; -import org.reactivestreams.Publisher; +import reactor.blockhound.BlockHound; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -64,10 +53,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.reactive.TransactionalOperator; -import reactor.blockhound.BlockHound; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; +import static org.assertj.core.api.Assertions.assertThat; /** * @author Michael J. Simons @@ -153,12 +139,13 @@ void clientShouldIntegrateWithCypherDSL(@Autowired TransactionalOperator transac Node namedAnswer = Cypher.node("TheAnswer", Cypher.mapOf("value", Cypher.literalOf(23).multiply(Cypher.literalOf(2)).subtract(Cypher.literalOf(4)))).named("n"); - NewReactiveExecutableResultStatement statement = new NewReactiveExecutableResultStatement(namedAnswer); + var cypher = Cypher.create(namedAnswer).returning(namedAnswer).build().getCypher(); AtomicLong vanishedId = new AtomicLong(); transactionalOperator.execute(transaction -> { - Flux inner = client.getQueryRunner() - .flatMapMany(statement::fetchWith) + var inner = client.getQueryRunner() + .flatMap(qr -> Mono.from(qr.run(cypher))) + .flatMapMany(r -> Flux.from(r.records())) .doOnNext(r -> vanishedId.set(TestIdentitySupport.getInternalId(r.get("n").asNode()))) .map(record -> record.get("n").get("value").asLong()); @@ -214,69 +201,4 @@ public boolean isCypher5Compatible() { return neo4jConnectionSupport.isCypher5SyntaxCompatible(); } } - - private static class NewReactiveExecutableResultStatement implements ExecutableResultStatement { - - private final Statement delegate; - - NewReactiveExecutableResultStatement(Node namedAnswer) { - delegate = Cypher.create(namedAnswer) - .returning(namedAnswer) - .build(); - } - - /** - * This method should move into a future Cypher-DSL version. - * @param reactiveQueryRunner The runner to run the statement with - * @return a publisher of records - */ - public Publisher fetchWith(ReactiveQueryRunner reactiveQueryRunner) { - return Mono.fromCallable(this::createQuery).flatMapMany(reactiveQueryRunner::run) - .flatMap(ReactiveResult::records); - } - - @Override - public List fetchWith(SimpleQueryRunner queryRunner, Function function) { - throw new UnsupportedOperationException(); - } - - @Override public CompletableFuture> fetchWith(AsyncQueryRunner asyncQueryRunner, - Function function) { - throw new UnsupportedOperationException(); - } - - @Override - public ResultSummary streamWith(SimpleQueryRunner queryRunner, Consumer> consumer) { - throw new UnsupportedOperationException(); - } - - @Override - public ResultSummary executeWith(SimpleQueryRunner queryRunner) { - throw new UnsupportedOperationException(); - } - - @Override - public CompletableFuture executeWith(AsyncQueryRunner queryRunner) { - throw new UnsupportedOperationException(); - } - - Query createQuery() { - return new Query(this.delegate.getCypher(), this.delegate.getCatalog().getParameters()); - } - - @Override - public Map getParameters() { - return this.delegate.getCatalog().getParameters(); - } - - @Override - public Collection getParameterNames() { - return this.delegate.getCatalog().getParameterNames(); - } - - @Override - public String getCypher() { - return this.delegate.getCypher(); - } - } } diff --git a/src/test/java/org/springframework/data/neo4j/test/Neo4jExtension.java b/src/test/java/org/springframework/data/neo4j/test/Neo4jExtension.java index dd1892b37..d625b771d 100644 --- a/src/test/java/org/springframework/data/neo4j/test/Neo4jExtension.java +++ b/src/test/java/org/springframework/data/neo4j/test/Neo4jExtension.java @@ -67,6 +67,7 @@ public class Neo4jExtension implements BeforeAllCallback, BeforeEachCallback { public final static String NEEDS_REACTIVE_SUPPORT = "reactive-test"; public final static String NEEDS_VERSION_SUPPORTING_ELEMENT_ID = "elementid-test"; + public static final String NEEDS_VECTOR_INDEX = "vectorindex-test"; public final static String COMMUNITY_EDITION_ONLY = "community-edition"; public final static String COMMERCIAL_EDITION_ONLY = "commercial-edition"; /** @@ -162,6 +163,13 @@ private void checkRequiredFeatures(Neo4jConnectionSupport neo4jConnectionSupport .describedAs("This test should be run on the commercial edition only").isTrue(); } + if (tags.contains(NEEDS_VECTOR_INDEX)) { + assumeThat( + neo4jConnectionSupport.getServerVersion().greaterThanOrEqual(ServerVersion.version("Neo4j/5.13.0"))) + .describedAs("This tests needs a Neo4j version supporting Vector indexes") + .isTrue(); + } + tags.stream().filter(s -> s.startsWith(REQUIRES)).map(ServerVersion::version).forEach(v -> { assumeThat(neo4jConnectionSupport.getServerVersion().greaterThanOrEqual(v)) .describedAs("This test requires at least " + v.toString()).isTrue(); diff --git a/src/test/java/org/springframework/data/neo4j/test/Neo4jTestConfiguration.java b/src/test/java/org/springframework/data/neo4j/test/Neo4jTestConfiguration.java index 09504885a..b3713b694 100644 --- a/src/test/java/org/springframework/data/neo4j/test/Neo4jTestConfiguration.java +++ b/src/test/java/org/springframework/data/neo4j/test/Neo4jTestConfiguration.java @@ -27,9 +27,9 @@ public interface Neo4jTestConfiguration { default Configuration getConfiguration() { if (isCypher5Compatible()) { - return Configuration.newConfig().withDialect(Dialect.NEO4J_5).build(); + return Configuration.newConfig().withDialect(Dialect.NEO4J_5_DEFAULT_CYPHER).build(); } - return Configuration.defaultConfig(); + return Configuration.newConfig().withDialect(Dialect.NEO4J_4).build(); } } diff --git a/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/KotlinInheritanceIT.kt b/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/KotlinInheritanceIT.kt index 463398b10..6f640cc3a 100644 --- a/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/KotlinInheritanceIT.kt +++ b/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/KotlinInheritanceIT.kt @@ -19,10 +19,12 @@ package org.springframework.data.neo4j.integration.imperative import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test +import org.neo4j.cypherdsl.core.renderer.Dialect import org.neo4j.driver.Driver import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Primary import org.springframework.data.neo4j.config.AbstractNeo4jConfig import org.springframework.data.neo4j.core.DatabaseSelectionProvider import org.springframework.data.neo4j.core.Neo4jTemplate @@ -243,5 +245,16 @@ class KotlinInheritanceIT @Autowired constructor( open fun transactionTemplate(transactionManager: PlatformTransactionManager): TransactionTemplate { return TransactionTemplate(transactionManager) } + + @Bean + @Primary + open fun getConfiguration(): org.neo4j.cypherdsl.core.renderer.Configuration? { + if (neo4jConnectionSupport.isCypher5SyntaxCompatible) { + return org.neo4j.cypherdsl.core.renderer.Configuration.newConfig() + .withDialect(Dialect.NEO4J_5_DEFAULT_CYPHER).build() + } + + return org.neo4j.cypherdsl.core.renderer.Configuration.newConfig().withDialect(Dialect.NEO4J_4).build() + } } } diff --git a/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/KotlinProjectionIT.kt b/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/KotlinProjectionIT.kt index 684dfea16..309941369 100644 --- a/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/KotlinProjectionIT.kt +++ b/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/KotlinProjectionIT.kt @@ -19,10 +19,12 @@ package org.springframework.data.neo4j.integration.imperative import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test +import org.neo4j.cypherdsl.core.renderer.Dialect import org.neo4j.driver.Driver import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Primary import org.springframework.data.neo4j.config.AbstractNeo4jConfig import org.springframework.data.neo4j.core.DatabaseSelectionProvider import org.springframework.data.neo4j.core.Neo4jTemplate @@ -135,5 +137,16 @@ internal class KotlinProjectionIT { override fun driver(): Driver { return neo4jConnectionSupport.driver } + + @Bean + @Primary + open fun getConfiguration(): org.neo4j.cypherdsl.core.renderer.Configuration? { + if (neo4jConnectionSupport.isCypher5SyntaxCompatible) { + return org.neo4j.cypherdsl.core.renderer.Configuration.newConfig() + .withDialect(Dialect.NEO4J_5_DEFAULT_CYPHER).build() + } + + return org.neo4j.cypherdsl.core.renderer.Configuration.newConfig().withDialect(Dialect.NEO4J_4).build() + } } } diff --git a/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/Neo4jClientKotlinInteropIT.kt b/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/Neo4jClientKotlinInteropIT.kt index 62e3f575a..2c3ebf949 100644 --- a/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/Neo4jClientKotlinInteropIT.kt +++ b/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/Neo4jClientKotlinInteropIT.kt @@ -20,11 +20,13 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.neo4j.cypherdsl.core.renderer.Dialect import org.neo4j.driver.Driver import org.neo4j.driver.Values import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Primary import org.springframework.data.neo4j.config.AbstractNeo4jConfig import org.springframework.data.neo4j.core.Neo4jClient import org.springframework.data.neo4j.core.cypher.asParam @@ -109,5 +111,16 @@ class Neo4jClientKotlinInteropIT @Autowired constructor( override fun driver(): Driver { return neo4jConnectionSupport.driver } + + @Bean + @Primary + open fun getConfiguration(): org.neo4j.cypherdsl.core.renderer.Configuration? { + if (neo4jConnectionSupport.isCypher5SyntaxCompatible) { + return org.neo4j.cypherdsl.core.renderer.Configuration.newConfig() + .withDialect(Dialect.NEO4J_5_DEFAULT_CYPHER).build() + } + + return org.neo4j.cypherdsl.core.renderer.Configuration.newConfig().withDialect(Dialect.NEO4J_4).build() + } } } diff --git a/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/Neo4jListContainsTest.kt b/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/Neo4jListContainsIT.kt similarity index 88% rename from src/test/kotlin/org/springframework/data/neo4j/integration/imperative/Neo4jListContainsTest.kt rename to src/test/kotlin/org/springframework/data/neo4j/integration/imperative/Neo4jListContainsIT.kt index fcdbb3f5b..347072f32 100644 --- a/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/Neo4jListContainsTest.kt +++ b/src/test/kotlin/org/springframework/data/neo4j/integration/imperative/Neo4jListContainsIT.kt @@ -18,10 +18,12 @@ package org.springframework.data.neo4j.integration.imperative import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test +import org.neo4j.cypherdsl.core.renderer.Dialect import org.neo4j.driver.Driver import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Primary import org.springframework.data.neo4j.config.AbstractNeo4jConfig import org.springframework.data.neo4j.core.DatabaseSelectionProvider import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager @@ -37,7 +39,7 @@ import org.springframework.transaction.PlatformTransactionManager import org.springframework.transaction.annotation.EnableTransactionManagement @Neo4jIntegrationTest -class Neo4jListContainsTest { +class Neo4jListContainsIT { companion object { @JvmStatic @@ -94,6 +96,17 @@ class Neo4jListContainsTest { val bookmarkCapture = bookmarkCapture() return Neo4jTransactionManager(driver, databaseNameProvider, Neo4jBookmarkManager.create(bookmarkCapture)) } + + @Bean + @Primary + open fun getConfiguration(): org.neo4j.cypherdsl.core.renderer.Configuration? { + if (neo4jConnectionSupport.isCypher5SyntaxCompatible) { + return org.neo4j.cypherdsl.core.renderer.Configuration.newConfig() + .withDialect(Dialect.NEO4J_5_DEFAULT_CYPHER).build() + } + + return org.neo4j.cypherdsl.core.renderer.Configuration.newConfig().withDialect(Dialect.NEO4J_4).build() + } } } diff --git a/src/test/kotlin/org/springframework/data/neo4j/integration/k/KotlinIssuesIT.kt b/src/test/kotlin/org/springframework/data/neo4j/integration/k/KotlinIssuesIT.kt index b7ffd0d14..e298f0e9f 100644 --- a/src/test/kotlin/org/springframework/data/neo4j/integration/k/KotlinIssuesIT.kt +++ b/src/test/kotlin/org/springframework/data/neo4j/integration/k/KotlinIssuesIT.kt @@ -19,10 +19,12 @@ package org.springframework.data.neo4j.integration.k import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test +import org.neo4j.cypherdsl.core.renderer.Dialect import org.neo4j.driver.Driver import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Primary import org.springframework.data.neo4j.config.AbstractNeo4jConfig import org.springframework.data.neo4j.core.DatabaseSelectionProvider import org.springframework.data.neo4j.core.Neo4jTemplate @@ -30,6 +32,7 @@ import org.springframework.data.neo4j.core.schema.Id import org.springframework.data.neo4j.core.schema.Node import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager +import org.springframework.data.neo4j.integration.imperative.Neo4jListContainsIT import org.springframework.data.neo4j.repository.Neo4jRepository import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories import org.springframework.data.neo4j.test.BookmarkCapture @@ -111,5 +114,16 @@ internal class KotlinIssuesIT { open fun transactionTemplate(transactionManager: PlatformTransactionManager): TransactionTemplate { return TransactionTemplate(transactionManager) } + + @Bean + @Primary + open fun getConfiguration(): org.neo4j.cypherdsl.core.renderer.Configuration? { + if (neo4jConnectionSupport.isCypher5SyntaxCompatible) { + return org.neo4j.cypherdsl.core.renderer.Configuration.newConfig() + .withDialect(Dialect.NEO4J_5_DEFAULT_CYPHER).build() + } + + return org.neo4j.cypherdsl.core.renderer.Configuration.newConfig().withDialect(Dialect.NEO4J_4).build() + } } }