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/06 17:37:29 UTC

[groovy] branch GROOVY-8258 updated (cbf4d0d -> 638464a)

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.


 discard cbf4d0d  GROOVY-8258: add more test cases
 discard 8a7c91a  GROOVY-8258: minor refactor and add more error test cases
 discard d6659c3  GROOVY-8258: Minor refactoring
 discard b8b1719  GROOVY-8258: support mixing `innerJoin` and `where`
 discard b126ecb  GROOVY-8258: tweak AST
 discard 1dcf588  GROOVY-8258: Add comment
 discard d4d6a22  GROOVY-8258: eliminate redundant metadata keys
 discard d1f7660  GROOVY-8258: add more test cases for ginq in 1 line
 discard aec5eea  GROOVY-8258: trivial refactoring
 discard 0c5ea13  GROOVY-8258: tweak AST
 discard ec53810  GROOVY-8258: rename class name
 discard 378a1ef  GROOVY-8258: simplify the ginq AST
 discard fd4169e  GROOVY-8258: add more test cases
 discard c7f0f4d  GROOVY-8258: trivial refactoring
 discard e8e95d8  GROOVY-8258: Implement inner join
 discard 0707349  GROOVY-8258: Add 1 more error test
 discard b94d20f  GROOVY-8258: Add node positions and error tests
 discard 2318b69  GROOVY-8258: Use `in` instead of `of` for better readability
 discard b69bf21  GROOVY-8258: make field `final`
 discard 9f92fc8  GROOVY-8258: Rename class name
 discard d413188  GROOVY-8258: add javadoc and rename linq to ginq
 discard 7273eed  GROOVY-8258: build AST for ginq and generate target method calls
 discard d355e9c  GROOVY-8258: rename linq to ginq
 discard 189c49d  GROOVY-8258: enable STC
 discard 0c7a6fe  GROOVY-8258: use lambda instead
 discard 581ee95  GROOVY-8258: rename test names
 discard 9ff78df  GROOVY-8258: Add 2 test cases
 discard 174b227  GROOVY-8258: minor refactor
 discard 1ab90b5  GROOVY-8258: Implement the very basic LINQ
 discard 2610bac  GROOVY-8258: mark backend classes as internal
 discard ffde533  GROOVY-8258: test `stream` method and tweak tests
 discard e233f34  GROOVY-8258: Add macro method LINQ
 discard 3e39e70  GROOVY-8258: Add macro method LINQ entry
 discard ac92248  GROOVY-8258: Implement the built-in collection LINQ provider
     add e5abb35  added final to parameters and other minor fix-ups
     new 638464a  GROOVY-8258: [GEP] Create a LINQ-like DSL(very basic version)

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (cbf4d0d)
            \
             N -- N -- N   refs/heads/GROOVY-8258 (638464a)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

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.


Summary of changes:
 .../groovy/transform/ASTTestTransformation.groovy  |   4 +-
 ...StaticTypesMethodReferenceExpressionWriter.java | 156 +++++++++------------
 2 files changed, 68 insertions(+), 92 deletions(-)


[groovy] 01/01: GROOVY-8258: [GEP] Create a LINQ-like DSL(very basic version)

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 638464a0f668978d51cba2a733949db1f6bd14eb
Author: Daniel Sun <su...@apache.org>
AuthorDate: Wed Oct 7 01:37:01 2020 +0800

    GROOVY-8258: [GEP] Create a LINQ-like DSL(very basic version)
---
 settings.gradle                                    |   1 +
 .../codehaus/groovy/ast/tools/GeneralUtils.java    |   9 +
 subprojects/groovy-linq/build.gradle               |  31 ++
 .../apache/groovy/linq/GinqGroovyMethods.groovy    |  47 +++
 .../groovy/org/apache/groovy/linq/Queryable.java   | 126 +++++++
 .../org/apache/groovy/linq/dsl/GinqAstBuilder.java | 141 +++++++
 .../apache/groovy/linq/dsl/GinqAstWalker.groovy    | 286 +++++++++++++++
 .../apache/groovy/linq/dsl/GinqSyntaxError.java    |  43 +++
 .../org/apache/groovy/linq/dsl/GinqVisitor.java    |  43 +++
 .../groovy/linq/dsl/SyntaxErrorReportable.java     |  43 +++
 .../dsl/expression/AbstractGinqExpression.java     |  69 ++++
 .../linq/dsl/expression/DataSourceExpression.java  |  43 +++
 .../linq/dsl/expression/FilterExpression.java      |  42 +++
 .../linq/dsl/expression/FilterableExpression.java  |  39 ++
 .../groovy/linq/dsl/expression/FromExpression.java |  47 +++
 .../groovy/linq/dsl/expression/GinqExpression.java |  30 ++
 .../linq/dsl/expression/InnerJoinExpression.java   |  39 ++
 .../groovy/linq/dsl/expression/JoinExpression.java |  42 +++
 .../groovy/linq/dsl/expression/OnExpression.java   |  38 ++
 .../linq/dsl/expression/SelectExpression.java      |  51 +++
 .../linq/dsl/expression/SimpleGinqExpression.java  |  72 ++++
 .../linq/dsl/expression/WhereExpression.java       |  38 ++
 .../groovy/linq/provider/QueryableCollection.java  | 251 +++++++++++++
 .../org/apache/groovy/linq/GinqErrorTest.groovy    |  82 +++++
 .../groovy/org/apache/groovy/linq/GinqTest.groovy  | 380 +++++++++++++++++++
 .../linq/provider/QueryableCollectionTest.groovy   | 407 +++++++++++++++++++++
 26 files changed, 2440 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/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java b/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java
index 6abbe61..15e4db6 100644
--- a/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java
+++ b/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java
@@ -43,6 +43,7 @@ import org.codehaus.groovy.ast.expr.ConstructorCallExpression;
 import org.codehaus.groovy.ast.expr.DeclarationExpression;
 import org.codehaus.groovy.ast.expr.Expression;
 import org.codehaus.groovy.ast.expr.FieldExpression;
+import org.codehaus.groovy.ast.expr.LambdaExpression;
 import org.codehaus.groovy.ast.expr.ListExpression;
 import org.codehaus.groovy.ast.expr.MapEntryExpression;
 import org.codehaus.groovy.ast.expr.MapExpression;
@@ -251,6 +252,14 @@ public class GeneralUtils {
         return closureX(Parameter.EMPTY_ARRAY, code);
     }
 
