Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -577,8 +577,26 @@ public String prepareQuery(String field, Object... params) {
prepared = prepared.replace(PARAM_PREFIX + i++, ObjectUtils.getDistanceAsRedisString(distance));
break;
default:
// unfold collections
if (param instanceof Collection<?> c) {
// unfold collections and arrays
Collection<?> c = null;
if (param instanceof Collection<?>) {
c = (Collection<?>) param;
} else if (param != null && param.getClass().isArray()) {
// Convert array to collection
if (param instanceof Object[]) {
c = Arrays.asList((Object[]) param);
} else {
// Handle primitive arrays
int length = java.lang.reflect.Array.getLength(param);
List<Object> list = new ArrayList<>(length);
for (int j = 0; j < length; j++) {
list.add(java.lang.reflect.Array.get(param, j));
}
c = list;
}
}

if (c != null) {
String value;
if (this == QueryClause.TAG_CONTAINING_ALL) {
value = c.stream().map(n -> "@" + field + ":{" + QueryUtils.escape(ObjectUtils.asString(n,
Expand All @@ -590,10 +608,35 @@ public String prepareQuery(String field, Object... params) {
.joining("|"));
prepared = prepared.replace(PARAM_PREFIX + i++, value);
} else if (this == QueryClause.NUMERIC_IN) {
value = c.stream().map(n -> "@" + field + ":[" + QueryUtils.escape(ObjectUtils.asString(n,
converter)) + " " + QueryUtils.escape(ObjectUtils.asString(n, converter)) + "]").collect(Collectors
.joining("|"));
prepared = value;
// For numeric values, don't escape - use toString() directly
if (c.isEmpty()) {
// Empty collection means no matches - return impossible query
prepared = prepared.replace(PARAM_PREFIX + i++, "[-1 -2]"); // Impossible range
} else if (c.size() == 1) {
// Single value - simple range query
Object val = c.iterator().next();
prepared = prepared.replace(PARAM_PREFIX + i++, "[" + val.toString() + " " + val.toString() + "]");
} else {
// Multiple values - need OR query with parentheses
value = "(" + c.stream().map(n -> "@" + field + ":[" + n.toString() + " " + n.toString() + "]").collect(
Collectors.joining("|")) + ")";
// Replace the entire prepared string with the OR query
prepared = value;
}
} else if (this == QueryClause.NUMERIC_NOT_IN) {
// For NUMERIC_NOT_IN, we need to exclude the values
// RediSearch doesn't have a direct NOT IN, so we use a negation pattern
if (c.isEmpty()) {
// Empty collection means match everything with this field
// Use the full numeric range query
prepared = "@" + field + ":[-inf inf]";
} else {
// Create a query that excludes specific values
// Replace the entire query with a combination of field existence and exclusions
value = "@" + field + ":[-inf inf] " + c.stream().map(n -> "-@" + field + ":[" + n.toString() + " " + n
.toString() + "]").collect(Collectors.joining(" "));
prepared = value;
}
} else if (this == QueryClause.NUMERIC_CONTAINING_ALL) {
value = c.stream().map(n -> "@" + field + ":[" + QueryUtils.escape(ObjectUtils.asString(n,
converter)) + " " + QueryUtils.escape(ObjectUtils.asString(n, converter)) + "]").collect(Collectors
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.redis.om.spring.fixtures.document.model;

import org.springframework.data.annotation.Id;

import com.redis.om.spring.annotations.Document;
import com.redis.om.spring.annotations.NumericIndexed;
import com.redis.om.spring.annotations.TagIndexed;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.NonNull;

@Data
@NoArgsConstructor
@RequiredArgsConstructor
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@Document
public class NumericIdTestEntity {
@Id
@NumericIndexed // Testing @NumericIndexed on @Id field
private Long id;

@NonNull
@TagIndexed
private String name;

@NonNull
@NumericIndexed
private Integer value;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.redis.om.spring.fixtures.document.model;

import org.springframework.data.annotation.Id;

import com.redis.om.spring.annotations.Document;
import com.redis.om.spring.annotations.NumericIndexed;
import com.redis.om.spring.annotations.TagIndexed;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.NonNull;

@Data
@NoArgsConstructor
@RequiredArgsConstructor
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@Document
public class NumericInTestEntity {
@Id
private String id;

@NonNull
@TagIndexed
private String name;

@NonNull
@NumericIndexed
private Integer age;

@NonNull
@NumericIndexed
private Long score;

@NonNull
@NumericIndexed
private Double rating;

@NumericIndexed
private Integer level;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.redis.om.spring.fixtures.document.repository;

import java.util.Collection;
import java.util.List;

import com.redis.om.spring.fixtures.document.model.NumericIdTestEntity;
import com.redis.om.spring.repository.RedisDocumentRepository;

public interface NumericIdTestEntityRepository extends RedisDocumentRepository<NumericIdTestEntity, Long> {
// Test querying by numeric ID
List<NumericIdTestEntity> findByIdIn(Collection<Long> ids);
List<NumericIdTestEntity> findByIdNotIn(Collection<Long> ids);

// Combined queries with numeric ID
List<NumericIdTestEntity> findByIdInAndName(Collection<Long> ids, String name);
List<NumericIdTestEntity> findByValueIn(Collection<Integer> values);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.redis.om.spring.fixtures.document.repository;

import java.util.Collection;
import java.util.List;
import java.util.Set;

import com.redis.om.spring.fixtures.document.model.NumericInTestEntity;
import com.redis.om.spring.repository.RedisDocumentRepository;

public interface NumericInTestEntityRepository extends RedisDocumentRepository<NumericInTestEntity, String> {
// Test methods for NUMERIC_IN
List<NumericInTestEntity> findByAgeIn(Collection<Integer> ages);
List<NumericInTestEntity> findByAgeIn(Set<Integer> ages);
List<NumericInTestEntity> findByAgeIn(List<Integer> ages);
List<NumericInTestEntity> findByAgeIn(Integer... ages);

// Test methods for Long type
List<NumericInTestEntity> findByScoreIn(Collection<Long> scores);
List<NumericInTestEntity> findByScoreIn(Long... scores);

// Test methods for Double type
List<NumericInTestEntity> findByRatingIn(Collection<Double> ratings);
List<NumericInTestEntity> findByRatingIn(Double... ratings);

// Test methods for NUMERIC_NOT_IN
List<NumericInTestEntity> findByAgeNotIn(Collection<Integer> ages);
List<NumericInTestEntity> findByScoreNotIn(Collection<Long> scores);
List<NumericInTestEntity> findByRatingNotIn(Collection<Double> ratings);

// Combined queries
List<NumericInTestEntity> findByAgeInAndScoreIn(Collection<Integer> ages, Collection<Long> scores);
List<NumericInTestEntity> findByNameAndAgeIn(String name, Collection<Integer> ages);

// Edge cases
List<NumericInTestEntity> findByLevelIn(Collection<Integer> levels); // nullable field
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package com.redis.om.spring.indexing;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;

import java.util.Arrays;
import java.util.List;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;

import com.redis.om.spring.AbstractBaseDocumentTest;
import com.redis.om.spring.fixtures.document.model.NumericIdTestEntity;
import com.redis.om.spring.fixtures.document.repository.NumericIdTestEntityRepository;

class NumericIndexedIdFieldTest extends AbstractBaseDocumentTest {

@Autowired
NumericIdTestEntityRepository repository;

private NumericIdTestEntity entity1;
private NumericIdTestEntity entity2;
private NumericIdTestEntity entity3;

@BeforeEach
void setUp() {
// Create test entities with numeric IDs
entity1 = new NumericIdTestEntity("Entity One", 100);
entity1.setId(1001L);

entity2 = new NumericIdTestEntity("Entity Two", 200);
entity2.setId(1002L);

entity3 = new NumericIdTestEntity("Entity Three", 100);
entity3.setId(1003L);

repository.saveAll(Arrays.asList(entity1, entity2, entity3));
}

@AfterEach
void tearDown() {
repository.deleteAll();
}

@Test
void testNumericIndexedIdFieldDoesNotCauseDuplicateSchema() {
// This test verifies that having @NumericIndexed on an @Id field
// doesn't cause duplicate schema field errors

// If the fix in RediSearchIndexer is working correctly,
// the entities should be saved and queryable without issues

// Verify entities were saved
assertThat(repository.count()).isEqualTo(3);

// Verify we can query by ID
assertThat(repository.findById(1001L)).isPresent();
assertThat(repository.findById(1002L)).isPresent();
assertThat(repository.findById(1003L)).isPresent();
}

@Test
void testFindByIdIn() {
List<Long> ids = Arrays.asList(1001L, 1003L);
List<NumericIdTestEntity> results = repository.findByIdIn(ids);

assertThat(results).hasSize(2);
assertThat(results).contains(entity1, entity3);
assertThat(results).doesNotContain(entity2);
}

@Test
void testFindByIdNotIn() {
List<Long> ids = Arrays.asList(1001L, 1002L);
List<NumericIdTestEntity> results = repository.findByIdNotIn(ids);

assertThat(results).hasSize(1);
assertThat(results).contains(entity3);
assertThat(results).doesNotContain(entity1, entity2);
}

@Test
void testFindByIdInAndName() {
List<Long> ids = Arrays.asList(1001L, 1002L, 1003L);
List<NumericIdTestEntity> results = repository.findByIdInAndName(ids, "Entity Two");

assertThat(results).hasSize(1);
assertThat(results).contains(entity2);
}

@Test
void testFindByValueIn() {
List<Integer> values = Arrays.asList(100, 300);
List<NumericIdTestEntity> results = repository.findByValueIn(values);

// entity1 and entity3 have value=100
assertThat(results).hasSize(2);
assertThat(results).contains(entity1, entity3);
assertThat(results).doesNotContain(entity2);
}

@Test
void testNumericIdWithLargeValues() {
// Test with larger ID values
NumericIdTestEntity largeIdEntity = new NumericIdTestEntity("Large ID Entity", 500);
largeIdEntity.setId(999999999L);
repository.save(largeIdEntity);

List<Long> ids = Arrays.asList(999999999L, 1001L);
List<NumericIdTestEntity> results = repository.findByIdIn(ids);

assertThat(results).hasSize(2);
assertThat(results).contains(largeIdEntity, entity1);

repository.delete(largeIdEntity);
}

@Test
void testNumericIdQueryPerformance() {
// Create more entities for performance testing
for (long i = 2000L; i < 2100L; i++) {
NumericIdTestEntity entity = new NumericIdTestEntity("Entity " + i, (int)(i % 10));
entity.setId(i);
repository.save(entity);
}

// Query with a large set of IDs
List<Long> ids = Arrays.asList(1001L, 1002L, 1003L, 2001L, 2050L, 2099L);
List<NumericIdTestEntity> results = repository.findByIdIn(ids);

assertThat(results).hasSize(6);

// Clean up additional entities
for (long i = 2000L; i < 2100L; i++) {
repository.deleteById(i);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,40 @@ void testValidDocumentNumericIndexedComplex(Results results) throws IOException
);
}

@Test
@Classpath(
"data.metamodel.ValidDocumentNumericIndexedId"
)
void testValidDocumentNumericIndexedId(Results results) throws IOException {
List<String> warnings = getWarningStrings(results);
assertThat(warnings).isEmpty();

List<String> errors = getErrorStrings(results);
assertThat(errors).isEmpty();

assertThat(results.generated).hasSize(1);
JavaFileObject metamodel = results.generated.get(0);
assertThat(metamodel.getName()).isEqualTo("/SOURCE_OUTPUT/valid/ValidDocumentNumericIndexedId$.java");

var fileContents = metamodel.getCharContent(true);

assertAll( //
// Test that @NumericIndexed on @Id field generates proper metamodel
// Note: ID field with @NumericIndexed is generated as TextTagField (this is the current behavior)
() -> assertThat(fileContents).contains("public static TextTagField<ValidDocumentNumericIndexedId, Long> ID;"),
() -> assertThat(fileContents).contains("public static TextTagField<ValidDocumentNumericIndexedId, String> NAME;"),
() -> assertThat(fileContents).contains("public static NumericField<ValidDocumentNumericIndexedId, Integer> VALUE;"),

// Test proper field initialization with SearchFieldAccessor
() -> assertThat(fileContents).contains(
"ID = new TextTagField<ValidDocumentNumericIndexedId, Long>(new SearchFieldAccessor(\"id\", \"$.id\", id),true);"),
() -> assertThat(fileContents).contains(
"NAME = new TextTagField<ValidDocumentNumericIndexedId, String>(new SearchFieldAccessor(\"name\", \"$.name\", name),true);"),
() -> assertThat(fileContents).contains(
"VALUE = new NumericField<ValidDocumentNumericIndexedId, Integer>(new SearchFieldAccessor(\"value\", \"$.value\", value),true);")
);
}

@Test
@Classpath(
"data.metamodel.ValidDocumentNumericIndexed"
Expand Down
Loading