diff --git a/build.gradle b/build.gradle index 891bdf4de..ac2e7a3e5 100644 --- a/build.gradle +++ b/build.gradle @@ -131,6 +131,6 @@ apply from: "licenses-source-header.gradle" ext { publicDir = "${project.rootDir}" - neo4jVersionEffective = project.hasProperty("neo4jVersionOverride") ? project.getProperty("neo4jVersionOverride") : "5.8.0" + neo4jVersionEffective = project.hasProperty("neo4jVersionOverride") ? project.getProperty("neo4jVersionOverride") : "5.9.0" testContainersVersion = '1.17.6' } diff --git a/common/src/main/java/apoc/util/Util.java b/common/src/main/java/apoc/util/Util.java index 29b65d34e..8abf529b9 100644 --- a/common/src/main/java/apoc/util/Util.java +++ b/common/src/main/java/apoc/util/Util.java @@ -26,6 +26,7 @@ import apoc.result.VirtualNode; import apoc.result.VirtualRelationship; import apoc.util.collection.Iterators; +import org.apache.commons.collections4.ListUtils; import org.apache.commons.compress.archivers.ArchiveEntry; import org.apache.commons.compress.archivers.ArchiveInputStream; import org.apache.commons.io.IOUtils; @@ -86,7 +87,6 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Function; -import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -1113,34 +1113,20 @@ public static boolean valueEquals(T one, T other) { .equals(ValueUtils.of(other)); } - // todo - parameterized??? - public static List removeAll(Collection remove, List second) { - return gettStream(remove, second) - .collect(Collectors.toList()); - } - - public static Set removeAll(Collection remove, Set second) { - return gettStream(remove, second) - .collect(Collectors.toSet()); - } - - - public static boolean isaBoolean(Collection second, Object i) { - return second.stream().anyMatch(i2 -> Util.valueEquals(i, i2)); + public static boolean containsValueEquals(Collection collection, Object value) { + return collection.stream() + .anyMatch(i -> Util.valueEquals(value, i)); } - private static Stream gettStream(Collection remove, Collection second) { - return remove.stream() - .filter(i -> isNoneMatch(second, i)); - } - - private static boolean isNoneMatch(Collection second, T i) { - return second.stream().noneMatch(i2 -> Util.valueEquals(i, i2)); - } - - public static List toAnyValues(List second) { - return second.stream() + public static List toAnyValues(List list) { + return list.stream() .map(ValueUtils::of) .collect(Collectors.toList()); } + + public static int indexOf(List list, Object value) { + return ListUtils.indexOf(list, + (i) -> Util.valueEquals(i, value) + ); + } } diff --git a/core/src/main/java/apoc/coll/Coll.java b/core/src/main/java/apoc/coll/Coll.java index 4919aa375..9a12149cf 100644 --- a/core/src/main/java/apoc/coll/Coll.java +++ b/core/src/main/java/apoc/coll/Coll.java @@ -21,7 +21,6 @@ import apoc.result.ListResult; import apoc.util.Util; import com.google.common.util.concurrent.AtomicDouble; -import org.apache.commons.collections4.ListUtils; import org.apache.commons.lang3.mutable.MutableInt; import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation; @@ -31,7 +30,6 @@ import org.neo4j.graphdb.Relationship; import org.neo4j.graphdb.Result; import org.neo4j.graphdb.Transaction; -import org.neo4j.kernel.impl.util.ValueUtils; import org.neo4j.procedure.Context; import org.neo4j.procedure.Description; import org.neo4j.procedure.Name; @@ -41,7 +39,20 @@ import java.lang.reflect.Array; import java.text.Collator; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.ListIterator; +import java.util.Locale; +import java.util.Map; +import java.util.Random; +import java.util.RandomAccess; +import java.util.Set; import java.util.concurrent.ThreadLocalRandom; import java.util.function.BiFunction; import java.util.function.Function; @@ -49,7 +60,7 @@ import java.util.stream.IntStream; import java.util.stream.Stream; -import static apoc.util.Util.isaBoolean; +import static apoc.util.Util.containsValueEquals; import static apoc.util.Util.toAnyValues; import static java.util.Arrays.asList; @@ -382,12 +393,12 @@ public Stream split(@Name("coll") List list, @Name("value") if (list==null || list.isEmpty()) return Stream.empty(); List l = new ArrayList<>(list); List> result = new ArrayList<>(10); - int idx = extracted(l, value); + int idx = Util.indexOf(l, value); while (idx != -1) { List subList = l.subList(0, idx); if (!subList.isEmpty()) result.add(subList); l = l.subList(idx+1,l.size()); - idx = extracted(l, value);// l.indexOf(value); + idx = Util.indexOf(l, value); } if (!l.isEmpty()) result.add(l); return result.stream().map(ListResult::new); @@ -464,35 +475,17 @@ public List remove(@Name("coll") List coll, @Name("index") long public long indexOf(@Name("coll") List coll, @Name("value") Object value) { // return reduce(res=[0,-1], x in $list | CASE WHEN x=$value AND res[1]=-1 THEN [res[0], res[0]+1] ELSE [res[0]+1, res[1]] END)[1] as value if (coll == null || coll.isEmpty()) return -1; - return new ArrayList<>(toAnyValues(coll)).indexOf(ValueUtils.of(value)); - } -// @UserFunction("apoc.coll.indexOf") -// @Description("Returns the index for the first occurrence of the specified value in the list.") -// public long indexOf(@Name("coll") List coll, @Name("value") Object value) { -// // return reduce(res=[0,-1], x in $list | CASE WHEN x=$value AND res[1]=-1 THEN [res[0], res[0]+1] ELSE [res[0]+1, res[1]] END)[1] as value -// return extracted(coll, value); -// } - - private int extracted(List coll, Object value) { - return ListUtils.indexOf(coll, - (i) -> Util.valueEquals(i, value)// ValueUtils.of(i).equals(ValueUtils.of(value)) - ); + return Util.indexOf(coll, value); } @UserFunction("apoc.coll.containsAll") @Description("Returns whether or not all of the given values exist in the given collection (using a HashSet).") public boolean containsAll(@Name("coll1") List coll, @Name("coll2") List values) { if (coll == null || coll.isEmpty() || values == null) return false; - // todo - forse anche questo?? -// CollectionUtils.containsAll(coll, values, null) Set objects = new HashSet<>(coll); -// values.stream().allMatch(i -> objects.removeIf(i1 -> i1.equals())) return values.stream() - .allMatch( i -> isaBoolean(objects, i)); -// .allMatch( i -> isaBoolean(objects, i) objects.stream().anyMatch(i1 -> Util.valueEquals(i, i1)/* i1.equals(i)*/) ); -// .containsAll(values); -// return new HashSet<>(coll).containsAll(values); + .allMatch( i -> containsValueEquals(objects, i)); } @UserFunction("apoc.coll.containsSorted") @@ -622,14 +615,6 @@ public List removeAll(@Name("list1") List first, @Name("list2") if (second!=null) list.removeAll(toAnyValues(second)); return list; } -// @UserFunction("apoc.coll.removeAll") -// @Description("Returns the first list with all elements of the second list removed.") -// public List removeAll(@Name("list1") List first, @Name("list2") List second) { -// if (first == null) return null; -// List list = new ArrayList<>(first); -// if (second!=null) list = Util.removeAll(list, second); -// return list; -// } @UserFunction("apoc.coll.subtract") @Description("Returns the first list as a set with all the elements of the second list removed.") public List subtract(@Name("list1") List first, @Name("list2") List second) { @@ -643,8 +628,8 @@ public List subtract(@Name("list1") List first, @Name("list2") L @Description("Returns the distinct intersection of two lists.") public List intersection(@Name("list1") List first, @Name("list2") List second) { if (first == null || second == null) return Collections.emptyList(); - Set set =first.stream().filter(i -> isaBoolean(second, i)) - .collect(Collectors.toSet()); + Set set = new HashSet<>(first); + set.retainAll(second); return new SetBackedList(set); } diff --git a/core/src/test/java/apoc/coll/CollTest.java b/core/src/test/java/apoc/coll/CollTest.java index 3fdc89e9d..f21f43813 100644 --- a/core/src/test/java/apoc/coll/CollTest.java +++ b/core/src/test/java/apoc/coll/CollTest.java @@ -20,6 +20,7 @@ import apoc.convert.Json; import apoc.util.TestUtil; +import org.junit.After; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.ClassRule; @@ -40,12 +41,19 @@ import static org.junit.Assert.*; public class CollTest { + // query that procedures a list, + // with both entity types, via collect(..), and hardcoded one + private static final String QUERY_WITH_MIXED_TYPES = "MATCH (n:Test) " + + "WITH n ORDER BY n.a " + + "WITH COLLECT({something: n.something}) + { something: [] } + {something: 'alpha'} + {something: [1,2,3]} AS collection \n"; + private static final String QUERY_WITH_ARRAY = "CREATE (:Test {a: 1, something: 'alpha' }), " + "(:Test { a: 2, something: [] }), " + - "(:Test { a: 3, something: 'beta' })"; - private static final String sss = "MATCH (n:Test) " + - "WITH n ORDER BY n.a " + - "WITH COLLECT({something: n.something}) as collection \n"; + "(:Test { a: 3, something: 'beta' })," + + "(:Test { a: 4, something: [1,2,3] })"; + + public static final Map MAP_WITH_ALPHA = Map.of("something", "alpha"); + public static final Map MAP_WITH_BETA = Map.of("something", "beta"); @ClassRule public static DbmsRule db = new ImpermanentDbmsRule(); @@ -60,6 +68,11 @@ public static void teardown() { db.shutdown(); } + @After + public void after() { + db.executeTransactionally("MATCH (n) DETACH DELETE n"); + } + @Test public void testRunningTotal() { testCall(db, "RETURN apoc.coll.runningTotal([1,2,3,4,5.5,1]) as value", @@ -211,13 +224,8 @@ public void testIndexOf() { @Test public void testIndexOfWithCollections() { - db.executeTransactionally("CREATE (:Test { someArray: [true, false, true] }), (:Test { someArray: [] })"); - testCall(db, """ - MATCH (n:Test) - WITH COLLECT({ - someArray: n.someArray - }) as collection - RETURN apoc.coll.indexOf(collection, { someArray: [] }) AS index""", + db.executeTransactionally(QUERY_WITH_ARRAY); + testCall(db, QUERY_WITH_MIXED_TYPES + "RETURN apoc.coll.indexOf(collection, { something: [] }) AS index", r -> { long index = (long) r.get("index"); assertTrue("Actual index is " + index, @@ -253,68 +261,59 @@ public void testSplit() { } @Test - public void testSplitOfWithCollections() { + public void testSplitOfWithBothHardcodedAndEntityTypes() { db.executeTransactionally(QUERY_WITH_ARRAY); - testResult(db, """ - MATCH (n:Test) - WITH n ORDER BY n.a - WITH COLLECT({ - something: n.something - }) as collection - CALL apoc.coll.split(collection, { something: [] }) YIELD value RETURN value""", + testResult(db, QUERY_WITH_MIXED_TYPES + "CALL apoc.coll.split(collection, { something: [] }) YIELD value RETURN value", r -> { Map row = r.next(); List> value = (List>) row.get("value"); - assertEquals(List.of(Map.of("something", "alpha")), value); + assertEquals(List.of(MAP_WITH_ALPHA), value); row = r.next(); value = (List>) row.get("value"); - assertEquals(List.of(Map.of("something", "beta")), value); + assertEquals(2, value.size()); + assertEquals(MAP_WITH_BETA, value.get(0)); + // in this case the `[1,2,3]` in `{ something: [1,2,3] }` is an array + assertMapWithNumericArray(value.get(1)); + + row = r.next(); + value = (List>) row.get("value"); + + assertEquals(2, value.size()); + assertEquals(MAP_WITH_ALPHA, value.get(0)); + // in this case the `[1,2,3]` in `{ something: [1,2,3] }` is an ArrayList + assertEquals(Map.of("something", List.of(1L,2L,3L)), value.get(1)); assertFalse(r.hasNext()); }); } @Test - public void testCollToSet() { - db.executeTransactionally("CREATE (:Test {a: 1, something: 'alpha' }), " + - "(:Test { a: 2, something: [] }), " + - "(:Test { a: 1, something: 'alpha' })"); - - testCall(db, """ - MATCH (n:Test) - WITH n ORDER BY n.a - WITH COLLECT({ - something: n.something - }) + { something: [] } AS collection - RETURN apoc.coll.toSet(collection) AS value""", + public void testRemoveWithBothHardcodedAndEntityTypes() { + db.executeTransactionally(QUERY_WITH_ARRAY); + testCall(db, QUERY_WITH_MIXED_TYPES + "RETURN apoc.coll.removeAll(collection, [{ something: [] }, { something: 'alpha' }]) AS value", row -> { List> value = (List>) row.get("value"); - assertEquals(List.of(Map.of("something", "alpha")), value); + assertEquals(3, value.size()); + assertEquals(MAP_WITH_BETA, value.get(0)); + // in this case the `[1,2,3]` in `{ something: [1,2,3] }` is an array + assertMapWithNumericArray(value.get(1)); + // in this case the `[1,2,3]` in `{ something: [1,2,3] }` is an ArrayList + assertEquals(Map.of("something", List.of(1L,2L,3L)), value.get(2)); }); } @Test - public void testSplitOfWithCollections1() { - db.executeTransactionally("CREATE (:Test {a: 1, something: [1,2,3] }), " + - "(:Test { a: 2, something: [1,2,2] }), " + - "(:Test { a: 4, something: [1,2,4] }), "); + public void testCollToSetWithBothHardcodedAndEntityTypes() { + db.executeTransactionally(QUERY_WITH_ARRAY); - testResult(db, """ - MATCH (n:Test) - WITH n ORDER BY n.a - WITH COLLECT({ - something: n.something - }) as collection - RETURN apoc.coll.sortMaps(collection, 'something') AS value""", - r -> { - Map row = r.next(); + testCall(db, QUERY_WITH_MIXED_TYPES + "RETURN apoc.coll.toSet(collection) AS value", + row -> { List> value = (List>) row.get("value"); - assertEquals(List.of(Map.of("something", "alpha")), value); - row = r.next(); - value = (List>) row.get("value"); - assertEquals(List.of(Map.of("something", "beta")), value); - - assertFalse(r.hasNext()); + assertEquals(4, value.size()); + assertEquals(MAP_WITH_ALPHA, value.get(0)); + assertMapWithEmptyArray(value.get(1)); + assertEquals(MAP_WITH_BETA, value.get(2)); + assertMapWithNumericArray(value.get(3)); }); } @@ -378,49 +377,23 @@ public void testContainsAll() { testCall(db, "RETURN apoc.coll.containsAll([1,2,3],null) AS value", (res) -> assertEquals(false, res.get("value"))); } - // todo - test names - @Test public void testContainsAllOfWithCollections() { db.executeTransactionally(QUERY_WITH_ARRAY); - testResult(db, """ - MATCH (n:Test) - WITH n ORDER BY n.a - WITH COLLECT({ - something: n.something - }) as collection - RETURN apoc.coll.containsAll(collection, [{ something: [] }]) AS value""", - r -> { - Map row = r.next(); - boolean value = (boolean) row.get("value"); - assertEquals(true, value); -// assertEquals(List.of(Map.of("something", "alpha")), value); -// row = r.next(); -// value = (List>) row.get("value"); -// assertEquals(List.of(Map.of("something", "beta")), value); - - assertFalse(r.hasNext()); - }); + testCall(db, QUERY_WITH_MIXED_TYPES + "RETURN apoc.coll.containsAll(collection, [{ something: [] }]) AS value", + row -> assertTrue( (boolean) row.get("value") ) + ); } - @Test - public void testContainsAllOfWithCollections1() { - db.executeTransactionally(QUERY_WITH_ARRAY); - - testCall(db, sss + "RETURN apoc.coll.intersection(collection, [{ something: [] }]) AS value", - row -> { - List value = ((List) row.get("value")); - assertMapWithEmptyArray(value); - }); + private static void assertMapWithEmptyArray(Map map) { + assertEquals(1, map.size()); + assertArrayEquals(new String[]{}, (String[]) map.get("something")); } - private static void assertMapWithEmptyArray(List value) { - assertEquals(1, value.size()); - Map map = value.get(0); + private static void assertMapWithNumericArray(Map map) { assertEquals(1, map.size()); - - assertArrayEquals(new String[]{}, (String[]) map.get("something")); + assertArrayEquals(new long[] {1,2,3}, (long[]) map.get("something")); } @Test @@ -575,7 +548,6 @@ public void testSortMapsCountReverse() { }); } - // todo - these?? @Test public void testSetOperations() { testCall(db, "RETURN apoc.coll.union([1,2],[3,2]) AS value", r -> assertEquals(asSet(asList(1L, 2L, 3L)), asSet((Iterable) r.get("value")))); @@ -588,42 +560,6 @@ public void testSetOperations() { testCall(db, "RETURN apoc.coll.removeAll([1,2],[3,2]) AS value", r -> assertEquals(asList(1L), r.get("value"))); } - @Test - public void testSubtractWithArray() { - db.executeTransactionally(QUERY_WITH_ARRAY); - - testCall(db, """ - MATCH (n:Test) - WITH n ORDER BY n.a - WITH COLLECT({ - something: n.something - }) as collection - RETURN apoc.coll.subtract(collection, [{ something: [] }]) AS value""", - row -> { - List value = (List) row.get("value"); - List> expected = List.of(Map.of("something", "alpha"), Map.of("something", "beta")); - assertEquals(expected, value); - }); - } - - @Test - public void testRemoveAllWithArray() { - db.executeTransactionally(QUERY_WITH_ARRAY); - - testCall(db, """ - MATCH (n:Test) - WITH n ORDER BY n.a - WITH COLLECT({ - something: n.something - }) as collection - RETURN apoc.coll.removeAll(collection, [{ something: [] }]) AS value""", - row -> { - List value = (List) row.get("value"); - List> expected = List.of(Map.of("something", "alpha"), Map.of("something", "beta")); - assertEquals(expected, value); - }); - } - @Test public void testIntersectionWithJsonMap(){ testCall(db, "WITH apoc.convert.fromJsonMap('{\"numbers\":[1,2]}') as set1, [2,3] as set2\n" +