diff --git a/langchain4j-infinispan/pom.xml b/langchain4j-infinispan/pom.xml
index acdb2766ff..944804857c 100644
--- a/langchain4j-infinispan/pom.xml
+++ b/langchain4j-infinispan/pom.xml
@@ -13,8 +13,8 @@
LangChain4j :: Integration :: Infinispan
- 16.0.6
- 6.0.6
+ 16.0.14
+ 6.0.7
@@ -26,6 +26,11 @@
pom
import
+
+ org.jboss.logging
+ jboss-logging
+ 3.6.3.Final
+
diff --git a/langchain4j-infinispan/src/main/java/dev/langchain4j/store/embedding/infinispan/InfinispanMetadataFilterMapper.java b/langchain4j-infinispan/src/main/java/dev/langchain4j/store/embedding/infinispan/InfinispanMetadataFilterMapper.java
index 8ff2cd5441..1629d3d63b 100644
--- a/langchain4j-infinispan/src/main/java/dev/langchain4j/store/embedding/infinispan/InfinispanMetadataFilterMapper.java
+++ b/langchain4j-infinispan/src/main/java/dev/langchain4j/store/embedding/infinispan/InfinispanMetadataFilterMapper.java
@@ -14,7 +14,6 @@ import dev.langchain4j.store.embedding.filter.logical.And;
import dev.langchain4j.store.embedding.filter.logical.Not;
import dev.langchain4j.store.embedding.filter.logical.Or;
import java.util.Collection;
-import java.util.Optional;
import java.util.stream.Collectors;
/**
@@ -107,48 +106,38 @@ class InfinispanMetadataFilterMapper {
}
private String mapIn(IsIn filter) {
- Optional> first = filter.comparisonValues().stream().findFirst();
- if (first.isEmpty()) {
- throw new UnsupportedOperationException("Infinispan metadata filter IN must contain values");
- }
- Object o = first.get();
- String inStatement = formattedComparisonValues(filter.comparisonValues(), o instanceof Number);
- String m = "m" + i + ".";
- String filterQuery = m + "value IN (" + inStatement + ")";
- if (o instanceof Integer || o instanceof Long) {
- filterQuery = m + "value_int IN (" + inStatement + ")";
- } else if (o instanceof Float || o instanceof Double) {
- filterQuery = m + "value_float IN (" + inStatement + ")";
- }
+ ensureNotEmpty(filter.comparisonValues());
+ String column = "m" + i + "." + valueColumn(filter.comparisonValues());
+ String inStatement = formattedComparisonValues(filter.comparisonValues());
- return metadataKey(filter.key()) + filterQuery;
+ return metadataKey(filter.key()) + column + " IN (" + inStatement + ")";
}
private String mapNotIn(IsNotIn filter) {
- Optional> first = filter.comparisonValues().stream().findFirst();
- if (first.isEmpty()) {
+ ensureNotEmpty(filter.comparisonValues());
+ String m = "m" + i + ".";
+ String column = m + valueColumn(filter.comparisonValues());
+ String inStatement = formattedComparisonValues(filter.comparisonValues());
+
+ return "(" + column + " NOT IN (" + inStatement + ")" + metadataKeyLast(filter.key()) + ") " + "OR ("
+ + column + " IN (" + inStatement + ") and " + m + "name!='" + escape(filter.key()) + "')"
+ + addMetadataNullCheck();
+ }
+
+ private void ensureNotEmpty(Collection> comparisonValues) {
+ if (comparisonValues.isEmpty()) {
throw new UnsupportedOperationException("Infinispan metadata filter IN must contain values");
}
- Object o = first.get();
- String inStatement = formattedComparisonValues(filter.comparisonValues(), o instanceof Number);
- String m = "m" + i + ".";
- String filterQuery = m + "value NOT IN (" + inStatement + ")";
- if (o instanceof Integer || o instanceof Long) {
- filterQuery = m + "value_int NOT IN (" + inStatement + ")";
- } else if (o instanceof Float || o instanceof Double) {
- filterQuery = m + "value_float NOT IN (" + inStatement + ")";
- }
+ }
- String inFilterQuery = m + "value IN (" + inStatement + ")";
- if (o instanceof Integer || o instanceof Long) {
- inFilterQuery = m + "value_int IN (" + inStatement + ")";
- } else if (o instanceof Float || o instanceof Double) {
- inFilterQuery = m + "value_float IN (" + inStatement + ")";
+ private String valueColumn(Collection> comparisonValues) {
+ if (comparisonValues.stream().anyMatch(v -> v instanceof Float || v instanceof Double)) {
+ return "value_float";
}
-
- return "(" + filterQuery + metadataKeyLast(filter.key()) + ") " + "OR ("
- + inFilterQuery + " and " + m + "name!='" + escape(filter.key()) + "')"
- + addMetadataNullCheck();
+ if (comparisonValues.stream().allMatch(v -> v instanceof Integer || v instanceof Long)) {
+ return "value_int";
+ }
+ return "value";
}
private String computeFilter(String operator, Object value) {
@@ -198,14 +187,30 @@ class InfinispanMetadataFilterMapper {
return "m" + i + ".name='" + escape(key) + "' and ";
}
- private String formattedComparisonValues(Collection> comparisonValues, boolean isNumeric) {
- String inStatement = comparisonValues.stream()
- .map(s -> isNumeric ? s.toString() : "'" + escape(String.valueOf(s)) + "'")
+ private String formattedComparisonValues(Collection> comparisonValues) {
+ boolean hasNumeric = comparisonValues.stream().anyMatch(v -> v instanceof Number);
+ boolean hasNonNumeric = comparisonValues.stream().anyMatch(v -> !(v instanceof Number));
+ if (hasNumeric && hasNonNumeric) {
+ throw new IllegalArgumentException(
+ "Infinispan metadata filter IN/NOT IN cannot mix numeric and non-numeric values");
+ }
+ boolean asFloat = "value_float".equals(valueColumn(comparisonValues));
+ return comparisonValues.stream()
+ .map(s -> formattedComparisonValue(s, asFloat))
.collect(Collectors.joining(", "));
- return inStatement;
+ }
+
+ private String formattedComparisonValue(Object value, boolean asFloat) {
+ if (!(value instanceof Number)) {
+ return "'" + escape(String.valueOf(value)) + "'";
+ }
+ if (asFloat && (value instanceof Integer || value instanceof Long)) {
+ return String.valueOf(((Number) value).doubleValue());
+ }
+ return value.toString();
}
private static String escape(String s) {
- return s.replace("'", "''");
+ return s.replace("\\", "\\\\").replace("'", "''");
}
}
diff --git a/langchain4j-infinispan/src/test/java/dev/langchain4j/store/embedding/infinispan/InfinispanMetadataFilterMapperTest.java b/langchain4j-infinispan/src/test/java/dev/langchain4j/store/embedding/infinispan/InfinispanMetadataFilterMapperTest.java
index a804cc7dce..249a82c691 100644
--- a/langchain4j-infinispan/src/test/java/dev/langchain4j/store/embedding/infinispan/InfinispanMetadataFilterMapperTest.java
+++ b/langchain4j-infinispan/src/test/java/dev/langchain4j/store/embedding/infinispan/InfinispanMetadataFilterMapperTest.java
@@ -369,15 +369,68 @@ class InfinispanMetadataFilterMapperTest {
@Test
void should_handle_mixed_numeric_types_in_in_filter() {
- // given
+ // given — all values are Number (different subtypes), no mixing with strings
Filter filter = new IsIn("mixed", Arrays.asList(1, 2L, 3.0f, 4.0));
// when
InfinispanMetadataFilterMapper.FilterResult result = mapper.map(filter);
+ // then — presence of a floating point value selects value_float for the whole list,
+ // and integral values are widened to match the column
+ assertThat(result.query).isEqualTo("m0.name='mixed' and m0.value_float IN (3.0, 4.0, 1.0, 2.0)");
+ }
+
+ @ParameterizedTest
+ @MethodSource("numericInColumnSelection")
+ void should_select_in_column_independently_of_iteration_order(List> values, String expectedColumn) {
+ // when
+ InfinispanMetadataFilterMapper.FilterResult result = mapper.map(new IsIn("n", values));
+
// then
- // Should use the type of the first element (Integer in this case)
- assertThat(result.query).isEqualTo("m0.name='mixed' and m0.value_float IN (3.0, 4.0, 1, 2)");
+ assertThat(result.query).startsWith("m0.name='n' and m0." + expectedColumn + " IN (");
+ }
+
+ static List numericInColumnSelection() {
+ return Arrays.asList(
+ Arguments.of(Arrays.asList(1, 2, 3), "value_int"),
+ Arguments.of(Arrays.asList(1, 2L), "value_int"),
+ Arguments.of(Arrays.asList(1.5, 2.5), "value_float"),
+ // these two pairs hash into opposite iteration orders; both must pick value_float
+ Arguments.of(Arrays.asList(1, 2.5), "value_float"),
+ Arguments.of(Arrays.asList(3, 0.5), "value_float"),
+ Arguments.of(Arrays.asList("a", "b"), "value"));
+ }
+
+ @Test
+ void should_select_not_in_column_independently_of_iteration_order() {
+ // when
+ InfinispanMetadataFilterMapper.FilterResult result = mapper.map(new IsNotIn("n", Arrays.asList(1, 2.5)));
+
+ // then — both occurrences of the column must agree
+ assertThat(result.query).contains("m0.value_float NOT IN (").contains("m0.value_float IN (");
+ assertThat(result.query).doesNotContain("value_int");
+ }
+
+ @Test
+ void should_reject_mixed_numeric_and_string_in_filter() {
+ // given — mix of Number and String triggers rejection regardless of iteration order
+ Filter filter = new IsIn("key", Arrays.asList(1, "x' OR 1=1 --"));
+
+ // when & then
+ assertThatThrownBy(() -> mapper.map(filter))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("cannot mix numeric and non-numeric values");
+ }
+
+ @Test
+ void should_reject_mixed_numeric_and_string_not_in_filter() {
+ // given
+ Filter filter = new IsNotIn("key", Arrays.asList(42, ""));
+
+ // when & then
+ assertThatThrownBy(() -> mapper.map(filter))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("cannot mix numeric and non-numeric values");
}
@Test
@@ -441,6 +494,90 @@ class InfinispanMetadataFilterMapperTest {
assertThat(result.query).contains("OR (i.metadata is null)");
}
+ @Test
+ void should_escape_ickle_injection_in_key() {
+ // given
+ Filter filter = new IsEqualTo("foo' OR 1=1 OR name='", "bar");
+
+ // when
+ InfinispanMetadataFilterMapper.FilterResult result = mapper.map(filter);
+
+ // then
+ assertThat(result.query).isEqualTo("m0.name='foo'' OR 1=1 OR name=''' and m0.value = 'bar'");
+ }
+
+ @Test
+ void should_escape_ickle_injection_in_value() {
+ // given
+ Filter filter = new IsEqualTo("name", "x' OR 1=1 --");
+
+ // when
+ InfinispanMetadataFilterMapper.FilterResult result = mapper.map(filter);
+
+ // then
+ assertThat(result.query).isEqualTo("m0.name='name' and m0.value = 'x'' OR 1=1 --'");
+ }
+
+ @Test
+ void should_escape_ickle_injection_in_not_equal_key() {
+ // given
+ Filter filter = new IsNotEqualTo("a' OR 1=1 --", "val");
+
+ // when
+ InfinispanMetadataFilterMapper.FilterResult result = mapper.map(filter);
+
+ // then
+ assertThat(result.query).contains("m0.name='a'' OR 1=1 --'");
+ }
+
+ @Test
+ void should_escape_ickle_injection_in_in_key() {
+ // given
+ Filter filter = new IsIn("k' OR 1=1 --", Arrays.asList("a", "b"));
+
+ // when
+ InfinispanMetadataFilterMapper.FilterResult result = mapper.map(filter);
+
+ // then
+ assertThat(result.query).contains("m0.name='k'' OR 1=1 --'");
+ }
+
+ @Test
+ void should_escape_ickle_injection_in_in_values() {
+ // given
+ Filter filter = new IsIn("key", Arrays.asList("x' OR 1=1 --", "normal"));
+
+ // when
+ InfinispanMetadataFilterMapper.FilterResult result = mapper.map(filter);
+
+ // then
+ assertThat(result.query).contains("'x'' OR 1=1 --'");
+ }
+
+ @Test
+ void should_escape_ickle_injection_in_not_in_key() {
+ // given
+ Filter filter = new IsNotIn("k' OR 1=1 --", Arrays.asList("a"));
+
+ // when
+ InfinispanMetadataFilterMapper.FilterResult result = mapper.map(filter);
+
+ // then
+ assertThat(result.query).contains("m0.name='k'' OR 1=1 --'").contains("m0.name!='k'' OR 1=1 --'");
+ }
+
+ @Test
+ void should_escape_backslash_in_value() {
+ // given
+ Filter filter = new IsEqualTo("path", "C:\\Users\\test");
+
+ // when
+ InfinispanMetadataFilterMapper.FilterResult result = mapper.map(filter);
+
+ // then
+ assertThat(result.query).isEqualTo("m0.name='path' and m0.value = 'C:\\\\Users\\\\test'");
+ }
+
@Test
void should_generate_correct_metadata_aliases_for_nested_filters() {
// given