You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by su...@apache.org on 2020/10/04 15:59:37 UTC

[groovy] branch GROOVY-8258 created (now fed55e0)

This is an automated email from the ASF dual-hosted git repository.

sunlan pushed a change to branch GROOVY-8258
in repository https://gitbox.apache.org/repos/asf/groovy.git.


      at fed55e0  GROOVY-8258: Implement the built-in collection LINQ provider

This branch includes the following new commits:

     new fed55e0  GROOVY-8258: Implement the built-in collection LINQ provider

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[groovy] 01/01: GROOVY-8258: Implement the built-in collection LINQ provider

Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

sunlan pushed a commit to branch GROOVY-8258
in repository https://gitbox.apache.org/repos/asf/groovy.git

commit fed55e09986dbab92f269e598622d90d01e54d49
Author: Daniel Sun <su...@apache.org>
AuthorDate: Sun Oct 4 23:54:31 2020 +0800

    GROOVY-8258: Implement the built-in collection LINQ provider
---
 settings.gradle                                    |   1 +
 subprojects/groovy-linq/build.gradle               |  24 ++
 .../java/org/apache/groovy/linq/Queryable.java     | 124 +++++++
 .../groovy/linq/provider/QueryableCollection.java  | 244 +++++++++++++
 .../linq/provider/QueryableCollectionTest.groovy   | 403 +++++++++++++++++++++
 5 files changed, 796 insertions(+)

