From bbf261e75003bd35fd97ba5678daae6ff68f7427 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Wed, 16 Apr 2025 16:59:38 +0200 Subject: [PATCH] GH-3003 - Add vector type support. This commit combines Spring Data Commons' Vector type with the Neo4j vector functionality. Fields defined as Spring Data Commons `Vector` will get persisted through the `setNodeVectorProperty` procedure. Closes #3003 Signed-off-by: Gerrit Meier --- .../ROOT/pages/appendix/conversions.adoc | 15 +++++ .../neo4j/core/convert/AdditionalTypes.java | 12 +++- .../data/neo4j/core/mapping/Constants.java | 2 + .../neo4j/core/mapping/CypherGenerator.java | 60 +++++++++---------- .../mapping/DefaultNeo4jEntityConverter.java | 11 +++- .../mapping/DefaultNeo4jPersistentEntity.java | 35 +++++++++++ .../core/mapping/Neo4jPersistentEntity.java | 5 ++ .../core/mapping/Neo4jPersistentProperty.java | 5 ++ .../DefaultNeo4jPersistentEntityTest.java | 39 ++++++++++++ .../TypeConversionIT.java | 31 ++++++++-- .../conversion/Neo4jConversionsITBase.java | 5 +- .../ThingWithAllAdditionalTypes.java | 34 +++++++++-- 12 files changed, 211 insertions(+), 43 deletions(-) diff --git a/src/main/antora/modules/ROOT/pages/appendix/conversions.adoc b/src/main/antora/modules/ROOT/pages/appendix/conversions.adoc index 268a55131..dc24a35c5 100644 --- a/src/main/antora/modules/ROOT/pages/appendix/conversions.adoc +++ b/src/main/antora/modules/ROOT/pages/appendix/conversions.adoc @@ -188,6 +188,10 @@ If you require the time zone, use a type that supports it (i.e. `ZoneDateTime`) |Point with CRS 4326 and x/y corresponding to lat/long | +|`org.springframework.data.domain.Vector` +|persisted through `setNodeVectorProperty` +| + |Instances of `Enum` |String (The name value of the enum) | @@ -210,6 +214,17 @@ If you require the time zone, use a type that supports it (i.e. `ZoneDateTime`) |=== +[[build-in.conversions.vector]] +=== Vector type +Spring Data has its own type for vector representation `org.springframework.data.domain.Vector`. +While this can be used as a wrapper around a `float` or `double` array, Spring Data Neo4j supports only the `double` variant right now. +From a user perspective, it is possible to only define the `Vector` interface on the property definition and use either `double` or `float`. +Neo4j will store both `double` and `float` variants as a 64-bit Cypher `FLOAT` value, which is consistent with values persisted through Cypher and the dedicated `setNodeVectorProperty` function that Spring Data Neo4j uses to persist the property. + +NOTE: Spring Data Neo4j only allows one `Vector` property to be present in an entity definition. + +NOTE: Please be aware that a persisted `float` value differs from a read back value due to the nature of floating numbers. + [[custom.conversions]] == Custom conversions diff --git a/src/main/java/org/springframework/data/neo4j/core/convert/AdditionalTypes.java b/src/main/java/org/springframework/data/neo4j/core/convert/AdditionalTypes.java index ea866e1b2..e26949acb 100644 --- a/src/main/java/org/springframework/data/neo4j/core/convert/AdditionalTypes.java +++ b/src/main/java/org/springframework/data/neo4j/core/convert/AdditionalTypes.java @@ -50,6 +50,7 @@ import org.springframework.core.convert.converter.GenericConverter; import org.springframework.data.convert.ConverterBuilder; import org.springframework.data.convert.ReadingConverter; import org.springframework.data.convert.WritingConverter; +import org.springframework.data.domain.Vector; import org.springframework.data.mapping.MappingException; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -104,7 +105,7 @@ final class AdditionalTypes { hlp.add(ConverterBuilder.reading(Value.class, Node.class, Value::asNode)); hlp.add(ConverterBuilder.reading(Value.class, Relationship.class, Value::asRelationship)); hlp.add(ConverterBuilder.reading(Value.class, Map.class, Value::asMap).andWriting(AdditionalTypes::value)); - + hlp.add(ConverterBuilder.reading(Value.class, Vector.class, AdditionalTypes::asVector).andWriting(AdditionalTypes::value)); CONVERTERS = Collections.unmodifiableList(hlp); } @@ -112,6 +113,10 @@ final class AdditionalTypes { return Values.value(map); } + static Value value(Vector vector) { + return Values.value(vector.toDoubleArray()); + } + static TimeZone asTimeZone(Value value) { return TimeZone.getTimeZone(value.asString()); } @@ -462,6 +467,11 @@ final class AdditionalTypes { return array; } + static Vector asVector(Value value) { + double[] array = asDoubleArray(value); + return Vector.of(array); + } + static Value value(short[] aShortArray) { if (aShortArray == null) { return Values.NULL; diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/Constants.java b/src/main/java/org/springframework/data/neo4j/core/mapping/Constants.java index e87bd0684..a2dd80575 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/Constants.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/Constants.java @@ -58,6 +58,8 @@ public final class Constants { public static final String NAME_OF_ID = "__id__"; public static final String NAME_OF_VERSION_PARAM = "__version__"; public static final String NAME_OF_PROPERTIES_PARAM = "__properties__"; + public static final String NAME_OF_VECTOR_PROPERTY = "__vectorProperty__"; + public static final String NAME_OF_VECTOR_VALUE = "__vectorValue__"; /** * Indicates the parameter that contains the static labels which are required to correctly compute the difference * in the list of dynamic labels when saving a node. 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 9eaccb6c4..5348331a7 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 @@ -308,6 +308,17 @@ public enum CypherGenerator { Assert.notNull(idDescription, "Cannot save individual nodes without an id attribute"); Parameter idParameter = parameter(Constants.NAME_OF_ID); + Function vectorProcedureCall = (bs) -> { + if (((Neo4jPersistentEntity) nodeDescription).hasVectorProperty()) { + return bs.with(rootNode) + .call("db.create.setNodeVectorProperty") + .withArgs(rootNode.getRequiredSymbolicName(), parameter(Constants.NAME_OF_VECTOR_PROPERTY), parameter(Constants.NAME_OF_VECTOR_VALUE)) + .withoutResults() + .returning(rootNode).build(); + } + return bs.returning(rootNode).build(); + }; + if (!idDescription.isInternallyGeneratedId()) { GraphPropertyDescription idPropertyDescription = ((Neo4jPersistentEntity) nodeDescription).getRequiredIdProperty(); @@ -316,92 +327,79 @@ public enum CypherGenerator { String nameOfPossibleExistingNode = "hlp"; Node possibleExistingNode = node(primaryLabel, additionalLabels).named(nameOfPossibleExistingNode); - Statement createIfNew = updateDecorator.apply(optionalMatch(possibleExistingNode) + Statement createIfNew = vectorProcedureCall.apply(updateDecorator.apply(optionalMatch(possibleExistingNode) .where(createCompositePropertyCondition(idPropertyDescription, possibleExistingNode.getRequiredSymbolicName(), idParameter)) .with(possibleExistingNode) .where(possibleExistingNode.isNull()) .create(rootNode.withProperties(versionProperty, literalOf(0))) .with(rootNode) - .mutate(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM))).returning(rootNode) - .build(); + .mutate(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM)))); - Statement updateIfExists = updateDecorator.apply(match(rootNode) + Statement updateIfExists = vectorProcedureCall.apply(updateDecorator.apply(match(rootNode) .where(createCompositePropertyCondition(idPropertyDescription, rootNode.getRequiredSymbolicName(), idParameter)) .and(versionProperty.isEqualTo(parameter(Constants.NAME_OF_VERSION_PARAM))) // Initial check .set(versionProperty.to(versionProperty.add(literalOf(1)))) // Acquire lock .with(rootNode) .where(versionProperty.isEqualTo(coalesce(parameter(Constants.NAME_OF_VERSION_PARAM), literalOf(0)).add( literalOf(1)))) - .mutate(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM))) - .returning(rootNode) - .build(); + .mutate(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM)))); return Cypher.union(createIfNew, updateIfExists); } else { String nameOfPossibleExistingNode = "hlp"; Node possibleExistingNode = node(primaryLabel, additionalLabels).named(nameOfPossibleExistingNode); - Statement createIfNew = updateDecorator.apply(optionalMatch(possibleExistingNode) + Statement createIfNew = vectorProcedureCall.apply(updateDecorator.apply(optionalMatch(possibleExistingNode) .where(createCompositePropertyCondition(idPropertyDescription, possibleExistingNode.getRequiredSymbolicName(), idParameter)) .with(possibleExistingNode) .where(possibleExistingNode.isNull()) .create(rootNode) .with(rootNode) - .mutate(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM))).returning(rootNode) - .build(); + .mutate(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM)))); - Statement updateIfExists = updateDecorator.apply(match(rootNode) + Statement updateIfExists = vectorProcedureCall.apply(updateDecorator.apply(match(rootNode) .where(createCompositePropertyCondition(idPropertyDescription, rootNode.getRequiredSymbolicName(), idParameter)) .with(rootNode) - .mutate(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM))) - .returning(rootNode) - .build(); + .mutate(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM)))); return Cypher.union(createIfNew, updateIfExists); } } else { String nameOfPossibleExistingNode = "hlp"; Node possibleExistingNode = node(primaryLabel, additionalLabels).named(nameOfPossibleExistingNode); - Statement createIfNew; - Statement updateIfExists; - var neo4jPersistentEntity = (Neo4jPersistentEntity) nodeDescription; var nodeIdFunction = getNodeIdFunction(neo4jPersistentEntity, canUseElementId); if (neo4jPersistentEntity.hasVersionProperty()) { Property versionProperty = rootNode.property(neo4jPersistentEntity.getRequiredVersionProperty().getName()); - createIfNew = updateDecorator.apply(optionalMatch(possibleExistingNode) + var createIfNew = vectorProcedureCall.apply(updateDecorator.apply(optionalMatch(possibleExistingNode) .where(nodeIdFunction.apply(possibleExistingNode).isEqualTo(idParameter)) .with(possibleExistingNode) .where(possibleExistingNode.isNull()) .create(rootNode.withProperties(versionProperty, literalOf(0))) .with(rootNode) - .mutate(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM))) - .returning(rootNode) - .build(); + .mutate(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM)))); - updateIfExists = updateDecorator.apply(match(rootNode) + var updateIfExists = vectorProcedureCall.apply(updateDecorator.apply(match(rootNode) .where(nodeIdFunction.apply(rootNode).isEqualTo(idParameter)) .and(versionProperty.isEqualTo(parameter(Constants.NAME_OF_VERSION_PARAM))) // Initial check .set(versionProperty.to(versionProperty.add(literalOf(1)))) // Acquire lock .with(rootNode) .where(versionProperty.isEqualTo(coalesce(parameter(Constants.NAME_OF_VERSION_PARAM), literalOf(0)).add( literalOf(1)))) - .mutate(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM))) - .returning(rootNode).build(); + .mutate(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM)))); + return Cypher.union(createIfNew, updateIfExists); } else { - createIfNew = updateDecorator - .apply(optionalMatch(possibleExistingNode).where(nodeIdFunction.apply(possibleExistingNode).isEqualTo(idParameter)) + var createStatement = vectorProcedureCall.apply(updateDecorator.apply(optionalMatch(possibleExistingNode).where(nodeIdFunction.apply(possibleExistingNode).isEqualTo(idParameter)) .with(possibleExistingNode).where(possibleExistingNode.isNull()).create(rootNode) - .set(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM))) - .returning(rootNode).build(); + .set(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM)))); + var updateStatement = vectorProcedureCall.apply(updateDecorator.apply(match(rootNode).where(nodeIdFunction.apply(rootNode).isEqualTo(idParameter)) + .mutate(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM)))); - updateIfExists = updateDecorator.apply(match(rootNode).where(nodeIdFunction.apply(rootNode).isEqualTo(idParameter)) - .mutate(rootNode, parameter(Constants.NAME_OF_PROPERTIES_PARAM))).returning(rootNode).build(); + return Cypher.union(createStatement, updateStatement); } - return Cypher.union(createIfNew, updateIfExists); } } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverter.java b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverter.java index 85ba9f1a6..70c13bd7e 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverter.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverter.java @@ -242,14 +242,13 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { PropertyHandlerSupport.of(nodeDescription).doWithProperties((Neo4jPersistentProperty p) -> { // Skip the internal properties, we don't want them to end up stored as properties - if (p.isInternalIdProperty() || p.isDynamicLabels() || p.isEntity() || p.isVersionProperty() || p.isReadOnly()) { + if (p.isInternalIdProperty() || p.isDynamicLabels() || p.isEntity() || p.isVersionProperty() || p.isReadOnly() || p.isVectorProperty()) { return; } final Value value = conversionService.writeValue(propertyAccessor.getProperty(p), p.getTypeInformation(), p.getOptionalConverter()); if (p.isComposite()) { properties.put(p.getPropertyName(), new MapValueWrapper(value)); - //value.keys().forEach(k -> properties.put(k, value.get(k))); } else { properties.put(p.getPropertyName(), value); } @@ -270,6 +269,14 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { // we incremented this upfront the persist operation so the matching version would be one "before" parameters.put(Constants.NAME_OF_VERSION_PARAM, versionProperty); } + + // special handling for vector property to provide the needed procedure information + if (nodeDescription.hasVectorProperty()) { + Neo4jPersistentProperty vectorProperty = nodeDescription.getRequiredVectorProperty(); + parameters.put(Constants.NAME_OF_VECTOR_PROPERTY, vectorProperty.getPropertyName()); + parameters.put(Constants.NAME_OF_VECTOR_VALUE, conversionService.writeValue(propertyAccessor.getProperty(vectorProperty), vectorProperty.getTypeInformation(), vectorProperty.getOptionalConverter())); + return; + } } /** diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java index d5df9f90a..83550c981 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntity.java @@ -35,6 +35,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.log.LogAccessor; import org.springframework.data.annotation.Persistent; +import org.springframework.data.domain.Vector; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.model.BasicPersistentEntity; import org.springframework.data.neo4j.core.schema.DynamicLabels; @@ -88,6 +89,8 @@ final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity> childNodeDescriptionsInHierarchy; + private final Lazy vectorProperty; + DefaultNeo4jPersistentEntity(TypeInformation information) { super(information); @@ -99,6 +102,8 @@ final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity isAnnotationPresent(RelationshipProperties.class)); this.idDescription = Lazy.of(this::computeIdDescription); this.childNodeDescriptionsInHierarchy = computeChildNodeDescriptionInHierarchy(); + this.vectorProperty = Lazy.of(() -> getGraphProperties().stream().map(Neo4jPersistentProperty.class::cast) + .filter(Neo4jPersistentProperty::isVectorProperty).findFirst().orElse(null)); } /* @@ -212,6 +217,7 @@ final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity extends BasicPersistentEntity foundVectorDefinition = new ArrayList<>(); + PropertyHandlerSupport.of(this).doWithProperties(persistentProperty -> { + if (persistentProperty.getType().isAssignableFrom(Vector.class)) { + foundVectorDefinition.add(persistentProperty); + } + }); + + Assert.state(foundVectorDefinition.size() <= 1, () -> String.format("There are multiple fields of type %s in entity %s: %s", + Vector.class.toString(), this.getName(), foundVectorDefinition.stream().map(p -> p.getPropertyName()).toList())); + } + /** * The primary label will get computed and returned by following rules:
* 1. If there is no {@link Node} annotation, use the class name.
@@ -410,6 +428,23 @@ final class DefaultNeo4jPersistentEntity extends BasicPersistentEntity true).orElse(false); + } + + public Neo4jPersistentProperty getVectorProperty() { + return this.vectorProperty.getNullable(); + } + + public Neo4jPersistentProperty getRequiredVectorProperty() { + Neo4jPersistentProperty property = getVectorProperty(); + if (property != null) { + return property; + } + throw new IllegalStateException(String.format("Required vector property not found for %s", this.getType())); + } + private static boolean hasEmptyLabelInformation(Node nodeAnnotation) { return nodeAnnotation.labels().length < 1 && !StringUtils.hasText(nodeAnnotation.primaryLabel()); } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntity.java b/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntity.java index 1c3724271..20ede2504 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntity.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentEntity.java @@ -75,4 +75,9 @@ public interface Neo4jPersistentEntity } return isUsingInternalIds() && Neo4jPersistentEntity.DEPRECATED_GENERATED_ID_TYPES.contains(getRequiredIdProperty().getType()); } + + boolean hasVectorProperty(); + + Neo4jPersistentProperty getVectorProperty(); + Neo4jPersistentProperty getRequiredVectorProperty(); } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentProperty.java b/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentProperty.java index fef820756..ca2571d56 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentProperty.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jPersistentProperty.java @@ -18,6 +18,7 @@ package org.springframework.data.neo4j.core.mapping; import java.util.Optional; import org.apiguardian.api.API; +import org.springframework.data.domain.Vector; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.neo4j.core.convert.Neo4jPersistentPropertyConverter; import org.springframework.data.neo4j.core.schema.CompositeProperty; @@ -67,6 +68,10 @@ public interface Neo4jPersistentProperty extends PersistentProperty getOptionalConverter(); diff --git a/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntityTest.java b/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntityTest.java index 5d87e6748..f212a5418 100644 --- a/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntityTest.java +++ b/src/test/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jPersistentEntityTest.java @@ -32,6 +32,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.springframework.data.annotation.ReadOnlyProperty; import org.springframework.data.annotation.Transient; +import org.springframework.data.domain.Vector; import org.springframework.data.mapping.AssociationHandler; import org.springframework.data.mapping.MappingException; import org.springframework.data.neo4j.core.convert.ConvertWith; @@ -378,6 +379,27 @@ class DefaultNeo4jPersistentEntityTest { } } + @Nested + class VectorType { + @Test + void validVectorProperties() { + Neo4jPersistentEntity persistentEntity = new Neo4jMappingContext() + .getPersistentEntity(VectorValid.class); + + assertThat(persistentEntity.getPersistentProperty("vectorProperty").isVectorProperty()); + } + + @Test + void invalidVectorProperties() { + assertThatIllegalStateException() + .isThrownBy(() -> new Neo4jMappingContext().getPersistentEntity(VectorInvalid.class)) + .withMessageContaining("There are multiple fields of type interface org.springframework.data.domain.Vector in entity org.springframework.data.neo4j.core.mapping.DefaultNeo4jPersistentEntityTest$VectorInvalid:") + // the order of properties might be not the same all the time + .withMessageContaining("vectorProperty1") + .withMessageContaining("vectorProperty2"); + } + } + @Node private static class SomeOtherNode { @Id Long id; @@ -751,4 +773,21 @@ class DefaultNeo4jPersistentEntityTest { static class IWillBeConverted { } + + @Node + static class VectorValid { + @Id @GeneratedValue + private Long id; + + Vector vectorProperty; + } + + @Node + static class VectorInvalid { + @Id @GeneratedValue + private Long id; + + Vector vectorProperty1; + Vector vectorProperty2; + } } 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 9e7b4c0a0..bdf6d543b 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 @@ -210,11 +210,34 @@ class TypeConversionIT extends Neo4jConversionsITBase { Map parameters = new HashMap<>(); parameters.put("id", id); parameters.put("attribute", fieldName); - parameters.put("v", driverValue); - long cnt = session - .run("MATCH (n) WHERE id(n) = $id AND n[$attribute] = $v RETURN COUNT(n) AS cnt", parameters) - .single().get("cnt").asLong(); + long cnt = 0L; + // the procedure will convert the value eventually and thus the equals check + // cannot be applied anymore + if (fieldName.equals("aVector")) { + var doubleList = driverValue.asList(v -> v.asDouble()); + parameters.put("v1_lower", doubleList.get(0) - 0.000001d); + parameters.put("v2_lower", doubleList.get(1) - 0.000001d); + parameters.put("v1_upper", doubleList.get(0) + 0.000001d); + parameters.put("v2_upper", doubleList.get(1) + 0.000001d); + cnt = session + .run(""" + MATCH (n) WHERE id(n) = $id + AND n[$attribute][0] > $v1_lower + AND n[$attribute][1] > $v2_lower + AND n[$attribute][0] < $v1_upper + AND n[$attribute][1] < $v2_upper + RETURN COUNT(n) AS cnt + """, + parameters) + .single().get("cnt").asLong(); + } else { + parameters.put("v", driverValue); + + cnt = session + .run("MATCH (n) WHERE id(n) = $id AND n[$attribute] = $v RETURN COUNT(n) AS cnt", parameters) + .single().get("cnt").asLong(); + } assertThat(cnt).isEqualTo(1L); } } diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/Neo4jConversionsITBase.java b/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/Neo4jConversionsITBase.java index 669e85085..7a1492c1e 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/Neo4jConversionsITBase.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/Neo4jConversionsITBase.java @@ -18,6 +18,7 @@ package org.springframework.data.neo4j.integration.shared.conversion; import org.junit.jupiter.api.BeforeAll; import org.neo4j.driver.Session; import org.neo4j.driver.Values; +import org.springframework.data.domain.Vector; import org.springframework.data.geo.Point; import org.springframework.data.neo4j.integration.shared.conversion.ThingWithAllAdditionalTypes.SomeEnum; import org.springframework.data.neo4j.test.BookmarkCapture; @@ -126,6 +127,7 @@ public abstract class Neo4jConversionsITBase { hlp.put("aZoneId", ZoneId.of("America/New_York")); hlp.put("aZeroPeriod", Period.of(0, 0, 0)); hlp.put("aZeroDuration", Duration.ZERO); + hlp.put("aVector", Vector.of(0.1d, 0.2d)); ADDITIONAL_TYPES = Collections.unmodifiableMap(hlp); } @@ -278,7 +280,8 @@ public abstract class Neo4jConversionsITBase { n.anEnum = 'TheUsualMisfit', n.anArrayOfEnums = ['ValueA', 'ValueB'], n.aCollectionOfEnums = ['ValueC', 'TheUsualMisfit'], n.aTimeZone = 'America/Los_Angeles',\s - n.aZoneId = 'America/New_York', n.aZeroPeriod = duration('PT0S'), n.aZeroDuration = duration('PT0S') + n.aZoneId = 'America/New_York', n.aZeroPeriod = duration('PT0S'), n.aZeroDuration = duration('PT0S'), + n.aVector = [0.1, 0.2] RETURN id(n) AS id """, parameters).single().get("id").asLong(); diff --git a/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/ThingWithAllAdditionalTypes.java b/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/ThingWithAllAdditionalTypes.java index 042197f10..0949ad21d 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/ThingWithAllAdditionalTypes.java +++ b/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/ThingWithAllAdditionalTypes.java @@ -30,6 +30,7 @@ import java.util.Set; import java.util.TimeZone; import java.util.UUID; +import org.springframework.data.domain.Vector; import org.springframework.data.neo4j.core.schema.GeneratedValue; import org.springframework.data.neo4j.core.schema.Id; import org.springframework.data.neo4j.core.schema.Node; @@ -43,7 +44,7 @@ import org.springframework.data.neo4j.core.schema.Node; @Node("AdditionalTypes") public class ThingWithAllAdditionalTypes { - private ThingWithAllAdditionalTypes(Long id, boolean[] booleanArray, byte aByte, char aChar, char[] charArray, Date aDate, BigDecimal aBigDecimal, BigInteger aBigInteger, double[] doubleArray, float aFloat, float[] floatArray, int anInt, int[] intArray, Locale aLocale, long[] longArray, short aShort, short[] shortArray, Period aPeriod, Duration aDuration, String[] stringArray, List listOfStrings, Set setOfStrings, Instant anInstant, UUID aUUID, URL aURL, URI aURI, SomeEnum anEnum, SomeEnum[] anArrayOfEnums, List listOfDoubles, List aCollectionOfEnums, TimeZone aTimeZone, ZoneId aZoneId, Period aZeroPeriod, Duration aZeroDuration) { + private ThingWithAllAdditionalTypes(Long id, boolean[] booleanArray, byte aByte, char aChar, char[] charArray, Date aDate, BigDecimal aBigDecimal, BigInteger aBigInteger, double[] doubleArray, float aFloat, float[] floatArray, int anInt, int[] intArray, Locale aLocale, long[] longArray, short aShort, short[] shortArray, Period aPeriod, Duration aDuration, String[] stringArray, List listOfStrings, Set setOfStrings, Instant anInstant, UUID aUUID, URL aURL, URI aURI, SomeEnum anEnum, SomeEnum[] anArrayOfEnums, List listOfDoubles, List aCollectionOfEnums, TimeZone aTimeZone, ZoneId aZoneId, Period aZeroPeriod, Duration aZeroDuration, Vector aVector) { this.id = id; this.booleanArray = booleanArray; this.aByte = aByte; @@ -78,6 +79,7 @@ public class ThingWithAllAdditionalTypes { this.aZoneId = aZoneId; this.aZeroPeriod = aZeroPeriod; this.aZeroDuration = aZeroDuration; + this.aVector = aVector; } public static ThingWithAllAdditionalTypesBuilder builder() { @@ -220,6 +222,10 @@ public class ThingWithAllAdditionalTypes { return this.aZeroDuration; } + public Vector getAVector() { + return this.aVector; + } + public void setBooleanArray(boolean[] booleanArray) { this.booleanArray = booleanArray; } @@ -352,6 +358,10 @@ public class ThingWithAllAdditionalTypes { this.aZeroDuration = aZeroDuration; } + public void setAVector(Vector aVector) { + this.aVector = aVector; + } + public boolean equals(final Object o) { if (o == this) { return true; @@ -505,6 +515,12 @@ public class ThingWithAllAdditionalTypes { if (this$aZeroDuration == null ? other$aZeroDuration != null : !this$aZeroDuration.equals(other$aZeroDuration)) { return false; } + + final Object this$aVector = this.getAVector(); + final Object other$aVector = other.getAVector(); + if (this$aVector == null ? other$aVector != null : !this$aVector.equals(other$aVector)) { + return false; + } return true; } @@ -569,6 +585,8 @@ public class ThingWithAllAdditionalTypes { result = result * PRIME + ($aZeroPeriod == null ? 43 : $aZeroPeriod.hashCode()); final Object $aZeroDuration = this.getAZeroDuration(); result = result * PRIME + ($aZeroDuration == null ? 43 : $aZeroDuration.hashCode()); + final Object $aVector = this.getAVector(); + result = result * PRIME + ($aVector == null ? 43 : $aVector.hashCode()); return result; } @@ -577,7 +595,7 @@ public class ThingWithAllAdditionalTypes { } public ThingWithAllAdditionalTypes withId(Long id) { - return this.id == id ? this : new ThingWithAllAdditionalTypes(id, this.booleanArray, this.aByte, this.aChar, this.charArray, this.aDate, this.aBigDecimal, this.aBigInteger, this.doubleArray, this.aFloat, this.floatArray, this.anInt, this.intArray, this.aLocale, this.longArray, this.aShort, this.shortArray, this.aPeriod, this.aDuration, this.stringArray, this.listOfStrings, this.setOfStrings, this.anInstant, this.aUUID, this.aURL, this.aURI, this.anEnum, this.anArrayOfEnums, this.listOfDoubles, this.aCollectionOfEnums, this.aTimeZone, this.aZoneId, this.aZeroPeriod, this.aZeroDuration); + return this.id == id ? this : new ThingWithAllAdditionalTypes(id, this.booleanArray, this.aByte, this.aChar, this.charArray, this.aDate, this.aBigDecimal, this.aBigInteger, this.doubleArray, this.aFloat, this.floatArray, this.anInt, this.intArray, this.aLocale, this.longArray, this.aShort, this.shortArray, this.aPeriod, this.aDuration, this.stringArray, this.listOfStrings, this.setOfStrings, this.anInstant, this.aUUID, this.aURL, this.aURI, this.anEnum, this.anArrayOfEnums, this.listOfDoubles, this.aCollectionOfEnums, this.aTimeZone, this.aZoneId, this.aZeroPeriod, this.aZeroDuration, this.aVector); } enum SomeEnum { @@ -654,6 +672,8 @@ public class ThingWithAllAdditionalTypes { private Duration aZeroDuration; + private Vector aVector; + /** * the builder */ @@ -692,6 +712,7 @@ public class ThingWithAllAdditionalTypes { private ZoneId aZoneId; private Period aZeroPeriod; private Duration aZeroDuration; + private Vector aVector; ThingWithAllAdditionalTypesBuilder() { } @@ -866,12 +887,17 @@ public class ThingWithAllAdditionalTypes { return this; } + public ThingWithAllAdditionalTypesBuilder aVector(Vector vector) { + this.aVector = aVector; + return this; + } + public ThingWithAllAdditionalTypes build() { - return new ThingWithAllAdditionalTypes(this.id, this.booleanArray, this.aByte, this.aChar, this.charArray, this.aDate, this.aBigDecimal, this.aBigInteger, this.doubleArray, this.aFloat, this.floatArray, this.anInt, this.intArray, this.aLocale, this.longArray, this.aShort, this.shortArray, this.aPeriod, this.aDuration, this.stringArray, this.listOfStrings, this.setOfStrings, this.anInstant, this.aUUID, this.aURL, this.aURI, this.anEnum, this.anArrayOfEnums, this.listOfDoubles, this.aCollectionOfEnums, this.aTimeZone, this.aZoneId, this.aZeroPeriod, this.aZeroDuration); + return new ThingWithAllAdditionalTypes(this.id, this.booleanArray, this.aByte, this.aChar, this.charArray, this.aDate, this.aBigDecimal, this.aBigInteger, this.doubleArray, this.aFloat, this.floatArray, this.anInt, this.intArray, this.aLocale, this.longArray, this.aShort, this.shortArray, this.aPeriod, this.aDuration, this.stringArray, this.listOfStrings, this.setOfStrings, this.anInstant, this.aUUID, this.aURL, this.aURI, this.anEnum, this.anArrayOfEnums, this.listOfDoubles, this.aCollectionOfEnums, this.aTimeZone, this.aZoneId, this.aZeroPeriod, this.aZeroDuration, this.aVector); } public String toString() { - return "ThingWithAllAdditionalTypes.ThingWithAllAdditionalTypesBuilder(id=" + this.id + ", booleanArray=" + java.util.Arrays.toString(this.booleanArray) + ", aByte=" + this.aByte + ", aChar=" + this.aChar + ", charArray=" + java.util.Arrays.toString(this.charArray) + ", aDate=" + this.aDate + ", aBigDecimal=" + this.aBigDecimal + ", aBigInteger=" + this.aBigInteger + ", doubleArray=" + java.util.Arrays.toString(this.doubleArray) + ", aFloat=" + this.aFloat + ", floatArray=" + java.util.Arrays.toString(this.floatArray) + ", anInt=" + this.anInt + ", intArray=" + java.util.Arrays.toString(this.intArray) + ", aLocale=" + this.aLocale + ", longArray=" + java.util.Arrays.toString(this.longArray) + ", aShort=" + this.aShort + ", shortArray=" + java.util.Arrays.toString(this.shortArray) + ", aPeriod=" + this.aPeriod + ", aDuration=" + this.aDuration + ", stringArray=" + java.util.Arrays.deepToString(this.stringArray) + ", listOfStrings=" + this.listOfStrings + ", setOfStrings=" + this.setOfStrings + ", anInstant=" + this.anInstant + ", aUUID=" + this.aUUID + ", aURL=" + this.aURL + ", aURI=" + this.aURI + ", anEnum=" + this.anEnum + ", anArrayOfEnums=" + java.util.Arrays.deepToString(this.anArrayOfEnums) + ", listOfDoubles=" + this.listOfDoubles + ", aCollectionOfEnums=" + this.aCollectionOfEnums + ", aTimeZone=" + this.aTimeZone + ", aZoneId=" + this.aZoneId + ", aZeroPeriod=" + this.aZeroPeriod + ", aZeroDuration=" + this.aZeroDuration + ")"; + return "ThingWithAllAdditionalTypes.ThingWithAllAdditionalTypesBuilder(id=" + this.id + ", booleanArray=" + java.util.Arrays.toString(this.booleanArray) + ", aByte=" + this.aByte + ", aChar=" + this.aChar + ", charArray=" + java.util.Arrays.toString(this.charArray) + ", aDate=" + this.aDate + ", aBigDecimal=" + this.aBigDecimal + ", aBigInteger=" + this.aBigInteger + ", doubleArray=" + java.util.Arrays.toString(this.doubleArray) + ", aFloat=" + this.aFloat + ", floatArray=" + java.util.Arrays.toString(this.floatArray) + ", anInt=" + this.anInt + ", intArray=" + java.util.Arrays.toString(this.intArray) + ", aLocale=" + this.aLocale + ", longArray=" + java.util.Arrays.toString(this.longArray) + ", aShort=" + this.aShort + ", shortArray=" + java.util.Arrays.toString(this.shortArray) + ", aPeriod=" + this.aPeriod + ", aDuration=" + this.aDuration + ", stringArray=" + java.util.Arrays.deepToString(this.stringArray) + ", listOfStrings=" + this.listOfStrings + ", setOfStrings=" + this.setOfStrings + ", anInstant=" + this.anInstant + ", aUUID=" + this.aUUID + ", aURL=" + this.aURL + ", aURI=" + this.aURI + ", anEnum=" + this.anEnum + ", anArrayOfEnums=" + java.util.Arrays.deepToString(this.anArrayOfEnums) + ", listOfDoubles=" + this.listOfDoubles + ", aCollectionOfEnums=" + this.aCollectionOfEnums + ", aTimeZone=" + this.aTimeZone + ", aZoneId=" + this.aZoneId + ", aZeroPeriod=" + this.aZeroPeriod + ", aZeroDuration=" + this.aZeroDuration + ", aVector=" + this.aVector + ")"; } } }