+    public static LambdaExpression lambdaX(final Parameter[] params, final Statement code) {
+        return new LambdaExpression(params, code);
+    }
+
+    public static LambdaExpression lambdaX(final Statement code) {
+        return lambdaX(Parameter.EMPTY_ARRAY, code);
+    }
+
     /**
      * Builds a binary expression that compares two values.
      *
diff --git a/subprojects/groovy-linq/build.gradle b/subprojects/groovy-linq/build.gradle
new file mode 100644
index 0000000..cc476c4
--- /dev/null
+++ b/subprojects/groovy-linq/build.gradle
@@ -0,0 +1,31 @@
+/*
+ *  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 {
+    implementation rootProject
+    implementation project(':groovy-macro')
+    testImplementation rootProject.sourceSets.test.runtimeClasspath
+    testImplementation project(':groovy-test')
+}
+
+task moduleDescriptor(type: org.codehaus.groovy.gradle.WriteExtensionDescriptorTask) {
+    extensionClasses = 'org.apache.groovy.linq.GinqGroovyMethods'
+}
+
+compileJava.dependsOn moduleDescriptor
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/GinqGroovyMethods.groovy b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/GinqGroovyMethods.groovy
new file mode 100644
index 0000000..534810b
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/GinqGroovyMethods.groovy
@@ -0,0 +1,47 @@
+/*
+ *  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.transform.CompileStatic
+import org.apache.groovy.linq.dsl.GinqAstBuilder
+import org.apache.groovy.linq.dsl.GinqAstWalker
+import org.apache.groovy.linq.dsl.expression.SimpleGinqExpression
+import org.codehaus.groovy.ast.expr.ClosureExpression
+import org.codehaus.groovy.ast.expr.Expression
+import org.codehaus.groovy.ast.expr.MethodCallExpression
+import org.codehaus.groovy.ast.stmt.Statement
+import org.codehaus.groovy.macro.runtime.Macro
+import org.codehaus.groovy.macro.runtime.MacroContext
+
+@CompileStatic
+class GinqGroovyMethods {
+    @Macro
+    static Expression GINQ(MacroContext ctx, final ClosureExpression closureExpression) {
+        Statement code = closureExpression.getCode()
+
+        GinqAstBuilder ginqAstBuilder = new GinqAstBuilder(ctx.getSourceUnit())
+        code.visit(ginqAstBuilder)
+        SimpleGinqExpression simpleGinqExpression = ginqAstBuilder.getSimpleGinqExpression()
+
+        GinqAstWalker ginqBuilder = new GinqAstWalker(ctx.getSourceUnit())
+        MethodCallExpression selectMethodCallExpression = ginqBuilder.visitSimpleGinqExpression(simpleGinqExpression)
+
+        return selectMethodCallExpression
+    }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/Queryable.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/Queryable.java
new file mode 100644
index 0000000..56803ac
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/Queryable.java
@@ -0,0 +1,126 @@
+/*
+ *  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 groovy.transform.Internal;
+
+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
+ */
+@Internal
+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/groovy/org/apache/groovy/linq/dsl/GinqAstBuilder.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqAstBuilder.java
new file mode 100644
index 0000000..ee37ce5
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqAstBuilder.java
@@ -0,0 +1,141 @@
+/*
+ *  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.dsl;
+
+import org.apache.groovy.linq.dsl.expression.FilterExpression;
+import org.apache.groovy.linq.dsl.expression.FilterableExpression;
+import org.apache.groovy.linq.dsl.expression.FromExpression;
+import org.apache.groovy.linq.dsl.expression.GinqExpression;
+import org.apache.groovy.linq.dsl.expression.InnerJoinExpression;
+import org.apache.groovy.linq.dsl.expression.JoinExpression;
+import org.apache.groovy.linq.dsl.expression.OnExpression;
+import org.apache.groovy.linq.dsl.expression.SelectExpression;
+import org.apache.groovy.linq.dsl.expression.SimpleGinqExpression;
+import org.apache.groovy.linq.dsl.expression.WhereExpression;
+import org.codehaus.groovy.GroovyBugError;
+import org.codehaus.groovy.ast.CodeVisitorSupport;
+import org.codehaus.groovy.ast.expr.ArgumentListExpression;
+import org.codehaus.groovy.ast.expr.BinaryExpression;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.expr.MethodCallExpression;
+import org.codehaus.groovy.control.SourceUnit;
+import org.codehaus.groovy.syntax.Types;
+
+/**
+ * Build the AST for GINQ
+ *
+ * @since 4.0.0
+ */
+public class GinqAstBuilder extends CodeVisitorSupport implements SyntaxErrorReportable {
+    private SimpleGinqExpression simpleGinqExpression = new SimpleGinqExpression(); // store the result
+    private GinqExpression ginqExpression; // store the return value
+    private final SourceUnit sourceUnit;
+
+    public GinqAstBuilder(SourceUnit sourceUnit) {
+        this.sourceUnit = sourceUnit;
+    }
+
+    public SimpleGinqExpression getSimpleGinqExpression() {
+        return simpleGinqExpression;
+    }
+
+    @Override
+    public void visitMethodCallExpression(MethodCallExpression call) {
+        super.visitMethodCallExpression(call);
+        final String methodName = call.getMethodAsString();
+
+        if ("from".equals(methodName)  || "innerJoin".equals(methodName)) {
+            ArgumentListExpression arguments = (ArgumentListExpression) call.getArguments();
+            if (arguments.getExpressions().size() != 1) {
+                this.collectSyntaxError(
+                        new GinqSyntaxError(
+                                "Only 1 argument expected for `" + methodName + "`, e.g. `" + methodName + " n in nums`",
+                                call.getLineNumber(), call.getColumnNumber()
+                        )
+                );
+            }
+            final Expression expression = arguments.getExpression(0);
+            if (!(expression instanceof BinaryExpression
+                    && ((BinaryExpression) expression).getOperation().getType() == Types.KEYWORD_IN)) {
+                this.collectSyntaxError(
+                        new GinqSyntaxError(
+                                "`in` is expected for `" + methodName + "`, e.g. `" + methodName + " n in nums`",
+                                call.getLineNumber(), call.getColumnNumber()
+                        )
+                );
+            }
+            BinaryExpression binaryExpression = (BinaryExpression) expression;
+            Expression aliasExpr = binaryExpression.getLeftExpression();
+            Expression dataSourceExpr = binaryExpression.getRightExpression();
+
+            FilterableExpression filterableExpression = null;
+            if ("from".equals(methodName)) {
+                filterableExpression = new FromExpression(aliasExpr, dataSourceExpr);
+                simpleGinqExpression.setFromExpression((FromExpression) filterableExpression);
+            } else if ("innerJoin".equals(methodName)) {
+                filterableExpression = new InnerJoinExpression(aliasExpr, dataSourceExpr);
+                simpleGinqExpression.addJoinExpression((JoinExpression) filterableExpression);
+            }
+            filterableExpression.setSourcePosition(call);
+            ginqExpression = filterableExpression;
+
+            return;
+        }
+
+        if ("where".equals(methodName) || "on".equals(methodName)) {
+            Expression filterExpr = ((ArgumentListExpression) call.getArguments()).getExpression(0);
+
+            FilterExpression filterExpression = null;
+            if ("where".equals(methodName)) {
+                filterExpression = new WhereExpression(filterExpr);
+            } else if ("on".equals(methodName)) {
+                filterExpression = new OnExpression(filterExpr);
+            }
+
+            if (null == filterExpression) {
+                throw new GroovyBugError("Unknown method: " + methodName);
+            }
+
+            filterExpression.setSourcePosition(call);
+
+            if (ginqExpression instanceof FilterableExpression) { // TODO more strict check
+                ((FilterableExpression) ginqExpression).addFilterExpression(filterExpression);
+            } else {
+                throw new GroovyBugError("The preceding expression is not a FilterableExpression: " + ginqExpression);
+            }
+
+            return;
+        }
+
+        if ("select".equals(methodName)) {
+            SelectExpression selectExpression = new SelectExpression(call.getArguments());
+            selectExpression.setSourcePosition(call);
+
+            simpleGinqExpression.setSelectExpression(selectExpression);
+            ginqExpression = selectExpression;
+
+            return;
+        }
+    }
+
+    @Override
+    public SourceUnit getSourceUnit() {
+        return sourceUnit;
+    }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqAstWalker.groovy b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqAstWalker.groovy
new file mode 100644
index 0000000..ad0f720
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqAstWalker.groovy
@@ -0,0 +1,286 @@
+/*
+ *  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.dsl
+
+import groovy.transform.CompileDynamic
+import groovy.transform.CompileStatic
+import org.apache.groovy.linq.dsl.expression.DataSourceExpression
+import org.apache.groovy.linq.dsl.expression.FilterExpression
+import org.apache.groovy.linq.dsl.expression.FromExpression
+import org.apache.groovy.linq.dsl.expression.GinqExpression
+import org.apache.groovy.linq.dsl.expression.InnerJoinExpression
+import org.apache.groovy.linq.dsl.expression.JoinExpression
+import org.apache.groovy.linq.dsl.expression.OnExpression
+import org.apache.groovy.linq.dsl.expression.SelectExpression
+import org.apache.groovy.linq.dsl.expression.SimpleGinqExpression
+import org.apache.groovy.linq.dsl.expression.WhereExpression
+import org.codehaus.groovy.ast.ClassHelper
+import org.codehaus.groovy.ast.expr.ArgumentListExpression
+import org.codehaus.groovy.ast.expr.Expression
+import org.codehaus.groovy.ast.expr.ExpressionTransformer
+import org.codehaus.groovy.ast.expr.ListExpression
+import org.codehaus.groovy.ast.expr.MethodCallExpression
+import org.codehaus.groovy.ast.expr.TupleExpression
+import org.codehaus.groovy.ast.expr.VariableExpression
+import org.codehaus.groovy.control.SourceUnit
+
+import static org.codehaus.groovy.ast.tools.GeneralUtils.callX
+import static org.codehaus.groovy.ast.tools.GeneralUtils.lambdaX
+import static org.codehaus.groovy.ast.tools.GeneralUtils.param
+import static org.codehaus.groovy.ast.tools.GeneralUtils.params
+import static org.codehaus.groovy.ast.tools.GeneralUtils.propX
+import static org.codehaus.groovy.ast.tools.GeneralUtils.stmt
+
+/**
+ * Visit AST of GINQ and generate target method calls for GINQ
+ *
+ * @since 4.0.0
+ */
+@CompileStatic
+class GinqAstWalker implements GinqVisitor<Object>, SyntaxErrorReportable {
+
+    private final SourceUnit sourceUnit;
+
+    public GinqAstWalker(SourceUnit sourceUnit) {
+        this.sourceUnit = sourceUnit;
+    }
+
+    @Override
+    MethodCallExpression visitSimpleGinqExpression(SimpleGinqExpression simpleGinqExpression) {
+        FromExpression fromExpression = simpleGinqExpression.getFromExpression()
+        MethodCallExpression fromMethodCallExpression = this.visitFromExpression(fromExpression)
+
+        MethodCallExpression selectMethodReceiver = fromMethodCallExpression
+
+        JoinExpression lastJoinExpression = null
+        MethodCallExpression lastJoinMethodCallExpression = null
+        for (JoinExpression joinExpression : simpleGinqExpression.getJoinExpressionList()) {
+            joinExpression.putNodeMetaData(__METHOD_CALL_RECEIVER, lastJoinMethodCallExpression ?: fromMethodCallExpression)
+            joinExpression.putNodeMetaData(__DATA_SOURCE_EXPRESSION, lastJoinExpression ?: fromExpression)
+
+            lastJoinExpression = joinExpression
+            lastJoinMethodCallExpression = this.visitInnerJoinExpression((InnerJoinExpression) lastJoinExpression)
+        }
+
+        if (lastJoinMethodCallExpression) {
+            selectMethodReceiver = lastJoinMethodCallExpression
+        }
+
+        SelectExpression selectExpression = simpleGinqExpression.getSelectExpression()
+        selectExpression.putNodeMetaData(__METHOD_CALL_RECEIVER, selectMethodReceiver)
+        selectExpression.putNodeMetaData(__DATA_SOURCE_EXPRESSION, lastJoinExpression ?: fromExpression)
+
+        MethodCallExpression selectMethodCallExpression = this.visitSelectExpression(selectExpression)
+
+
+        return selectMethodCallExpression
+    }
+
+    @Override
+    MethodCallExpression visitFromExpression(FromExpression fromExpression) {
+        MethodCallExpression fromMethodCallExpression = constructFromMethodCallExpression(fromExpression)
+
+        List<FilterExpression> filterExpressionList = fromExpression.getFilterExpressionList()
+        if (filterExpressionList) {
+            WhereExpression whereExpression = (WhereExpression) filterExpressionList.get(0)
+            whereExpression.putNodeMetaData(__DATA_SOURCE_EXPRESSION, fromExpression)
+            whereExpression.putNodeMetaData(__METHOD_CALL_RECEIVER, fromMethodCallExpression)
+
+            return visitWhereExpression(whereExpression)
+        }
+
+        return fromMethodCallExpression
+    }
+
+    @Override
+    MethodCallExpression visitInnerJoinExpression(InnerJoinExpression innerJoinExpression) {
+        Expression receiver = innerJoinExpression.getNodeMetaData(__METHOD_CALL_RECEIVER)
+        DataSourceExpression dataSourceExpression = innerJoinExpression.getNodeMetaData(__DATA_SOURCE_EXPRESSION)
+        Expression receiverAliasExpr = dataSourceExpression.aliasExpr
+        List<FilterExpression> filterExpressionList = innerJoinExpression.getFilterExpressionList()
+        int filterExpressionListSize = filterExpressionList.size()
+
+        if (0 == filterExpressionListSize) {
+            this.collectSyntaxError(
+                    new GinqSyntaxError(
+                            "`on` clause is expected for `innerJoin`",
+                            innerJoinExpression.getLineNumber(), innerJoinExpression.getColumnNumber()
+                    )
+            );
+        }
+
+        OnExpression onExpression = (OnExpression) filterExpressionList.get(0)
+
+        WhereExpression whereExpression = null
+        if (filterExpressionListSize > 1) {
+            whereExpression = (WhereExpression) filterExpressionList.get(1)
+        }
+
+        MethodCallExpression innerJoinMethodCallExpression = constructInnerJoinMethodCallExpression(receiver, receiverAliasExpr, innerJoinExpression, onExpression, whereExpression)
+
+        return innerJoinMethodCallExpression
+    }
+
+    @Override
+    MethodCallExpression visitOnExpression(OnExpression onExpression) {
+        return null // do nothing
+    }
+
+    @CompileDynamic
+    private MethodCallExpression constructFromMethodCallExpression(FromExpression fromExpression) {
+        macro {
+            org.apache.groovy.linq.provider.QueryableCollection.from($v { fromExpression.dataSourceExpr })
+        }
+    }
+
+    @CompileDynamic
+    private MethodCallExpression constructInnerJoinMethodCallExpression(
+            Expression receiver, Expression receiverAliasExpr, InnerJoinExpression innerJoinExpression,
+            OnExpression onExpression, WhereExpression whereExpression) {
+
+        MethodCallExpression innerJoinMethodCallExpression = macro {
+            $v{receiver}.innerJoin(org.apache.groovy.linq.provider.QueryableCollection.from($v { innerJoinExpression.dataSourceExpr }))
+        }
+
+        ((ArgumentListExpression) innerJoinMethodCallExpression.getArguments()).getExpressions().add(
+                lambdaX(
+                        params(
+                                param(ClassHelper.DYNAMIC_TYPE, receiverAliasExpr.text),
+                                param(ClassHelper.DYNAMIC_TYPE, innerJoinExpression.aliasExpr.text)
+                        ),
+                        stmt(onExpression.getFilterExpr())
+                )
+        )
+
+        if (whereExpression) {
+            whereExpression.putNodeMetaData(__DATA_SOURCE_EXPRESSION, innerJoinExpression)
+            whereExpression.putNodeMetaData(__METHOD_CALL_RECEIVER, innerJoinMethodCallExpression)
+            return visitWhereExpression(whereExpression)
+        }
+
+        return innerJoinMethodCallExpression
+    }
+
+    @Override
+    MethodCallExpression visitWhereExpression(WhereExpression whereExpression) {
+        DataSourceExpression dataSourceExpression = whereExpression.getNodeMetaData(__DATA_SOURCE_EXPRESSION)
+        Expression fromMethodCallExpression = whereExpression.getNodeMetaData(__METHOD_CALL_RECEIVER)
+        Expression filterExpr = whereExpression.getFilterExpr()
+
+        return callXWithLambda(fromMethodCallExpression, "where", dataSourceExpression, filterExpr)
+    }
+
+    @Override
+    MethodCallExpression visitSelectExpression(SelectExpression selectExpression) {
+        Expression selectMethodReceiver = selectExpression.getNodeMetaData(__METHOD_CALL_RECEIVER)
+        DataSourceExpression dataSourceExpression = selectExpression.getNodeMetaData(__DATA_SOURCE_EXPRESSION)
+        Expression projectionExpr = selectExpression.getProjectionExpr()
+
+        List<Expression> expressionList = ((TupleExpression) projectionExpr).getExpressions()
+        Expression lambdaCode
+        if (expressionList.size() > 1) {
+            lambdaCode = new ListExpression(expressionList)
+        } else {
+            lambdaCode = expressionList.get(0)
+        }
+
+        return callXWithLambda(selectMethodReceiver, "select", dataSourceExpression, lambdaCode)
+    }
+
+    private static Expression correctVariablesOfGinqExpression(JoinExpression joinExpression, Expression expr) {
+        DataSourceExpression dataSourceExpression = joinExpression.getNodeMetaData(__DATA_SOURCE_EXPRESSION)
+        final Expression firstAliasExpr = dataSourceExpression.aliasExpr
+        final Expression secondAliasExpr = joinExpression.aliasExpr
+
+        // The synthetic lambda parameter `__t` represents the element from the result datasource of joining, e.g. `n1` innerJoin `n2`
+        // The element from first datasource(`n1`) is referenced via `_t.v1`
+        // and the element from second datasource(`n2`) is referenced via `_t.v2`
+        expr = expr.transformExpression(new ExpressionTransformer() {
+            @Override
+            Expression transform(Expression expression) {
+                if (expression instanceof VariableExpression) {
+                    Expression transformedExpression = null
+                    if (firstAliasExpr.text == expression.text) {
+                        // replace `n1` with `__t.v1`
+                        transformedExpression = constructFirstAliasVariableAccess()
+                    } else if (secondAliasExpr.text == expression.text) {
+                        // replace `n2` with `__t.v2`
+                        transformedExpression = constructSecondAliasVariableAccess()
+                    }
+
+                    if (null != transformedExpression) {
+                        return transformedExpression
+                    }
+                }
+
+                return expression.transformExpression(this)
+            }
+        })
+        return expr
+    }
+
+    @Override
+    Object visit(GinqExpression expression) {
+        return expression.accept(this)
+    }
+
+    private static MethodCallExpression callXWithLambda(Expression receiver, String methodName, DataSourceExpression dataSourceExpression, Expression lambdaCode) {
+        String lambdaParamName
+        if (dataSourceExpression instanceof JoinExpression) {
+            lambdaParamName = __T
+            lambdaCode = correctVariablesOfGinqExpression((JoinExpression) dataSourceExpression, lambdaCode)
+        } else {
+            lambdaParamName = dataSourceExpression.aliasExpr.text
+        }
+
+        callXWithLambda(receiver, methodName, lambdaParamName, lambdaCode)
+    }
+
+    private static MethodCallExpression callXWithLambda(Expression receiver, String methodName, String lambdaParamName, Expression lambdaCode) {
+        callX(
+                receiver,
+                methodName,
+                lambdaX(
+                        params(param(ClassHelper.DYNAMIC_TYPE, lambdaParamName)),
+                        stmt(lambdaCode)
+                )
+        )
+    }
+
+    private static Expression constructFirstAliasVariableAccess() {
+        constructAliasVariableAccess('v1')
+    }
+
+    private static Expression constructSecondAliasVariableAccess() {
+        constructAliasVariableAccess('v2')
+    }
+
+    private static Expression constructAliasVariableAccess(String name) {
+        propX(new VariableExpression(__T), name)
+    }
+
+    @Override
+    SourceUnit getSourceUnit() {
+        sourceUnit
+    }
+
+    private static final String __DATA_SOURCE_EXPRESSION = "__dataSourceExpression"
+    private static final String __METHOD_CALL_RECEIVER = "__methodCallReceiver"
+    private static final String __T = "__t"
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqSyntaxError.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqSyntaxError.java
new file mode 100644
index 0000000..652d835
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqSyntaxError.java
@@ -0,0 +1,43 @@
+/*
+ *  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.dsl;
+
+/**
+ * Represents GINQ syntax error
+ *
+ * @since 4.0.0
+ */
+public class GinqSyntaxError extends AssertionError {
+    private final int line;
+    private final int column;
+
+    public GinqSyntaxError(String message, int line, int column) {
+        super(message, null);
+        this.line = line;
+        this.column = column;
+    }
+
+    public int getLine() {
+        return line;
+    }
+
+    public int getColumn() {
+        return column;
+    }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqVisitor.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqVisitor.java
new file mode 100644
index 0000000..3e5f27e
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqVisitor.java
@@ -0,0 +1,43 @@
+/*
+ *  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.dsl;
+
+import org.apache.groovy.linq.dsl.expression.FromExpression;
+import org.apache.groovy.linq.dsl.expression.GinqExpression;
+import org.apache.groovy.linq.dsl.expression.InnerJoinExpression;
+import org.apache.groovy.linq.dsl.expression.OnExpression;
+import org.apache.groovy.linq.dsl.expression.SelectExpression;
+import org.apache.groovy.linq.dsl.expression.SimpleGinqExpression;
+import org.apache.groovy.linq.dsl.expression.WhereExpression;
+
+/**
+ * Represents the visitor for AST of GINQ
+ *
+ * @param <R> the type of visit result
+ * @since 4.0.0
+ */
+public interface GinqVisitor<R> {
+    R visitSimpleGinqExpression(SimpleGinqExpression simpleGinqExpression);
+    R visitFromExpression(FromExpression fromExpression);
+    R visitInnerJoinExpression(InnerJoinExpression innerJoinExpression);
+    R visitOnExpression(OnExpression onExpression);
+    R visitWhereExpression(WhereExpression whereExpression);
+    R visitSelectExpression(SelectExpression selectExpression);
+    R visit(GinqExpression expression);
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/SyntaxErrorReportable.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/SyntaxErrorReportable.java
new file mode 100644
index 0000000..5628451
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/SyntaxErrorReportable.java
@@ -0,0 +1,43 @@
+/*
+ *  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.dsl;
+
+import org.codehaus.groovy.control.SourceUnit;
+import org.codehaus.groovy.control.messages.SyntaxErrorMessage;
+import org.codehaus.groovy.syntax.SyntaxException;
+
+/**
+ * Supports reporting the syntax error of GINQ
+ *
+ * @since 4.0.0
+ */
+public interface SyntaxErrorReportable {
+    SourceUnit getSourceUnit();
+
+    default void collectSyntaxError(GinqSyntaxError ginqSyntaxError) {
+        SourceUnit sourceUnit = getSourceUnit();
+
+        SyntaxException e = new SyntaxException(
+                ginqSyntaxError.getMessage(),
+                ginqSyntaxError,
+                ginqSyntaxError.getLine(),
+                ginqSyntaxError.getColumn());
+        sourceUnit.getErrorCollector().addFatalError(new SyntaxErrorMessage(e, sourceUnit));
+    }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/AbstractGinqExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/AbstractGinqExpression.java
new file mode 100644
index 0000000..5cc3c36
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/AbstractGinqExpression.java
@@ -0,0 +1,69 @@
+package org.apache.groovy.linq.dsl.expression;
+
+import org.codehaus.groovy.ast.ASTNode;
+import org.codehaus.groovy.ast.NodeMetaDataHandler;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Represents GINQ expression which could hold meta data
+ *
+ * @since 4.0.0
+ */
+public abstract class AbstractGinqExpression implements GinqExpression, NodeMetaDataHandler {
+    private Map<?, ?> metaDataMap = new LinkedHashMap<>();
+    private int lineNumber = -1;
+    private int columnNumber = -1;
+    private int lastLineNumber = -1;
+    private int lastColumnNumber = -1;
+
+    @Override
+    public Map<?, ?> getMetaDataMap() {
+        return metaDataMap;
+    }
+
+    @Override
+    public void setMetaDataMap(Map<?, ?> metaDataMap) {
+        this.metaDataMap = metaDataMap;
+    }
+
+    public void setSourcePosition(ASTNode node) {
+        this.lineNumber = node.getLineNumber();
+        this.columnNumber = node.getColumnNumber();
+        this.lastLineNumber = node.getLastLineNumber();
+        this.lastColumnNumber = node.getLastColumnNumber();
+    }
+
+    public int getLineNumber() {
+        return lineNumber;
+    }
+
+    public void setLineNumber(int lineNumber) {
+        this.lineNumber = lineNumber;
+    }
+
+    public int getColumnNumber() {
+        return columnNumber;
+    }
+
+    public void setColumnNumber(int columnNumber) {
+        this.columnNumber = columnNumber;
+    }
+
+    public int getLastLineNumber() {
+        return lastLineNumber;
+    }
+
+    public void setLastLineNumber(int lastLineNumber) {
+        this.lastLineNumber = lastLineNumber;
+    }
+
+    public int getLastColumnNumber() {
+        return lastColumnNumber;
+    }
+
+    public void setLastColumnNumber(int lastColumnNumber) {
+        this.lastColumnNumber = lastColumnNumber;
+    }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/DataSourceExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/DataSourceExpression.java
new file mode 100644
index 0000000..9961f14
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/DataSourceExpression.java
@@ -0,0 +1,43 @@
+/*
+ *  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.dsl.expression;
+
+import org.codehaus.groovy.ast.expr.Expression;
+
+/**
+ * Represents data source expression
+ *
+ * @since 4.0.0
+ */
+public abstract class DataSourceExpression extends FilterableExpression {
+    protected Expression aliasExpr;
+    protected Expression dataSourceExpr;
+
+    public DataSourceExpression(Expression aliasExpr, Expression dataSourceExpr) {
+        this.aliasExpr = aliasExpr;
+        this.dataSourceExpr = dataSourceExpr;
+    }
+
+    public Expression getAliasExpr() {
+        return aliasExpr;
+    }
+    public Expression getDataSourceExpr() {
+        return dataSourceExpr;
+    }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/FilterExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/FilterExpression.java
new file mode 100644
index 0000000..0fb4dde
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/FilterExpression.java
@@ -0,0 +1,42 @@
+/*
+ *  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.dsl.expression;
+
+import org.codehaus.groovy.ast.expr.Expression;
+
+/**
+ * Represents filter expression
+ *
+ * @since 4.0.0
+ */
+public abstract class FilterExpression extends AbstractGinqExpression {
+    protected Expression filterExpr;
+
+    public FilterExpression(Expression filterExpr) {
+        this.filterExpr = filterExpr;
+    }
+
+    public Expression getFilterExpr() {
+        return filterExpr;
+    }
+
+    public void setFilterExpr(Expression filterExpr) {
+        this.filterExpr = filterExpr;
+    }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/FilterableExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/FilterableExpression.java
new file mode 100644
index 0000000..2808438
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/FilterableExpression.java
@@ -0,0 +1,39 @@
+/*
+ *  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.dsl.expression;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents expression which could be filtered via {@code where} expression
+ *
+ * @since 4.0.0
+ */
+public abstract class FilterableExpression extends AbstractGinqExpression {
+    protected List<FilterExpression> filterExpressionList = new ArrayList<>();
+
+    public List<FilterExpression> getFilterExpressionList() {
+        return filterExpressionList;
+    }
+
+    public void addFilterExpression(FilterExpression filterExpression) {
+        this.filterExpressionList.add(filterExpression);
+    }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/FromExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/FromExpression.java
new file mode 100644
index 0000000..4133eec
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/FromExpression.java
@@ -0,0 +1,47 @@
+/*
+ *  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.dsl.expression;
+
+import org.apache.groovy.linq.dsl.GinqVisitor;
+import org.codehaus.groovy.ast.expr.Expression;
+
+/**
+ * Represents the from expression
+ *
+ * @since 4.0.0
+ */
+public class FromExpression extends DataSourceExpression {
+    public FromExpression(Expression aliasExpr, Expression dataSourceExpr) {
+        super(aliasExpr, dataSourceExpr);
+    }
+
+    @Override
+    public <R> R accept(GinqVisitor<R> visitor) {
+        return visitor.visitFromExpression(this);
+    }
+
+    @Override
+    public String toString() {
+        return "FromExpression{" +
+                "aliasExpr=" + aliasExpr +
+                ", dataSourceExpr=" + dataSourceExpr +
+                ", whereExpression=" + filterExpressionList.get(0) +
+                '}';
+    }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/GinqExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/GinqExpression.java
new file mode 100644
index 0000000..32ae24b
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/GinqExpression.java
@@ -0,0 +1,30 @@
+/*
+ *  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.dsl.expression;
+
+import org.apache.groovy.linq.dsl.GinqVisitor;
+
+/**
+ * Represents the GINQ expression
+ *
+ * @since 4.0.0
+ */
+public interface GinqExpression {
+    <R> R accept(GinqVisitor<R> visitor);
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/InnerJoinExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/InnerJoinExpression.java
new file mode 100644
index 0000000..8df0377
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/InnerJoinExpression.java
@@ -0,0 +1,39 @@
+/*
+ *  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.dsl.expression;
+
+import org.apache.groovy.linq.dsl.GinqVisitor;
+import org.codehaus.groovy.ast.expr.Expression;
+
+/**
+ * Represents inner join expression
+ *
+ * @since 4.0.0
+ */
+public class InnerJoinExpression extends JoinExpression {
+
+    public InnerJoinExpression(Expression aliasExpr, Expression dataSourceExpr) {
+        super(aliasExpr, dataSourceExpr);
+    }
+
+    @Override
+    public <R> R accept(GinqVisitor<R> visitor) {
+        return visitor.visitInnerJoinExpression(this);
+    }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/JoinExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/JoinExpression.java
new file mode 100644
index 0000000..4a98b36
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/JoinExpression.java
@@ -0,0 +1,42 @@
+/*
+ *  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.dsl.expression;
+
+import org.codehaus.groovy.ast.expr.Expression;
+
+/**
+ * Represents join expression
+ *
+ * @since 4.0.0
+ */
+public abstract class JoinExpression extends DataSourceExpression {
+    protected OnExpression onExpression;
+
+    public JoinExpression(Expression aliasExpr, Expression dataSourceExpr) {
+        super(aliasExpr, dataSourceExpr);
+    }
+
+    public OnExpression getOnExpression() {
+        return onExpression;
+    }
+
+    public void setOnExpression(OnExpression onExpression) {
+        this.onExpression = onExpression;
+    }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/OnExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/OnExpression.java
new file mode 100644
index 0000000..98394d7
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/OnExpression.java
@@ -0,0 +1,38 @@
+/*
+ *  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.dsl.expression;
+
+import org.apache.groovy.linq.dsl.GinqVisitor;
+import org.codehaus.groovy.ast.expr.Expression;
+
+/**
+ * Represents on expression
+ *
+ * @since 4.0.0
+ */
+public class OnExpression extends FilterExpression {
+    public OnExpression(Expression filterExpr) {
+        super(filterExpr);
+    }
+
+    @Override
+    public <R> R accept(GinqVisitor<R> visitor) {
+        return visitor.visitOnExpression(this);
+    }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/SelectExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/SelectExpression.java
new file mode 100644
index 0000000..82ec0c1
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/SelectExpression.java
@@ -0,0 +1,51 @@
+/*
+ *  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.dsl.expression;
+
+import org.apache.groovy.linq.dsl.GinqVisitor;
+import org.codehaus.groovy.ast.expr.Expression;
+
+/**
+ * Represents the select expression
+ *
+ * @since 4.0.0
+ */
+public class SelectExpression extends AbstractGinqExpression {
+    private final Expression projectionExpr;
+
+    public SelectExpression(Expression projectionExpr) {
+        this.projectionExpr = projectionExpr;
+    }
+
+    @Override
+    public <R> R accept(GinqVisitor<R> visitor) {
+        return visitor.visitSelectExpression(this);
+    }
+
+    public Expression getProjectionExpr() {
+        return projectionExpr;
+    }
+
+    @Override
+    public String toString() {
+        return "SelectExpression{" +
+                "projectionExpr=" + projectionExpr +
+                '}';
+    }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/SimpleGinqExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/SimpleGinqExpression.java
new file mode 100644
index 0000000..43abe75
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/SimpleGinqExpression.java
@@ -0,0 +1,72 @@
+/*
+ *  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.dsl.expression;
+
+import org.apache.groovy.linq.dsl.GinqVisitor;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represent the root expression of GINQ
+ *
+ * @since 4.0.0
+ */
+public class SimpleGinqExpression extends AbstractGinqExpression {
+    private FromExpression fromExpression;
+    private final List<JoinExpression> joinExpressionList = new ArrayList<>();
+    private SelectExpression selectExpression;
+
+    @Override
+    public <R> R accept(GinqVisitor<R> visitor) {
+        return visitor.visitSimpleGinqExpression(this);
+    }
+
+    public FromExpression getFromExpression() {
+        return fromExpression;
+    }
+
+    public void setFromExpression(FromExpression fromExpression) {
+        this.fromExpression = fromExpression;
+    }
+
+    public List<JoinExpression> getJoinExpressionList() {
+        return joinExpressionList;
+    }
+
+    public void addJoinExpression(JoinExpression joinExpression) {
+        joinExpressionList.add(joinExpression);
+    }
+
+    public SelectExpression getSelectExpression() {
+        return selectExpression;
+    }
+
+    public void setSelectExpression(SelectExpression selectExpression) {
+        this.selectExpression = selectExpression;
+    }
+
+    @Override
+    public String toString() {
+        return "SimpleGinqExpression{" +
+                "fromExpression=" + fromExpression +
+                ", selectExpression=" + selectExpression +
+                '}';
+    }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/WhereExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/WhereExpression.java
new file mode 100644
index 0000000..66bde4f
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/WhereExpression.java
@@ -0,0 +1,38 @@
+/*
+ *  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.dsl.expression;
+
+import org.apache.groovy.linq.dsl.GinqVisitor;
+import org.codehaus.groovy.ast.expr.Expression;
+
+/**
+ * Represent the where expression
+ *
+ * @since 4.0.0
+ */
+public class WhereExpression extends FilterExpression {
+    public WhereExpression(Expression filterExpr) {
+        super(filterExpr);
+    }
+
+    @Override
+    public <R> R accept(GinqVisitor<R> visitor) {
+        return visitor.visitWhereExpression(this);
+    }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/provider/QueryableCollection.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/provider/QueryableCollection.java
new file mode 100644
index 0000000..9d5d19d
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/provider/QueryableCollection.java
@@ -0,0 +1,251 @@
+/*
+ *  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 groovy.transform.Internal;
+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
+ */
+@Internal
+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;
+    }
+
+    @Override
+    public String toString() {
+        return toList().toString();
+    }
+}
diff --git a/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/GinqErrorTest.groovy b/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/GinqErrorTest.groovy
new file mode 100644
index 0000000..0fedabd
--- /dev/null
+++ b/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/GinqErrorTest.groovy
@@ -0,0 +1,82 @@
+/*
+ *  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.transform.CompileStatic
+import org.junit.Test
+
+import static groovy.test.GroovyAssert.shouldFail
+
+@CompileStatic
+class GinqErrorTest {
+    @Test
+    void "testGinq - from select - 1"() {
+        def err = shouldFail '''\
+            def numbers = [0, 1, 2]
+            GINQ {
+                from numbers
+                select n
+            }
+        '''
+
+        assert err.toString().contains('`in` is expected for `from`, e.g. `from n in nums` @ line 3, column 17.')
+    }
+
+    @Test
+    void "testGinq - from select - 2"() {
+        def err = shouldFail '''\
+            def numbers = [0, 1, 2]
+            GINQ {
+                from n as numbers
+                select n
+            }
+        '''
+
+        assert err.toString().contains('`in` is expected for `from`, e.g. `from n in nums` @ line 3, column 17.')
+    }
+
+    @Test
+    void "testGinq - from select - 3"() {
+        def err = shouldFail '''\
+            def numbers = [0, 1, 2]
+            GINQ {
+                from n, numbers
+                select n
+            }
+        '''
+
+        assert err.toString().contains('Only 1 argument expected for `from`, e.g. `from n in nums` @ line 3, column 17.')
+    }
+
+    @Test
+    void "testGinq - from innerJoin select - 1"() {
+        def err = shouldFail '''\
+            def nums1 = [1, 2, 3]
+            def nums2 = [1, 2, 3]
+            assert [[1, 1], [2, 2], [3, 3]] == GINQ {
+                from n1 in nums1
+                innerJoin n2 in nums2
+                select n1, n2
+            }.toList()
+        '''
+
+        assert err.toString().contains('`on` clause is expected for `innerJoin` @ line 5, column 17.')
+    }
+
+}
diff --git a/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/GinqTest.groovy b/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/GinqTest.groovy
new file mode 100644
index 0000000..e10301d
--- /dev/null
+++ b/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/GinqTest.groovy
@@ -0,0 +1,380 @@
+/*
+ *  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.transform.CompileStatic
+import org.junit.Test
+
+import static groovy.test.GroovyAssert.assertScript
+
+
+@CompileStatic
+class GinqTest {
+    @Test
+    void "testGinq - from select - 1"() {
+        assertScript '''
+            def numbers = [0, 1, 2]
+            assert [0, 1, 2] == GINQ {
+                from n in numbers
+                select n
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from select - 2"() {
+        assertScript '''
+            def numbers = [0, 1, 2]
+            assert [0, 2, 4] == GINQ {
+                from n in numbers
+                select n * 2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from select - 3"() {
+        assertScript '''
+            class Person {
+                String name
+                int age
+                
+                Person(String name, int age) {
+                    this.name = name
+                    this.age = age
+                }
+            }
+
+            def persons = [new Person('Daniel', 35), new Person('Linda', 21), new Person('Peter', 30)]
+            assert [35, 21, 30] == GINQ {
+                from p in persons
+                select p.age
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from select - 4"() {
+        assertScript '''
+            class Person {
+                String name
+                int age
+                
+                Person(String name, int age) {
+                    this.name = name
+                    this.age = age
+                }
+            }
+
+            def persons = [new Person('Daniel', 35), new Person('Linda', 21), new Person('Peter', 30)]
+            assert [['Daniel', 35], ['Linda', 21], ['Peter', 30]] == GINQ {
+                from p in persons
+                select p.name, p.age
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from select - 5"() {
+        assertScript '''
+            class Person {
+                String name
+                int age
+                
+                Person(String name, int age) {
+                    this.name = name
+                    this.age = age
+                }
+            }
+
+            def persons = [new Person('Daniel', 35), new Person('Linda', 21), new Person('Peter', 30)]
+            assert [[name:'Daniel', age:35], [name:'Linda', age:21], [name:'Peter', age:30]] == GINQ {
+                from p in persons
+                select (name: p.name, age: p.age)
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from select - 6"() {
+        assertScript '''
+            def numbers = [0, 1, 2]
+            assert [0, 1, 2] == GINQ {
+                from n in numbers select n
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from where select - 1"() {
+        assertScript '''
+            def numbers = [0, 1, 2, 3, 4, 5]
+            assert [2, 4, 6] == GINQ {
+                from n in numbers
+                where n > 0 && n <= 3
+                select n * 2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from where select - 2"() {
+        assertScript '''
+            def numbers = [0, 1, 2, 3, 4, 5]
+            assert [2, 4, 6] == GINQ {
+                from n in numbers where n > 0 && n <= 3 select n * 2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from innerJoin select - 1"() {
+        assertScript '''
+            def nums1 = [1, 2, 3]
+            def nums2 = [1, 2, 3]
+            assert [[1, 1], [2, 2], [3, 3]] == GINQ {
+                from n1 in nums1
+                innerJoin n2 in nums2
+                on n1 == n2
+                select n1, n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from innerJoin select - 2"() {
+        assertScript '''
+            def nums1 = [1, 2, 3]
+            def nums2 = [1, 2, 3]
+            assert [[2, 1], [3, 2], [4, 3]] == GINQ {
+                from n1 in nums1
+                innerJoin n2 in nums2
+                on n1 == n2
+                select n1 + 1, n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from innerJoin select - 3"() {
+        assertScript '''
+            def nums1 = [1, 2, 3]
+            def nums2 = [1, 2, 3]
+            assert [[1, 2], [2, 3], [3, 4]] == GINQ {
+                from n1 in nums1
+                innerJoin n2 in nums2
+                on n1 == n2
+                select n1, n2 + 1
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from innerJoin select - 4"() {
+        assertScript '''
+            def nums1 = [1, 2, 3]
+            def nums2 = [1, 2, 3]
+            assert [[1, 2], [2, 3]] == GINQ {
+                from n1 in nums1
+                innerJoin n2 in nums2
+                on n1 + 1 == n2
+                select n1, n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from innerJoin select - 5"() {
+        assertScript '''
+            def nums1 = [1, 2, 3]
+            def nums2 = [1, 2, 3]
+            assert [[1, 2], [2, 3]] == GINQ {
+                from n1 in nums1 innerJoin n2 in nums2 on n1 + 1 == n2 select n1, n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from innerJoin select - 6"() {
+        assertScript '''
+            class Person {
+                String name
+                int age
+                
+                Person(String name, int age) {
+                    this.name = name
+                    this.age = age
+                }
+            }
+
+            def persons1 = [new Person('Daniel', 35), new Person('Linda', 21), new Person('Peter', 30)]
+            def persons2 = [new Person('Jack', 35), new Person('Rose', 21), new Person('Smith', 30)]
+            assert [['Daniel', 'Jack'], ['Linda', 'Rose'], ['Peter', 'Smith']] == GINQ {
+                from p1 in persons1
+                innerJoin p2 in persons2
+                on p1.age == p2.age
+                select p1.name, p2.name
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from innerJoin select - 7"() {
+        assertScript '''
+            class Person {
+                String name
+                int age
+                
+                Person(String name, int age) {
+                    this.name = name
+                    this.age = age
+                }
+            }
+            
+            def persons1 = [new Person('Daniel', 35), new Person('Linda', 21), new Person('Peter', 30)]
+            def persons2 = [new Person('Jack', 35), new Person('Rose', 21), new Person('Smith', 30)]
+            assert [['DANIEL', 'JACK'], ['LINDA', 'ROSE'], ['PETER', 'SMITH']] == GINQ {
+                from p1 in persons1
+                innerJoin p2 in persons2
+                on p1.age == p2.age
+                select p1.name.toUpperCase(), p2.name.toUpperCase()
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from innerJoin select - 8"() {
+        assertScript '''
+            class Person {
+                String name
+                int age
+                
+                Person(String name, int age) {
+                    this.name = name
+                    this.age = age
+                }
+            }
+            
+            def same(str) { str }
+
+            def persons1 = [new Person('Daniel', 35), new Person('Linda', 21), new Person('Peter', 30)]
+            def persons2 = [new Person('Jack', 35), new Person('Rose', 21), new Person('Smith', 30)]
+            assert [['DANIEL', 'JACK'], ['LINDA', 'ROSE'], ['PETER', 'SMITH']] == GINQ {
+                from p1 in persons1
+                innerJoin p2 in persons2
+                on p1.age == p2.age
+                select same(p1.name.toUpperCase()), same(p2.name.toUpperCase())
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from innerJoin where select - 1"() {
+        assertScript '''
+            def nums1 = [1, 2, 3]
+            def nums2 = [1, 2, 3]
+            assert [[2, 2], [3, 3]] == GINQ {
+                from n1 in nums1
+                innerJoin n2 in nums2
+                on n1 == n2
+                where n1 > 1 && n2 <= 3
+                select n1, n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from innerJoin where select - 2"() {
+        assertScript '''
+            def nums1 = [1, 2, 3]
+            def nums2 = [1, 2, 3]
+            assert [[2, 2], [3, 3]] == GINQ {
+                from n1 in nums1
+                innerJoin n2 in nums2
+                on n1 == n2
+                where Math.pow(n1, 1) > 1 && Math.pow(n2, 1) <= 3
+                select n1, n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from innerJoin where select - 3"() {
+        assertScript '''
+            def nums1 = [1, 2, 3]
+            def nums2 = [1, 2, 3]
+            assert [[2, 2], [3, 3]] == GINQ {
+                from n1 in nums1 innerJoin n2 in nums2 on n1 == n2 where Math.pow(n1, 1) > 1 && Math.pow(n2, 1) <= 3 select n1, n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from innerJoin where select - 4"() {
+        assertScript '''
+            class Person {
+                String name
+                int age
+                
+                Person(String name, int age) {
+                    this.name = name
+                    this.age = age
+                }
+            }
+
+            def persons1 = [new Person('Daniel', 35), new Person('Linda', 21), new Person('David', 30)]
+            def persons2 = [new Person('Jack', 35), new Person('Rose', 21), new Person('Smith', 30)]
+            assert [['Daniel', 'Jack']] == GINQ {
+                from p1 in persons1
+                innerJoin p2 in persons2
+                on p1.age == p2.age
+                where p1.name.startsWith('D') && p2.name.endsWith('k')
+                select p1.name, p2.name
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from innerJoin where select - 5"() {
+        assertScript '''
+            class Person {
+                String name
+                int age
+                
+                Person(String name, int age) {
+                    this.name = name
+                    this.age = age
+                }
+            }
+            
+            def same(obj) {obj}
+
+            def persons1 = [new Person('Daniel', 35), new Person('Linda', 21), new Person('David', 30)]
+            def persons2 = [new Person('Jack', 35), new Person('Rose', 21), new Person('Smith', 30)]
+            assert [['Daniel', 'Jack']] == GINQ {
+                from p1 in persons1
+                innerJoin p2 in persons2
+                on p1.age == p2.age
+                where same(p1.name.startsWith('D')) && same(p2.name.endsWith('k'))
+                select p1.name, p2.name
+            }.toList()
+        '''
+    }
+}
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..344ffa4
--- /dev/null
+++ b/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/provider/QueryableCollectionTest.groovy
@@ -0,0 +1,407 @@
+/*
+ *  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.Collectors
+import java.util.stream.Stream
+
+import static org.apache.groovy.linq.provider.QueryableCollection.from
+
+@CompileStatic
+class QueryableCollectionTest {
+    @Test
+    void testFrom() {
+        assert [1, 2, 3] == from(Stream.of(1, 2, 3)).toList()
+        assert [1, 2, 3] == from(Arrays.asList(1, 2, 3)).toList()
+    }
+
+    @Test
+    void testInnerJoin0() {
+        def nums1 = [1, 2, 3]
+        def nums2 = [1, 2, 3]
+        def result = from(nums1).innerJoin(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 = from(nums1).innerJoin(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 = from(nums1).leftJoin(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 = from(nums1).rightJoin(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 = from(nums1).leftJoin(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 = from(nums1).rightJoin(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 = from(nums1).leftJoin(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 = from(nums1).rightJoin(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 = from(nums1).leftJoin(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 = from(nums1).rightJoin(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 = from(nums1).leftJoin(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 = from(nums1).rightJoin(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 = from(nums1).leftJoin(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 = from(nums1).rightJoin(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 = from(nums1).leftJoin(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 = from(nums1).rightJoin(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 = from(nums1).leftJoin(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 = from(nums1).rightJoin(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 = from(nums1).leftJoin(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 = from(nums1).rightJoin(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 = from(nums1).leftJoin(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 = from(nums1).rightJoin(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 = from(nums1).fullJoin(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 = from(nums1).crossJoin(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 = 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 = 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 = 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 =
+                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 =
+                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 = 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 = 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 = 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 = 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 = from(nums).limit(1, 2).toList()
+        assert [2, 3] == result
+
+        result = from(nums).limit(2).toList()
+        assert [1, 2] == result
+    }
+
+    @Test
+    void testSelect() {
+        def nums = [1, 2, 3, 4, 5]
+        def result = 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 = 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 = from(nums1).union(from(nums2)).toList()
+        assert [1, 2, 3, 4] == result
+    }
+
+    @Test
+    void testUnionAll() {
+        def nums1 = [1, 2, 3]
+        def nums2 = [2, 3, 4]
+        def result = from(nums1).unionAll(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 = from(nums1).intersect(from(nums2)).toList()
+        assert [2, 3] == result
+    }
+
+    @Test
+    void testMinus() {
+        def nums1 = [1, 1, 2, 3]
+        def nums2 = [2, 3, 4]
+        def result = from(nums1).minus(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 =
+                from(nums1)
+                        .innerJoin(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 testStream() {
+        def nums = [1, 2, 3]
+        def result = from(nums).stream().collect(Collectors.toList())
+        assert nums == result
+    }
+
+    @ToString
+    @EqualsAndHashCode
+    static class Person {
+        String name
+        int age
+
+        Person(String name, int age) {
+            this.name = name
+            this.age = age
+        }
+    }
+}