diff --git a/settings.gradle b/settings.gradle
index b8a3622..3aca670 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -45,6 +45,7 @@ def subprojects = ['groovy-ant',
         'groovy-jmx',
         'groovy-json',
         'groovy-jsr223',
+        'groovy-linq',
         'groovy-macro',
         'groovy-macro-library',
         'groovy-nio',
diff --git a/subprojects/groovy-linq/build.gradle b/subprojects/groovy-linq/build.gradle
new file mode 100644
index 0000000..8e249dd
--- /dev/null
+++ b/subprojects/groovy-linq/build.gradle
@@ -0,0 +1,24 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+
+dependencies {
+    api rootProject
+    testImplementation project(':groovy-test')
+}
diff --git a/subprojects/groovy-linq/src/main/java/org/apache/groovy/linq/Queryable.java b/subprojects/groovy-linq/src/main/java/org/apache/groovy/linq/Queryable.java
new file mode 100644
index 0000000..95d67f9
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/java/org/apache/groovy/linq/Queryable.java
@@ -0,0 +1,124 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.groovy.linq;
+
+import groovy.lang.Tuple2;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.BiPredicate;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+
+/**
+ * Represents the queryable objects, e.g. Java collections
+ *
+ * @param <T> the type of Queryable element
+ * @since 4.0.0
+ */
+public interface Queryable<T> {
+    <U> Queryable<Tuple2<T, U>> innerJoin(Queryable<? extends U> queryable, BiPredicate<? super T, ? super U> joiner);
+
+    <U> Queryable<Tuple2<T, U>> leftJoin(Queryable<? extends U> queryable, BiPredicate<? super T, ? super U> joiner);
+
+    <U> Queryable<Tuple2<T, U>> rightJoin(Queryable<? extends U> queryable, BiPredicate<? super T, ? super U> joiner);
+
+    default <U> Queryable<Tuple2<T, U>> fullJoin(Queryable<? extends U> queryable, BiPredicate<? super T, ? super U> joiner) {
+        Queryable<Tuple2<T, U>> lj = this.leftJoin(queryable, joiner);
+        Queryable<Tuple2<T, U>> rj = this.rightJoin(queryable, joiner);
+        return lj.union(rj);
+    }
+
+    <U> Queryable<Tuple2<T, U>> crossJoin(Queryable<? extends U> queryable);
+
+    Queryable<T> where(Predicate<? super T> filter);
+
+    <K> Queryable<Tuple2<K, Queryable<T>>> groupBy(Function<? super T, ? extends K> classifier, BiPredicate<? super K, ? super Queryable<? extends T>> having);
+
+    default <K> Queryable<Tuple2<K, Queryable<T>>> groupBy(Function<? super T, ? extends K> classifier) {
+        return groupBy(classifier, (k, l) -> true);
+    }
+
+    <U extends Comparable<? super U>> Queryable<T> orderBy(Order<? super T, ? extends U>... orders);
+
+    Queryable<T> limit(int offset, int size);
+
+    default Queryable<T> limit(int size) {
+        return limit(0, size);
+    }
+
+    <U> Queryable<U> select(Function<? super T, ? extends U> mapper);
+
+    Queryable<T> distinct();
+
+    default Queryable<T> union(Queryable<? extends T> queryable) {
+        return this.unionAll(queryable).distinct();
+    }
+
+    Queryable<T> unionAll(Queryable<? extends T> queryable);
+
+    Queryable<T> intersect(Queryable<? extends T> queryable);
+
+    Queryable<T> minus(Queryable<? extends T> queryable);
+
+    List<T> toList();
+
+    default Stream<T> stream() {
+        return toList().stream();
+    }
+
+    //  Built-in aggregate functions {
+    int count();
+    BigDecimal sum(Function<? super T, BigDecimal> mapper);
+    // } Built-in aggregate functions
+
+    class Order<T, U extends Comparable<? super U>> {
+        private final Function<? super T, ? extends U> keyExtractor;
+        private final boolean asc;
+
+        public Order(Function<? super T, ? extends U> keyExtractor, boolean asc) {
+            this.keyExtractor = keyExtractor;
+            this.asc = asc;
+        }
+
+        public Function<? super T, ? extends U> getKeyExtractor() {
+            return keyExtractor;
+        }
+
+        public boolean isAsc() {
+            return asc;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (!(o instanceof Order)) return false;
+            Order<?, ?> order = (Order<?, ?>) o;
+            return asc == order.asc &&
+                    keyExtractor.equals(order.keyExtractor);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(keyExtractor, asc);
+        }
+    }
+}
diff --git a/subprojects/groovy-linq/src/main/java/org/apache/groovy/linq/provider/QueryableCollection.java b/subprojects/groovy-linq/src/main/java/org/apache/groovy/linq/provider/QueryableCollection.java
new file mode 100644
index 0000000..282e68b
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/java/org/apache/groovy/linq/provider/QueryableCollection.java
@@ -0,0 +1,244 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.groovy.linq.provider;
+
+import groovy.lang.Tuple;
+import groovy.lang.Tuple2;
+import org.apache.groovy.linq.Queryable;
+
+import java.math.BigDecimal;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.function.BiPredicate;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+/**
+ * Represents the queryable collections
+ *
+ * @param <T> the type of Queryable element
+ * @since 4.0.0
+ */
+public class QueryableCollection<T> implements Queryable<T>, Iterable<T> {
+    private final Iterable<T> sourceIterable;
+    private Stream<T> sourceStream;
+
+    public static <T> Queryable<T> from(Iterable<T> sourceIterable) {
+        return new QueryableCollection<>(sourceIterable);
+    }
+
+    @SuppressWarnings("unchecked")
+    public static <T> Queryable<T> from(Stream<? extends T> sourceStream) {
+        Iterable<T> sourceIterable = (Iterable<T>) toIterable(sourceStream);
+        return from(sourceIterable);
+    }
+
+    private QueryableCollection(Iterable<T> sourceIterable) {
+        this.sourceIterable = sourceIterable;
+        this.sourceStream = toStream(sourceIterable);
+    }
+
+    @Override
+    public Iterator<T> iterator() {
+        return sourceIterable.iterator();
+    }
+
+    @Override
+    public <U> Queryable<Tuple2<T, U>> innerJoin(Queryable<? extends U> queryable, BiPredicate<? super T, ? super U> joiner) {
+        Stream<Tuple2<T, U>> stream =
+                this.stream()
+                        .flatMap(p ->
+                                queryable.stream()
+                                        .filter(c -> joiner.test(p, c))
+                                        .map(c -> Tuple.tuple(p, c)));
+
+        return from(stream);
+    }
+
+    @Override
+    public <U> Queryable<Tuple2<T, U>> leftJoin(Queryable<? extends U> queryable, BiPredicate<? super T, ? super U> joiner) {
+        return outerJoin(this, queryable, joiner);
+    }
+
+    @Override
+    public <U> Queryable<Tuple2<T, U>> rightJoin(Queryable<? extends U> queryable, BiPredicate<? super T, ? super U> joiner) {
+        return outerJoin(queryable, this, (a, b) -> joiner.test(b, a)).select(e -> Tuple.tuple(e.getV2(), e.getV1()));
+    }
+
+    @Override
+    public <U> Queryable<Tuple2<T, U>> crossJoin(Queryable<? extends U> queryable) {
+        Stream<Tuple2<T, U>> stream =
+                this.stream()
+                        .flatMap(p ->
+                                queryable.stream()
+                                        .map(c -> Tuple.tuple(p, c)));
+
+        return from(stream);
+    }
+
+    @Override
+    public Queryable<T> where(Predicate<? super T> filter) {
+        Stream<T> stream = this.stream().filter(filter::test);
+
+        return from(stream);
+    }
+
+    @Override
+    public <K> Queryable<Tuple2<K, Queryable<T>>> groupBy(Function<? super T, ? extends K> classifier, BiPredicate<? super K, ? super Queryable<? extends T>> having) {
+        Stream<Tuple2<K, Queryable<T>>> stream =
+                this.stream()
+                        .collect(Collectors.groupingBy(classifier, Collectors.toList()))
+                        .entrySet().stream()
+                        .filter(m -> having.test(m.getKey(), from(m.getValue())))
+                        .map(m -> Tuple.tuple(m.getKey(), from(m.getValue())));
+
+        return from(stream);
+    }
+
+    @Override
+    public <U extends Comparable<? super U>> Queryable<T> orderBy(Order<? super T, ? extends U>... orders) {
+        Comparator<T> comparator = null;
+        for (int i = 0, n = orders.length; i < n; i++) {
+            Order<? super T, ? extends U> order = orders[i];
+            Comparator<U> ascOrDesc = order.isAsc() ? Comparator.naturalOrder() : Comparator.reverseOrder();
+            comparator =
+                    0 == i
+                            ? Comparator.comparing(order.getKeyExtractor(), ascOrDesc)
+                            : comparator.thenComparing(order.getKeyExtractor(), ascOrDesc);
+        }
+
+        if (null == comparator) {
+            return this;
+        }
+
+        return from(this.stream().sorted(comparator));
+    }
+
+    @Override
+    public Queryable<T> limit(int offset, int size) {
+        Stream<T> stream = this.stream().skip(offset).limit(size);
+
+        return from(stream);
+    }
+
+    @Override
+    public <U> Queryable<U> select(Function<? super T, ? extends U> mapper) {
+        Stream<U> stream = this.stream().map(mapper);
+
+        return from(stream);
+    }
+
+    @Override
+    public Queryable<T> distinct() {
+        Stream<? extends T> stream = this.stream().distinct();
+
+        return from(stream);
+    }
+
+    @Override
+    public Queryable<T> unionAll(Queryable<? extends T> queryable) {
+        Stream<T> stream = Stream.concat(this.stream(), queryable.stream());
+
+        return from(stream);
+    }
+
+    @Override
+    public Queryable<T> intersect(Queryable<? extends T> queryable) {
+        Stream<T> stream = this.stream().filter(a -> queryable.stream().anyMatch(b -> b.equals(a))).distinct();
+
+        return from(stream);
+    }
+
+    @Override
+    public Queryable<T> minus(Queryable<? extends T> queryable) {
+        Stream<T> stream = this.stream().filter(a -> queryable.stream().noneMatch(b -> b.equals(a))).distinct();
+
+        return from(stream);
+    }
+
+    @Override
+    public List<T> toList() {
+        return stream().collect(Collectors.toList());
+    }
+
+    @Override
+    public Stream<T> stream() {
+        try {
+            sourceStream = sourceStream.peek(e -> {}); // check whether the stream is usable
+        } catch (IllegalStateException ex) {
+            sourceStream = toStream(sourceIterable);  // we have to create new stream every time because Java stream can not be reused
+        }
+
+        return sourceStream;
+    }
+
+    @Override
+    public int count() {
+        return toList().size();
+    }
+
+    @Override
+    public BigDecimal sum(Function<? super T, BigDecimal> mapper) {
+        return this.stream().map(mapper).reduce(BigDecimal.ZERO, BigDecimal::add);
+    }
+
+    private static <T, U> Queryable<Tuple2<T, U>> outerJoin(Queryable<? extends T> queryable1, Queryable<? extends U> queryable2, BiPredicate<? super T, ? super U> joiner) {
+        Stream<Tuple2<T, U>> stream =
+                queryable1.stream()
+                        .flatMap(p ->
+                                queryable2.stream()
+                                        .map(c -> joiner.test(p, c) ? c : null)
+                                        .reduce(new LinkedList<U>(), (r, e) -> {
+                                            int size = r.size();
+                                            if (0 == size) {
+                                                r.add(e);
+                                                return r;
+                                            }
+
+                                            int lastIndex = size - 1;
+                                            Object lastElement = r.get(lastIndex);
+
+                                            if (null != e) {
+                                                if (null == lastElement) {
+                                                    r.set(lastIndex, e);
+                                                } else {
+                                                    r.add(e);
+                                                }
+                                            }
+
+                                            return r;
+                                        }, (i, o) -> o).stream()
+                                        .map(c -> null == c ? Tuple.tuple(p, null) : Tuple.tuple(p, c)));
+
+        return from(stream);
+    }
+
+    private static <T> Stream<T> toStream(Iterable<T> sourceIterable) {
+        return StreamSupport.stream(sourceIterable.spliterator(), false);
+    }
+
+    private static <T> Iterable<T> toIterable(Stream<T> sourceStream) {
+        return sourceStream::iterator;
+    }
+}
diff --git a/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/provider/QueryableCollectionTest.groovy b/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/provider/QueryableCollectionTest.groovy
new file mode 100644
index 0000000..be20ea6
--- /dev/null
+++ b/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/provider/QueryableCollectionTest.groovy
@@ -0,0 +1,403 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.groovy.linq.provider
+
+
+import groovy.transform.CompileDynamic
+import groovy.transform.CompileStatic
+import groovy.transform.EqualsAndHashCode
+import groovy.transform.ToString
+import org.apache.groovy.linq.Queryable
+import org.junit.Test
+
+import java.util.stream.Stream
+
+@CompileStatic
+class QueryableCollectionTest {
+    @Test
+    void testFrom() {
+        assert [1, 2, 3] == QueryableCollection.from(Stream.of(1, 2, 3)).toList()
+        assert [1, 2, 3] == QueryableCollection.from(Arrays.asList(1, 2, 3)).toList()
+    }
+
+    @Test
+    void testInnerJoin0() {
+        def nums1 = [1, 2, 3]
+        def nums2 = [1, 2, 3]
+        def result = QueryableCollection.from(nums1).innerJoin(QueryableCollection.from(nums2), (a, b) -> a == b).toList()
+        assert [[1, 1], [2, 2], [3, 3]] == result
+    }
+
+    @Test
+    void testInnerJoin1() {
+        def nums1 = [1, 2, 3]
+        def nums2 = [2, 3, 4]
+        def result = QueryableCollection.from(nums1).innerJoin(QueryableCollection.from(nums2), (a, b) -> a == b).toList()
+        assert [[2, 2], [3, 3]] == result
+    }
+
+    @Test
+    void testLeftJoin0() {
+        def nums1 = [1, 2, 3]
+        def nums2 = [1, 2, 3]
+        def result = QueryableCollection.from(nums1).leftJoin(QueryableCollection.from(nums2), (a, b) -> a == b).toList()
+        assert [[1, 1], [2, 2], [3, 3]] == result
+    }
+
+    @Test
+    void testRightJoin0() {
+        def nums2 = [1, 2, 3]
+        def nums1 = [1, 2, 3]
+        def result = QueryableCollection.from(nums1).rightJoin(QueryableCollection.from(nums2), (a, b) -> a == b).toList()
+        assert [[1, 1], [2, 2], [3, 3]] == result
+    }
+
+    @Test
+    void testLeftJoin1() {
+        def nums1 = [1, 2, 3]
+        def nums2 = [2, 3, 4]
+        def result = QueryableCollection.from(nums1).leftJoin(QueryableCollection.from(nums2), (a, b) -> a == b).toList()
+        assert [[1, null], [2, 2], [3, 3]] == result
+    }
+
+    @Test
+    void testRightJoin1() {
+        def nums2 = [1, 2, 3]
+        def nums1 = [2, 3, 4]
+        def result = QueryableCollection.from(nums1).rightJoin(QueryableCollection.from(nums2), (a, b) -> a == b).toList()
+        assert [[null, 1], [2, 2], [3, 3]] == result
+    }
+
+    @Test
+    void testLeftJoin2() {
+        def nums1 = [1, 2, 3, null]
+        def nums2 = [2, 3, 4]
+        def result = QueryableCollection.from(nums1).leftJoin(QueryableCollection.from(nums2), (a, b) -> a == b).toList()
+        assert [[1, null], [2, 2], [3, 3], [null, null]] == result
+    }
+
+    @Test
+    void testRightJoin2() {
+        def nums2 = [1, 2, 3, null]
+        def nums1 = [2, 3, 4]
+        def result = QueryableCollection.from(nums1).rightJoin(QueryableCollection.from(nums2), (a, b) -> a == b).toList()
+        assert [[null, 1], [2, 2], [3, 3], [null, null]] == result
+    }
+
+    @Test
+    void testLeftJoin3() {
+        def nums1 = [1, 2, 3, null]
+        def nums2 = [2, 3, 4, null]
+        def result = QueryableCollection.from(nums1).leftJoin(QueryableCollection.from(nums2), (a, b) -> a == b).toList()
+        assert [[1, null], [2, 2], [3, 3], [null, null]] == result
+    }
+
+    @Test
+    void testRightJoin3() {
+        def nums2 = [1, 2, 3, null]
+        def nums1 = [2, 3, 4, null]
+        def result = QueryableCollection.from(nums1).rightJoin(QueryableCollection.from(nums2), (a, b) -> a == b).toList()
+        assert [[null, 1], [2, 2], [3, 3], [null, null]] == result
+    }
+
+    @Test
+    void testLeftJoin4() {
+        def nums1 = [1, 2, 3]
+        def nums2 = [2, 3, 4, null]
+        def result = QueryableCollection.from(nums1).leftJoin(QueryableCollection.from(nums2), (a, b) -> a == b).toList()
+        assert [[1, null], [2, 2], [3, 3]] == result
+    }
+
+    @Test
+    void testRightJoin4() {
+        def nums2 = [1, 2, 3]
+        def nums1 = [2, 3, 4, null]
+        def result = QueryableCollection.from(nums1).rightJoin(QueryableCollection.from(nums2), (a, b) -> a == b).toList()
+        assert [[null, 1], [2, 2], [3, 3]] == result
+    }
+
+    @Test
+    void testLeftJoin5() {
+        def nums1 = [1, 2, 3, null, null]
+        def nums2 = [2, 3, 4]
+        def result = QueryableCollection.from(nums1).leftJoin(QueryableCollection.from(nums2), (a, b) -> a == b).toList()
+        assert [[1, null], [2, 2], [3, 3], [null, null], [null, null]] == result
+    }
+
+    @Test
+    void testRightJoin5() {
+        def nums2 = [1, 2, 3, null, null]
+        def nums1 = [2, 3, 4]
+        def result = QueryableCollection.from(nums1).rightJoin(QueryableCollection.from(nums2), (a, b) -> a == b).toList()
+        assert [[null, 1], [2, 2], [3, 3], [null, null], [null, null]] == result
+    }
+
+    @Test
+    void testLeftJoin6() {
+        def nums1 = [1, 2, 3, null, null]
+        def nums2 = [2, 3, 4, null]
+        def result = QueryableCollection.from(nums1).leftJoin(QueryableCollection.from(nums2), (a, b) -> a == b).toList()
+        assert [[1, null], [2, 2], [3, 3], [null, null], [null, null]] == result
+    }
+
+    @Test
+    void testRightJoin6() {
+        def nums2 = [1, 2, 3, null, null]
+        def nums1 = [2, 3, 4, null]
+        def result = QueryableCollection.from(nums1).rightJoin(QueryableCollection.from(nums2), (a, b) -> a == b).toList()
+        assert [[null, 1], [2, 2], [3, 3], [null, null], [null, null]] == result
+    }
+
+    @Test
+    void testLeftJoin7() {
+        def nums1 = [1, 2, 3, null, null]
+        def nums2 = [2, 3, 4, null, null]
+        def result = QueryableCollection.from(nums1).leftJoin(QueryableCollection.from(nums2), (a, b) -> a == b).toList()
+        assert [[1, null], [2, 2], [3, 3], [null, null], [null, null]] == result
+    }
+
+    @Test
+    void testRightJoin7() {
+        def nums2 = [1, 2, 3, null, null]
+        def nums1 = [2, 3, 4, null, null]
+        def result = QueryableCollection.from(nums1).rightJoin(QueryableCollection.from(nums2), (a, b) -> a == b).toList()
+        assert [[null, 1], [2, 2], [3, 3], [null, null], [null, null]] == result
+    }
+
+    @Test
+    void testLeftJoin8() {
+        def nums1 = [1, 2, 3, null]
+        def nums2 = [2, 3, 4, null, null]
+        def result = QueryableCollection.from(nums1).leftJoin(QueryableCollection.from(nums2), (a, b) -> a == b).toList()
+        assert [[1, null], [2, 2], [3, 3], [null, null]] == result
+    }
+
+    @Test
+    void testRightJoin8() {
+        def nums2 = [1, 2, 3, null]
+        def nums1 = [2, 3, 4, null, null]
+        def result = QueryableCollection.from(nums1).rightJoin(QueryableCollection.from(nums2), (a, b) -> a == b).toList()
+        assert [[null, 1], [2, 2], [3, 3], [null, null]] == result
+    }
+
+    @Test
+    void testLeftJoin9() {
+        def nums1 = [1, 2, 3]
+        def nums2 = [2, 3, 4, null, null]
+        def result = QueryableCollection.from(nums1).leftJoin(QueryableCollection.from(nums2), (a, b) -> a == b).toList()
+        assert [[1, null], [2, 2], [3, 3]] == result
+    }
+
+    @Test
+    void testRightJoin9() {
+        def nums2 = [1, 2, 3]
+        def nums1 = [2, 3, 4, null, null]
+        def result = QueryableCollection.from(nums1).rightJoin(QueryableCollection.from(nums2), (a, b) -> a == b).toList()
+        assert [[null, 1], [2, 2], [3, 3]] == result
+    }
+
+    @Test
+    void testFullJoin() {
+        def nums1 = [1, 2, 3]
+        def nums2 = [2, 3, 4]
+        def result = QueryableCollection.from(nums1).fullJoin(QueryableCollection.from(nums2), (a, b) -> a == b).toList()
+        assert [[1, null], [2, 2], [3, 3], [null, 4]] == result
+    }
+
+    @Test
+    void testCrossJoin() {
+        def nums1 = [1, 2, 3]
+        def nums2 = [3, 4, 5]
+        def result = QueryableCollection.from(nums1).crossJoin(QueryableCollection.from(nums2)).toList()
+        assert [[1, 3], [1, 4], [1, 5], [2, 3], [2, 4], [2, 5], [3, 3], [3, 4], [3, 5]] == result
+    }
+
+    @Test
+    void testWhere() {
+        def nums = [1, 2, 3, 4, 5]
+        def result = QueryableCollection.from(nums).where(e -> e > 3).toList()
+        assert [4, 5] == result
+    }
+
+    @Test
+    void testGroupBySelect0() {
+        def nums = [1, 2, 2, 3, 3, 4, 4, 5]
+        def result = QueryableCollection.from(nums).groupBy(e -> e).select(e -> Tuple.tuple(e.v1, e.v2.toList())).toList()
+        assert [[1, [1]], [2, [2, 2]], [3, [3, 3]], [4, [4, 4]], [5, [5]]] == result
+    }
+
+    @Test
+    void testGroupBySelect1() {
+        def nums = [1, 2, 2, 3, 3, 4, 4, 5]
+        def result = QueryableCollection.from(nums).groupBy(e -> e).select(e -> Tuple.tuple(e.v1, e.v2.count())).toList()
+        assert [[1, 1], [2, 2], [3, 2], [4, 2], [5, 1]] == result
+    }
+
+    @Test
+    void testGroupBySelect2() {
+        def nums = [1, 2, 2, 3, 3, 4, 4, 5]
+        def result =
+                QueryableCollection.from(nums).groupBy(e -> e)
+                        .select(e ->
+                                Tuple.tuple(
+                                        e.v1,
+                                        e.v2.count(),
+                                        e.v2.sum(n -> new BigDecimal(n))
+                                )
+                        ).toList()
+        assert [[1, 1, 1], [2, 2, 4], [3, 2, 6], [4, 2, 8], [5, 1, 5]] == result
+    }
+
+    @Test
+    @CompileDynamic
+    void testGroupBySelect3() {
+        def nums = [1, 2, 2, 3, 3, 4, 4, 5]
+        def result =
+                QueryableCollection.from(nums).groupBy(e -> e, (k, q) -> k > 2)
+                        .select(e ->
+                                Tuple.tuple(
+                                        e.v1,
+                                        e.v2.count(),
+                                        e.v2.sum(n -> new BigDecimal(n))
+                                )
+                        ).toList()
+        assert [[3, 2, 6], [4, 2, 8], [5, 1, 5]] == result
+    }
+
+    @Test
+    void testOrderBy() {
+        Person daniel = new Person('Daniel', 35)
+        Person peter = new Person('Peter', 10)
+        Person alice = new Person('Alice', 22)
+        Person john = new Person('John', 10)
+
+        def persons = [daniel, peter, alice, john]
+        def result = QueryableCollection.from(persons).orderBy(
+                new Queryable.Order<Person, Comparable>((Person e) -> e.age, true),
+                new Queryable.Order<Person, Comparable>((Person e) -> e.name, true)
+        ).toList()
+        assert [john, peter, alice, daniel] == result
+
+        result = QueryableCollection.from(persons).orderBy(
+                new Queryable.Order<Person, Comparable>((Person e) -> e.age, false),
+                new Queryable.Order<Person, Comparable>((Person e) -> e.name, true)
+        ).toList()
+        assert [daniel, alice, john, peter] == result
+
+        result = QueryableCollection.from(persons).orderBy(
+                new Queryable.Order<Person, Comparable>((Person e) -> e.age, true),
+                new Queryable.Order<Person, Comparable>((Person e) -> e.name, false)
+        ).toList()
+        assert [peter, john, alice, daniel] == result
+
+        result = QueryableCollection.from(persons).orderBy(
+                new Queryable.Order<Person, Comparable>((Person e) -> e.age, false),
+                new Queryable.Order<Person, Comparable>((Person e) -> e.name, false)
+        ).toList()
+        assert [daniel, alice, peter, john] == result
+    }
+
+    @Test
+    void testLimit() {
+        def nums = [1, 2, 3, 4, 5]
+        def result = QueryableCollection.from(nums).limit(1, 2).toList()
+        assert [2, 3] == result
+
+        result = QueryableCollection.from(nums).limit(2).toList()
+        assert [1, 2] == result
+    }
+
+    @Test
+    void testSelect() {
+        def nums = [1, 2, 3, 4, 5]
+        def result = QueryableCollection.from(nums).select(e -> e + 1).toList()
+        assert [2, 3, 4, 5, 6] == result
+    }
+
+    @Test
+    void testDistinct() {
+        def nums = [1, 2, 2, 3, 3, 2, 3, 4, 5, 5]
+        def result = QueryableCollection.from(nums).distinct().toList()
+        assert [1, 2, 3, 4, 5] == result
+    }
+
+    @Test
+    void testUnion() {
+        def nums1 = [1, 2, 3]
+        def nums2 = [2, 3, 4]
+        def result = QueryableCollection.from(nums1).union(QueryableCollection.from(nums2)).toList()
+        assert [1, 2, 3, 4] == result
+    }
+
+    @Test
+    void testUnionAll() {
+        def nums1 = [1, 2, 3]
+        def nums2 = [2, 3, 4]
+        def result = QueryableCollection.from(nums1).unionAll(QueryableCollection.from(nums2)).toList()
+        assert [1, 2, 3, 2, 3, 4] == result
+    }
+
+    @Test
+    void testIntersect() {
+        def nums1 = [1, 2, 2, 3]
+        def nums2 = [2, 3, 3, 4]
+        def result = QueryableCollection.from(nums1).intersect(QueryableCollection.from(nums2)).toList()
+        assert [2, 3] == result
+    }
+
+    @Test
+    void testMinus() {
+        def nums1 = [1, 1, 2, 3]
+        def nums2 = [2, 3, 4]
+        def result = QueryableCollection.from(nums1).minus(QueryableCollection.from(nums2)).toList()
+        assert [1] == result
+    }
+
+    @Test
+    void testFromWhereLimitSelect() {
+        def nums1 = [1, 2, 3, 4, 5]
+        def nums2 = [0, 1, 2, 3, 4, 5, 6]
+        def result =
+                QueryableCollection.from(nums1)
+                        .innerJoin(QueryableCollection.from(nums2), (a, b) -> a == b)
+                        .where(t -> t.v1 > 1)
+                        .limit(1, 2)
+                        .select(t -> t.v1 + 1)
+                        .toList()
+        assert [4, 5] == result
+    }
+
+//    @Test void testIterator() {
+//        def nums = [1, 2, 3]
+//        def result = from(nums).iterator().toList()
+//        assert nums == result
+//    }
+
+    @ToString
+    @EqualsAndHashCode
+    static class Person {
+        String name
+        int age
+
+        Person(String name, int age) {
+            this.name = name
+            this.age = age
+        }
+    }
+}