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 <meistermeier@gmail.com>
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<StatementBuilder.OngoingMatchAndUpdate, Statement> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<T> extends BasicPersistentEntity<T, Neo
|
||||
|
||||
private List<NodeDescription<?>> childNodeDescriptionsInHierarchy;
|
||||
|
||||
private final Lazy<Neo4jPersistentProperty> vectorProperty;
|
||||
|
||||
DefaultNeo4jPersistentEntity(TypeInformation<T> information) {
|
||||
super(information);
|
||||
|
||||
@@ -99,6 +102,8 @@ final class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo
|
||||
this.isRelationshipPropertiesEntity = Lazy.of(() -> 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<T> extends BasicPersistentEntity<T, Neo
|
||||
verifyDynamicAssociations();
|
||||
verifyAssociationsWithProperties();
|
||||
verifyDynamicLabels();
|
||||
verifyAtMostOneVectorDefinition();
|
||||
}
|
||||
|
||||
private void verifyIdDescription() {
|
||||
@@ -301,6 +307,18 @@ final class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo
|
||||
DynamicLabels.class.getSimpleName(), namesOfPropertiesWithDynamicLabels));
|
||||
}
|
||||
|
||||
private void verifyAtMostOneVectorDefinition() {
|
||||
List<Neo4jPersistentProperty> 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:<br>
|
||||
* 1. If there is no {@link Node} annotation, use the class name.<br>
|
||||
@@ -410,6 +428,23 @@ final class DefaultNeo4jPersistentEntity<T> extends BasicPersistentEntity<T, Neo
|
||||
return this.getTypeInformation().getRawTypeInformation().getType().isInterface();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasVectorProperty() {
|
||||
return Optional.ofNullable(getVectorProperty()).map(v -> 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());
|
||||
}
|
||||
|
||||
@@ -75,4 +75,9 @@ public interface Neo4jPersistentEntity<T>
|
||||
}
|
||||
return isUsingInternalIds() && Neo4jPersistentEntity.DEPRECATED_GENERATED_ID_TYPES.contains(getRequiredIdProperty().getType());
|
||||
}
|
||||
|
||||
boolean hasVectorProperty();
|
||||
|
||||
Neo4jPersistentProperty getVectorProperty();
|
||||
Neo4jPersistentProperty getRequiredVectorProperty();
|
||||
}
|
||||
|
||||
@@ -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<Neo4jPersist
|
||||
return this.isAnnotationPresent(DynamicLabels.class) && this.isCollectionLike();
|
||||
}
|
||||
|
||||
default boolean isVectorProperty() {
|
||||
return this.getType().isAssignableFrom(Vector.class);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Neo4jPersistentPropertyConverter<?> getOptionalConverter();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,11 +210,34 @@ class TypeConversionIT extends Neo4jConversionsITBase {
|
||||
Map<String, Object> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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<String> listOfStrings, Set<String> setOfStrings, Instant anInstant, UUID aUUID, URL aURL, URI aURI, SomeEnum anEnum, SomeEnum[] anArrayOfEnums, List<Double> listOfDoubles, List<SomeEnum> 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<String> listOfStrings, Set<String> setOfStrings, Instant anInstant, UUID aUUID, URL aURL, URI aURI, SomeEnum anEnum, SomeEnum[] anArrayOfEnums, List<Double> listOfDoubles, List<SomeEnum> 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 + ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user