diff --git a/src/main/java/org/springframework/data/aerospike/query/FilterOperation.java b/src/main/java/org/springframework/data/aerospike/query/FilterOperation.java index 926cb1570..298dfa992 100644 --- a/src/main/java/org/springframework/data/aerospike/query/FilterOperation.java +++ b/src/main/java/org/springframework/data/aerospike/query/FilterOperation.java @@ -483,11 +483,11 @@ public Filter sIndexFilter(Map qualifierMap) { MAP_VAL_NOTEQ_BY_KEY { @Override public Exp filterExp(Map qualifierMap) { - if (getValue1(qualifierMap) instanceof Value.NullValue || getValue1(qualifierMap).getObject() != null) { - return findExistingByMapKey(qualifierMap); - } else { +// if (getValue1(qualifierMap) instanceof Value.NullValue || getValue1(qualifierMap).getObject() != null) { +// return findExistingByMapKey(qualifierMap); +// } else { return getFilterExpMapValNotEqOrFail(qualifierMap, Exp::ne); - } +// } } @Override @@ -781,7 +781,7 @@ public Filter sIndexFilter(Map qualifierMap) { MAP_VAL_EXISTS_BY_KEY { @Override public Exp filterExp(Map qualifierMap) { - return FilterOperation.findExistingByMapKey(qualifierMap); + return mapKeysContain(qualifierMap); } @Override @@ -792,7 +792,7 @@ public Filter sIndexFilter(Map qualifierMap) { MAP_VAL_NOT_EXISTS_BY_KEY { @Override public Exp filterExp(Map qualifierMap) { - return findNotExistingByMapKey(qualifierMap); + return mapKeysNotContain(qualifierMap); } @Override @@ -803,8 +803,17 @@ public Filter sIndexFilter(Map qualifierMap) { MAP_VAL_IS_NOT_NULL_BY_KEY { @Override public Exp filterExp(Map qualifierMap) { - return findExistingByMapKey(qualifierMap); - } + String[] dotPathArray = getDotPathArray(getDotPath(qualifierMap), + "MAP_VAL_IS_NULL_BY_KEY: dotPath was not set"); + if (dotPathArray.length > 1) { + // in case it is a field of an object set to null the key does not get added to a Map, + // so it is enough to look for Maps with the given key + return mapKeysContain(qualifierMap); + } else { + // currently querying for a specific Map key with not null value is not supported, + // it is recommended to use querying for an existing key and then filtering key:!=null + return getMapValEqOrFail(qualifierMap, Exp::eq, "MAP_VAL_IS_NULL_BY_KEY"); + } } @Override public Filter sIndexFilter(Map qualifierMap) { @@ -814,7 +823,17 @@ public Filter sIndexFilter(Map qualifierMap) { MAP_VAL_IS_NULL_BY_KEY { @Override public Exp filterExp(Map qualifierMap) { - return getMapValEqOrFail(qualifierMap, Exp::eq, "MAP_VAL_IS_NULL_BY_KEY"); + String[] dotPathArray = getDotPathArray(getDotPath(qualifierMap), + "MAP_VAL_IS_NULL_BY_KEY: dotPath was not set"); + if (dotPathArray.length > 1) { + // in case it is a field of an object set to null the key does not get added to a Map, + // so it is enough to look for Maps without the given key + return mapKeysNotContain(qualifierMap); + } else { + // currently querying for a specific Map key with explicit null value is not supported, + // it is recommended to use querying for an existing key and then filtering key:null + return getMapValEqOrFail(qualifierMap, Exp::eq, "MAP_VAL_IS_NULL_BY_KEY"); + } } @Override @@ -825,21 +844,7 @@ public Filter sIndexFilter(Map qualifierMap) { MAP_KEYS_CONTAIN { @Override public Exp filterExp(Map qualifierMap) { - - Exp value = switch (getValue1(qualifierMap).getType()) { - case INTEGER -> Exp.val(getValue1(qualifierMap).toLong()); - case STRING -> Exp.val(getValue1(qualifierMap).toString()); - case JBLOB -> getConvertedValue1Exp(qualifierMap); - case LIST -> Exp.val((List) getValue1(qualifierMap).getObject()); - case MAP -> Exp.val(getConvertedMap(qualifierMap, FilterOperation::getValue1)); - default -> throw new UnsupportedOperationException( - "MAP_KEYS_CONTAIN FilterExpression unsupported type: got " + - getValue1(qualifierMap).getClass().getSimpleName()); - }; - - return Exp.gt( - MapExp.getByKey(MapReturnType.COUNT, Exp.Type.INT, value, Exp.mapBin(getField(qualifierMap))), - Exp.val(0)); + return mapKeysContain(qualifierMap); } @Override @@ -850,23 +855,7 @@ public Filter sIndexFilter(Map qualifierMap) { MAP_KEYS_NOT_CONTAIN { @Override public Exp filterExp(Map qualifierMap) { - - Exp value = switch (getValue1(qualifierMap).getType()) { - case INTEGER -> Exp.val(getValue1(qualifierMap).toLong()); - case STRING -> Exp.val(getValue1(qualifierMap).toString()); - case JBLOB -> getConvertedValue1Exp(qualifierMap); - case LIST -> Exp.val((List) getValue1(qualifierMap).getObject()); - case MAP -> Exp.val(getConvertedMap(qualifierMap, FilterOperation::getValue1)); - default -> throw new UnsupportedOperationException( - "MAP_KEYS_CONTAIN FilterExpression unsupported type: got " + - getValue1(qualifierMap).getClass().getSimpleName()); - }; - - Exp mapIsNull = Exp.not(Exp.binExists(getField(qualifierMap))); - Exp mapKeysNotContaining = Exp.eq( - MapExp.getByKey(MapReturnType.COUNT, Exp.Type.INT, value, Exp.mapBin(getField(qualifierMap))), - Exp.val(0)); - return Exp.or(mapIsNull, mapKeysNotContaining); + return mapKeysNotContain(qualifierMap); } @Override @@ -877,21 +866,7 @@ public Filter sIndexFilter(Map qualifierMap) { MAP_VALUES_CONTAIN { @Override public Exp filterExp(Map qualifierMap) { - Exp value = switch (getValue1(qualifierMap).getType()) { - case INTEGER -> Exp.val(getValue1(qualifierMap).toLong()); - case STRING -> Exp.val(getValue1(qualifierMap).toString()); - case JBLOB -> getConvertedValue1Exp(qualifierMap); - case LIST -> Exp.val((List) getValue1(qualifierMap).getObject()); - case MAP -> Exp.val(getConvertedMap(qualifierMap, FilterOperation::getValue1)); - case NULL -> Exp.nil(); - default -> throw new UnsupportedOperationException( - "MAP_VALUES_CONTAIN FilterExpression unsupported type: got " + - getValue1(qualifierMap).getClass().getSimpleName()); - }; - - return Exp.gt( - MapExp.getByValue(MapReturnType.COUNT, value, Exp.mapBin(getField(qualifierMap))), - Exp.val(0)); + return mapValuesContain(qualifierMap); } @Override @@ -902,23 +877,7 @@ public Filter sIndexFilter(Map qualifierMap) { MAP_VALUES_NOT_CONTAIN { @Override public Exp filterExp(Map qualifierMap) { - Exp value = switch (getValue1(qualifierMap).getType()) { - case INTEGER -> Exp.val(getValue1(qualifierMap).toLong()); - case STRING -> Exp.val(getValue1(qualifierMap).toString()); - case JBLOB -> getConvertedValue1Exp(qualifierMap); - case LIST -> Exp.val((List) getValue1(qualifierMap).getObject()); - case MAP -> Exp.val(getConvertedMap(qualifierMap, FilterOperation::getValue1)); - case NULL -> Exp.nil(); - default -> throw new UnsupportedOperationException( - "MAP_VALUES_CONTAIN FilterExpression unsupported type: got " + - getValue1(qualifierMap).getClass().getSimpleName()); - }; - - Exp mapIsNull = Exp.not(Exp.binExists(getField(qualifierMap))); - Exp mapValuesNotContaining = Exp.eq( - MapExp.getByValue(MapReturnType.COUNT, value, Exp.mapBin(getField(qualifierMap))), - Exp.val(0)); - return Exp.or(mapIsNull, mapValuesNotContaining); + return mapValuesNotContain(qualifierMap); } @Override @@ -1298,19 +1257,54 @@ public Filter sIndexFilter(Map qualifierMap) { } }; - private static Exp findExistingByMapKey(Map qualifierMap) { - String key = getValue2(qualifierMap).toString(); - return Exp.gt( - MapExp.getByKey(MapReturnType.COUNT, Exp.Type.INT, Exp.val(key), - Exp.mapBin(getField(qualifierMap))), + private static Exp mapKeysNotContain(Map qualifierMap) { + String errMsg = "MAP_KEYS_NOT_CONTAIN FilterExpression unsupported type: got " + + getValue1(qualifierMap).getClass().getSimpleName(); + return mapKeysCount(qualifierMap, Exp::eq, errMsg); + } + + private static Exp mapKeysContain(Map qualifierMap) { + String errMsg = "MAP_KEYS_CONTAIN FilterExpression unsupported type: got " + + getValue1(qualifierMap).getClass().getSimpleName(); + return mapKeysCount(qualifierMap, Exp::gt, errMsg); + } + + private static Exp mapKeysCount(Map qualifierMap, BinaryOperator operator, String errMsg) { + Exp value = getValue1Exp(qualifierMap, errMsg); + return operator.apply( + MapExp.getByKey(MapReturnType.COUNT, Exp.Type.INT, value, Exp.mapBin(getField(qualifierMap))), Exp.val(0)); + } - private static Exp findNotExistingByMapKey(Map qualifierMap) { - String key = getValue2(qualifierMap).toString(); - return Exp.eq( - MapExp.getByKey(MapReturnType.COUNT, Exp.Type.INT, Exp.val(key), - Exp.mapBin(getField(qualifierMap))), + private static Exp mapValuesNotContain(Map qualifierMap) { + String errMsg = "MAP_VALUES_NOT_CONTAIN FilterExpression unsupported type: got " + + getValue1(qualifierMap).getClass().getSimpleName(); + return mapValuesCount(qualifierMap, Exp::eq, errMsg); + } + + private static Exp mapValuesContain(Map qualifierMap) { + String errMsg = "MAP_VALUES_CONTAIN FilterExpression unsupported type: got " + + getValue1(qualifierMap).getClass().getSimpleName(); + return mapValuesCount(qualifierMap, Exp::gt, errMsg); + } + + private static Exp getValue1Exp(Map qualifierMap, String errMsg) { + return switch (getValue1(qualifierMap).getType()) { + case INTEGER -> Exp.val(getValue1(qualifierMap).toLong()); + case STRING -> Exp.val(getValue1(qualifierMap).toString()); + case JBLOB -> getConvertedValue1Exp(qualifierMap); + case LIST -> Exp.val((List) getValue1(qualifierMap).getObject()); + case MAP -> Exp.val(getConvertedMap(qualifierMap, FilterOperation::getValue1)); + case NULL -> Exp.nil(); + default -> throw new UnsupportedOperationException(errMsg); + }; + } + + private static Exp mapValuesCount(Map qualifierMap, BinaryOperator operator, String errMsg) { + Exp value = getValue1Exp(qualifierMap, errMsg); + return operator.apply( + MapExp.getByValue(MapReturnType.COUNT, value, Exp.mapBin(getField(qualifierMap))), Exp.val(0)); } @@ -1450,8 +1444,6 @@ yield getMapValEqExp(qualifierMap, expType, convertedValue, dotPathArr, operator useCtx); case MAP -> getMapValEqExp(qualifierMap, Exp.Type.MAP, Exp.val(getConvertedMap(qualifierMap, value1)), dotPathArr, operator, useCtx); - case ParticleType.NULL -> getMapValEqExp(qualifierMap, Exp.Type.NIL, value1, dotPathArr, operator, - useCtx); default -> throw new UnsupportedOperationException( opName + " FilterExpression unsupported type: " + value1.getClass().getSimpleName()); }; diff --git a/src/main/java/org/springframework/data/aerospike/repository/query/AerospikeQueryCreator.java b/src/main/java/org/springframework/data/aerospike/repository/query/AerospikeQueryCreator.java index c98e11698..fdc6790c1 100644 --- a/src/main/java/org/springframework/data/aerospike/repository/query/AerospikeQueryCreator.java +++ b/src/main/java/org/springframework/data/aerospike/repository/query/AerospikeQueryCreator.java @@ -42,6 +42,8 @@ import java.util.Optional; import java.util.stream.Collectors; +import static org.springframework.data.aerospike.query.FilterOperation.IS_NOT_NULL; +import static org.springframework.data.aerospike.query.FilterOperation.IS_NULL; import static org.springframework.data.aerospike.query.FilterOperation.LIST_VAL_CONTAINING; import static org.springframework.data.aerospike.query.FilterOperation.MAP_KEYS_CONTAIN; import static org.springframework.data.aerospike.query.FilterOperation.MAP_KEYS_NOT_CONTAIN; @@ -124,8 +126,9 @@ private AerospikeCriteria create(Part part, AerospikePersistentProperty property case NOT_IN -> getCriteria(part, property, v1, null, parameters, FilterOperation.NOT_IN); case TRUE -> getCriteria(part, property, true, null, parameters, FilterOperation.EQ); case FALSE -> getCriteria(part, property, false, null, parameters, FilterOperation.EQ); - case EXISTS, IS_NOT_NULL -> getCriteria(part, property, null, null, parameters, FilterOperation.IS_NOT_NULL); - case IS_NULL -> getCriteria(part, property, null, null, parameters, FilterOperation.IS_NULL); + case EXISTS, IS_NOT_NULL -> + getCriteria(part, property, null, null, parameters, FilterOperation.IS_NOT_NULL); + case IS_NULL -> getCriteria(part, property, null, null, parameters, IS_NULL); default -> throw new IllegalArgumentException("Unsupported keyword '" + part.getType() + "'"); }; } @@ -240,6 +243,8 @@ public AerospikeCriteria getCriteria(Part part, AerospikePersistentProperty prop if (part.getProperty().hasNext()) { // if it is a POJO field (a simple field or an inner POJO) if (op == FilterOperation.BETWEEN) { value3 = Value.get(value2); // contains upper limit + } else if (op == IS_NOT_NULL || op == IS_NULL) { + value1 = Value.get(property.getFieldName()); // contains key (field name) } op = getCorrespondingMapValueFilterOperationOrFail(op); value2 = Value.get(property.getFieldName()); // VALUE2 contains key (field name) diff --git a/src/test/java/org/springframework/data/aerospike/repository/PersonRepositoryQueryTests.java b/src/test/java/org/springframework/data/aerospike/repository/PersonRepositoryQueryTests.java index aeb2d7f6b..2cb71a3ed 100644 --- a/src/test/java/org/springframework/data/aerospike/repository/PersonRepositoryQueryTests.java +++ b/src/test/java/org/springframework/data/aerospike/repository/PersonRepositoryQueryTests.java @@ -597,13 +597,11 @@ void findByIsNull() { repository.save(stefan); assertThat(repository.findByAddressIsNull()).doesNotContain(stefan); assertThat(repository.findByAddressZipCodeIsNull()).contains(stefan).doesNotContain(carter, dave); - assertThat(repository.findByAddressZipCode(null)).contains(stefan).doesNotContain(carter, dave); dave.setBestFriend(stefan); repository.save(dave); carter.setFriend(dave); repository.save(carter); - assertThat(repository.findByFriendBestFriendAddressZipCode(null)).contains(carter); assertThat(repository.findByFriendBestFriendAddressZipCodeIsNull()).contains(carter); stefan.setAddress(null); // cleanup @@ -614,8 +612,15 @@ void findByIsNull() { stringMap.put("key", null); stefan.setStringMap(stringMap); repository.save(stefan); - assertThat(repository.findByStringMapContaining("key", (String) null)).contains(stefan); // key-specific - assertThat(repository.findByStringMapContaining(null, VALUE)).contains(stefan); // among all map values + assertThat(repository.findByStringMapContaining(null, VALUE)).contains(stefan); // among map values + + // Currently getting key-specific results for a Map requires 2 steps: + // firstly query for all entities with existing map key + List personsWithMapKeyExists = repository.findByStringMapContaining("key", KEY); + // and then leave only the records that have the key's value == null + List personsWithMapValueNull = personsWithMapKeyExists.stream() + .filter(person -> person.getStringMap().get("key") == null).toList(); + assertThat(personsWithMapValueNull).contains(stefan); List strings = new ArrayList<>(); strings.add(null); @@ -639,13 +644,11 @@ void findByIsNotNull() { repository.save(stefan); assertThat(repository.findByAddressIsNotNull()).contains(stefan); // Address is not null assertThat(repository.findByAddressZipCodeIsNotNull()).contains(stefan); // zipCode is not null - assertThat(repository.findByAddressZipCodeIsNot(null)).contains(stefan); stefan.setAddress(new Address(null, null, null, null)); repository.save(stefan); assertThat(repository.findByAddressIsNotNull()).contains(stefan); // Address is not null assertThat(repository.findByAddressZipCodeIsNotNull()).doesNotContain(stefan); // zipCode is null - assertThat(repository.findByAddressZipCodeIsNot(null)).doesNotContain(stefan); stefan.setAddress(null); // cleanup repository.save(stefan); @@ -654,13 +657,12 @@ void findByIsNotNull() { stringMap.put("key", "str"); stefan.setStringMap(stringMap); repository.save(stefan); - assertThat(repository.findByStringMapNotContaining(null, VALUE)).contains(stefan); // looks at all values + assertThat(repository.findByStringMapNotContaining(null, VALUE)).contains(stefan); // among map values - // Currently only for a Map "IsNotNull" differs from "Exists" as map values can both exist and store null - // Getting key-specific results requires post-processing: - // firstly getting all entities with existing map key + // Currently getting key-specific results for a Map requires 2 steps: + // firstly query for all entities with existing map key List personsWithMapKeyExists = repository.findByStringMapContaining("key", KEY); - // and then filtering those that have the key's value equal to null + // and then leave only the records that have the key's value != null List personsWithMapValueNotNull = personsWithMapKeyExists.stream() .filter(person -> person.getStringMap().get("key") != null).toList(); assertThat(personsWithMapValueNotNull).contains(stefan); diff --git a/src/test/java/org/springframework/data/aerospike/sample/PersonRepository.java b/src/test/java/org/springframework/data/aerospike/sample/PersonRepository.java index f8fb80dea..bf04f38ba 100644 --- a/src/test/java/org/springframework/data/aerospike/sample/PersonRepository.java +++ b/src/test/java/org/springframework/data/aerospike/sample/PersonRepository.java @@ -168,13 +168,7 @@ public interface PersonRepository

extends AerospikeRepository< */ List

findByAddressLessThan(Address address); - List

findByAddressZipCode(String zipCode); - - /** - * Find all entities that satisfy the condition "have address with zipCode which is not equal to the given - * argument" - */ - List

findByAddressZipCodeIsNot(String zipCode); + List

findByAddressZipCode(@NotNull String zipCode); List

findByAddressZipCodeContaining(String str); @@ -497,7 +491,7 @@ public interface PersonRepository

extends AerospikeRepository< * @param key Map key * @param value String to check if map value equals it */ - List

findByStringMapContaining(String key, String value); + List

findByStringMapContaining(String key, @NotNull String value); /** * Find all entities that satisfy the condition "have the given map key3 and the value3 equal to the given strings" @@ -693,7 +687,7 @@ List

findByStringMapContaining(String key1, String value1, String key2, Strin * * @param zipCode - Zip code to check for equality */ - List

findByFriendBestFriendAddressZipCode(String zipCode); + List

findByFriendBestFriendAddressZipCode(@NotNull String zipCode); /** * Find all entities that satisfy the condition "have a friend who has bestFriend with the address with apartment