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/11 09:11:14 UTC

[groovy] 01/03: GROOVY-8258: [GEP] Create a LINQ-like DSL

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 12b2b884fa8192812559f44a53242fb906dafed1
Author: Daniel Sun <su...@apache.org>
AuthorDate: Sat Oct 10 19:21:50 2020 +0800

    GROOVY-8258: [GEP] Create a LINQ-like DSL
---
 settings.gradle                                    |    1 +
 .../codehaus/groovy/ast/tools/GeneralUtils.java    |    9 +
 subprojects/groovy-linq/build.gradle               |   35 +
 .../apache/groovy/linq/GinqGroovyMethods.groovy    |   47 +
 .../org/apache/groovy/linq/dsl/GinqAstBuilder.java |  211 +++
 .../apache/groovy/linq/dsl/GinqSyntaxError.java    |   43 +
 .../org/apache/groovy/linq/dsl/GinqVisitor.java    |   47 +
 .../groovy/linq/dsl/SyntaxErrorReportable.java     |   43 +
 .../dsl/expression/AbstractGinqExpression.java     |   38 +
 .../linq/dsl/expression/DataSourceExpression.java  |   70 +
 .../linq/dsl/expression/FilterExpression.java      |   42 +
 .../groovy/linq/dsl/expression/FromExpression.java |   47 +
 .../groovy/linq/dsl/expression/GinqExpression.java |   72 +
 .../linq/dsl/expression/GroupExpression.java       |   44 +
 .../groovy/linq/dsl/expression/JoinExpression.java |   67 +
 .../groovy/linq/dsl/expression/OnExpression.java   |   38 +
 .../linq/dsl/expression/OrderExpression.java       |   44 +
 .../linq/dsl/expression/SelectExpression.java      |   51 +
 .../linq/dsl/expression/WhereExpression.java       |   38 +
 .../linq/provider/collection/GinqAstWalker.groovy  |  490 +++++++
 .../linq/provider/collection/NamedTuple.groovy     |   58 +
 .../groovy/linq/provider/collection/Queryable.java |  135 ++
 .../provider/collection/QueryableCollection.java   |  263 ++++
 .../org/apache/groovy/linq/GinqErrorTest.groovy    |   82 ++
 .../groovy/org/apache/groovy/linq/GinqTest.groovy  | 1445 ++++++++++++++++++++
 .../linq/provider/collection/NamedTupleTest.groovy |   31 +
 .../collection/QueryableCollectionTest.groovy      |  406 ++++++
 27 files changed, 3897 insertions(+)

diff --git a/settings.gradle b/settings.gradle
index d4a4425..8855341 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -49,6 +49,7 @@ def subprojects = [
         '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..5227c38
--- /dev/null
+++ b/subprojects/groovy-linq/build.gradle
@@ -0,0 +1,35 @@
+/*
+ *  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.
+ */
+plugins {
+    id 'org.apache.groovy-library'
+}
+
+dependencies {
+    implementation rootProject
+    implementation project(':groovy-macro')
+    testImplementation project(':groovy-test')
+    testImplementation project(':groovy-json')
+}
+
+groovyLibrary {
+    withoutBinaryCompatibilityChecks()
+    moduleDescriptor {
+        extensionClasses = 'org.apache.groovy.linq.GinqGroovyMethods'
+    }
+}
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..59d46a3
--- /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.expression.GinqExpression
+import org.apache.groovy.linq.provider.collection.GinqAstWalker
+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)
+        GinqExpression ginqExpression = ginqAstBuilder.getGinqExpression()
+
+        GinqAstWalker ginqBuilder = new GinqAstWalker(ctx.getSourceUnit())
+        MethodCallExpression selectMethodCallExpression = ginqBuilder.visitGinqExpression(ginqExpression)
+
+        return selectMethodCallExpression
+    }
+}
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..c446bc6
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqAstBuilder.java
@@ -0,0 +1,211 @@
+/*
+ *  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.AbstractGinqExpression;
+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.GroupExpression;
+import org.apache.groovy.linq.dsl.expression.JoinExpression;
+import org.apache.groovy.linq.dsl.expression.OnExpression;
+import org.apache.groovy.linq.dsl.expression.OrderExpression;
+import org.apache.groovy.linq.dsl.expression.SelectExpression;
+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;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+
+/**
+ * Build the AST for GINQ
+ *
+ * @since 4.0.0
+ */
+public class GinqAstBuilder extends CodeVisitorSupport implements SyntaxErrorReportable {
+    private Deque<GinqExpression> ginqExpressionStack = new ArrayDeque<>();
+    private GinqExpression latestGinqExpression;
+    private final SourceUnit sourceUnit;
+
+    public GinqAstBuilder(SourceUnit sourceUnit) {
+        this.sourceUnit = sourceUnit;
+    }
+
+    public GinqExpression getGinqExpression() {
+        return latestGinqExpression;
+    }
+
+    private void setLatestGinqExpressionClause(AbstractGinqExpression ginqExpressionClause) {
+        GinqExpression ginqExpression = ginqExpressionStack.peek();
+        ginqExpression.putNodeMetaData(__LATEST_GINQ_EXPRESSION_CLAUSE, ginqExpressionClause);
+    }
+
+    private AbstractGinqExpression getLatestGinqExpressionClause() {
+        GinqExpression ginqExpression = ginqExpressionStack.peek();
+        return ginqExpression.getNodeMetaData(__LATEST_GINQ_EXPRESSION_CLAUSE);
+    }
+
+    @Override
+    public void visitMethodCallExpression(MethodCallExpression call) {
+        super.visitMethodCallExpression(call);
+        final String methodName = call.getMethodAsString();
+
+        if ("from".equals(methodName)) {
+            ginqExpressionStack.push(new GinqExpression()); // store the result
+        }
+
+        GinqExpression currentGinqExpression = ginqExpressionStack.peek();
+        AbstractGinqExpression latestGinqExpressionClause = getLatestGinqExpressionClause();
+
+        if ("from".equals(methodName)  || JoinExpression.isJoinExpression(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;
+            if (null == latestGinqExpression) {
+                dataSourceExpr = binaryExpression.getRightExpression();
+            } else {
+                // use the nested linq expresion and clear it
+                dataSourceExpr = latestGinqExpression;
+                latestGinqExpression = null;
+            }
+
+            DataSourceExpression dataSourceExpression;
+            if ("from".equals(methodName)) {
+                dataSourceExpression = new FromExpression(aliasExpr, dataSourceExpr);
+                currentGinqExpression.setFromExpression((FromExpression) dataSourceExpression);
+            } else {
+                dataSourceExpression = new JoinExpression(methodName, aliasExpr, dataSourceExpr);
+                currentGinqExpression.addJoinExpression((JoinExpression) dataSourceExpression);
+            }
+            dataSourceExpression.setSourcePosition(call);
+            setLatestGinqExpressionClause(dataSourceExpression);
+
+            return;
+        }
+
+        if ("where".equals(methodName) || "on".equals(methodName)) {
+            Expression filterExpr = ((ArgumentListExpression) call.getArguments()).getExpression(0);
+
+            if (filterExpr instanceof BinaryExpression && ((BinaryExpression) filterExpr).getOperation().getType() == Types.KEYWORD_IN) {
+                if (null != latestGinqExpression) {
+                    // use the nested ginq and clear it
+                    ((BinaryExpression) filterExpr).setRightExpression(latestGinqExpression);
+                    latestGinqExpression = null;
+                }
+            }
+
+            FilterExpression filterExpression = null;
+            if ("where".equals(methodName)) {
+                filterExpression = new WhereExpression(filterExpr);
+            } else {
+                filterExpression = new OnExpression(filterExpr);
+            }
+
+            if (null == filterExpression) {
+                throw new GroovyBugError("Unknown method: " + methodName);
+            }
+
+            filterExpression.setSourcePosition(call);
+
+            if (latestGinqExpressionClause instanceof DataSourceExpression) {
+                if (filterExpression instanceof WhereExpression) {
+                    ((DataSourceExpression) latestGinqExpressionClause).setWhereExpression((WhereExpression) filterExpression);
+                } else {
+                    ((JoinExpression) latestGinqExpressionClause).setOnExpression((OnExpression) filterExpression);
+                }
+            } else {
+                throw new GroovyBugError("The preceding expression is not a DataSourceExpression: " + latestGinqExpressionClause);
+            }
+
+            return;
+        }
+
+        if ("groupby".equals(methodName)) {
+            GroupExpression groupExpression = new GroupExpression(call.getArguments());
+            groupExpression.setSourcePosition(call);
+
+            if (latestGinqExpressionClause instanceof DataSourceExpression) {
+                ((DataSourceExpression) latestGinqExpressionClause).setGroupExpression(groupExpression);
+            } else {
+                throw new GroovyBugError("The preceding expression is not a DataSourceExpression: " + latestGinqExpressionClause);
+            }
+
+            return;
+        }
+
+        if ("orderby".equals(methodName)) {
+            OrderExpression orderExpression = new OrderExpression(call.getArguments());
+            orderExpression.setSourcePosition(call);
+
+            if (latestGinqExpressionClause instanceof DataSourceExpression) {
+                ((DataSourceExpression) latestGinqExpressionClause).setOrderExpression(orderExpression);
+            } else {
+                throw new GroovyBugError("The preceding expression is not a DataSourceExpression: " + latestGinqExpressionClause);
+            }
+
+            return;
+        }
+
+        if ("select".equals(methodName)) {
+            SelectExpression selectExpression = new SelectExpression(call.getArguments());
+            selectExpression.setSourcePosition(call);
+
+            currentGinqExpression.setSelectExpression(selectExpression);
+            setLatestGinqExpressionClause(selectExpression);
+
+            latestGinqExpression = ginqExpressionStack.pop();
+
+            return;
+        }
+    }
+
+    @Override
+    public SourceUnit getSourceUnit() {
+        return sourceUnit;
+    }
+
+    private static final String __LATEST_GINQ_EXPRESSION_CLAUSE = "__latestGinqExpressionClause";
+}
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..8515b71
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqVisitor.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;
+
+import org.apache.groovy.linq.dsl.expression.AbstractGinqExpression;
+import org.apache.groovy.linq.dsl.expression.FromExpression;
+import org.apache.groovy.linq.dsl.expression.GinqExpression;
+import org.apache.groovy.linq.dsl.expression.GroupExpression;
+import org.apache.groovy.linq.dsl.expression.JoinExpression;
+import org.apache.groovy.linq.dsl.expression.OnExpression;
+import org.apache.groovy.linq.dsl.expression.OrderExpression;
+import org.apache.groovy.linq.dsl.expression.SelectExpression;
+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 visitGinqExpression(GinqExpression ginqExpression);
+    R visitFromExpression(FromExpression fromExpression);
+    R visitJoinExpression(JoinExpression joinExpression);
+    R visitOnExpression(OnExpression onExpression);
+    R visitWhereExpression(WhereExpression whereExpression);
+    R visitGroupExpression(GroupExpression groupExpression);
+    R visitOrderExpression(OrderExpression orderExpression);
+    R visitSelectExpression(SelectExpression selectExpression);
+    R visit(AbstractGinqExpression 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..5633bab
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/AbstractGinqExpression.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.NodeMetaDataHandler;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.expr.ExpressionTransformer;
+
+/**
+ * Represents GINQ expression which could hold meta data
+ *
+ * @since 4.0.0
+ */
+public abstract class AbstractGinqExpression extends Expression implements NodeMetaDataHandler {
+    @Override
+    public Expression transformExpression(ExpressionTransformer transformer) {
+        throw new UnsupportedOperationException("transform GINQ expression is not supported yet");
+    }
+
+    public abstract <R> R accept(GinqVisitor<R> visitor);
+}
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..88f27f7
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/DataSourceExpression.java
@@ -0,0 +1,70 @@
+/*
+ *  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 AbstractGinqExpression {
+    protected Expression aliasExpr;
+    protected Expression dataSourceExpr;
+    protected WhereExpression whereExpression;
+    protected GroupExpression groupExpression;
+    protected OrderExpression orderExpression;
+
+    public DataSourceExpression(Expression aliasExpr, Expression dataSourceExpr) {
+        this.aliasExpr = aliasExpr;
+        this.dataSourceExpr = dataSourceExpr;
+    }
+
+    public Expression getAliasExpr() {
+        return aliasExpr;
+    }
+    public Expression getDataSourceExpr() {
+        return dataSourceExpr;
+    }
+
+    public void setWhereExpression(WhereExpression whereExpression) {
+        this.whereExpression = whereExpression;
+    }
+
+    public WhereExpression getWhereExpression() {
+        return whereExpression;
+    }
+
+    public GroupExpression getGroupExpression() {
+        return groupExpression;
+    }
+
+    public void setGroupExpression(GroupExpression groupExpression) {
+        this.groupExpression = groupExpression;
+    }
+
+    public OrderExpression getOrderExpression() {
+        return orderExpression;
+    }
+
+    public void setOrderExpression(OrderExpression orderExpression) {
+        this.orderExpression = orderExpression;
+    }
+}
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/FromExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/FromExpression.java
new file mode 100644
index 0000000..42dde72
--- /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=" + whereExpression +
+                '}';
+    }
+}
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..3ab9126
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/GinqExpression.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 GinqExpression 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.visitGinqExpression(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 "GinqExpression{" +
+                "fromExpression=" + fromExpression +
+                ", selectExpression=" + selectExpression +
+                '}';
+    }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/GroupExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/GroupExpression.java
new file mode 100644
index 0000000..d5d56c8
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/GroupExpression.java
@@ -0,0 +1,44 @@
+/*
+ *  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 group by expression
+ *
+ * @since 4.0.0
+ */
+public class GroupExpression extends AbstractGinqExpression {
+    private final Expression classifierExpr;
+
+    public GroupExpression(Expression classifierExpr) {
+        this.classifierExpr = classifierExpr;
+    }
+
+    @Override
+    public <R> R accept(GinqVisitor<R> visitor) {
+        return visitor.visitGroupExpression(this);
+    }
+
+    public Expression getClassifierExpr() {
+        return classifierExpr;
+    }
+}
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..4642afd
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/JoinExpression.java
@@ -0,0 +1,67 @@
+/*
+ *  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;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Represents join expression
+ *
+ * @since 4.0.0
+ */
+public class JoinExpression extends DataSourceExpression {
+    private static final String CROSS_JOIN = "crossjoin";
+    private static final List<String> JOIN_NAME_LIST = Arrays.asList("innerjoin", "leftjoin", "rightjoin", "fulljoin", CROSS_JOIN);
+    private final String joinName;
+    private OnExpression onExpression;
+
+    public JoinExpression(String joinName, Expression aliasExpr, Expression dataSourceExpr) {
+        super(aliasExpr, dataSourceExpr);
+        this.joinName = joinName;
+    }
+
+    public static boolean isJoinExpression(String methodName) {
+        return JOIN_NAME_LIST.contains(methodName);
+    }
+
+    public boolean isCrossJoin() {
+        return CROSS_JOIN.equals(joinName);
+    }
+
+    @Override
+    public <R> R accept(GinqVisitor<R> visitor) {
+        return visitor.visitJoinExpression(this);
+    }
+
+    public String getJoinName() {
+        return joinName;
+    }
+
+    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/OrderExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/OrderExpression.java
new file mode 100644
index 0000000..8acbaad
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/OrderExpression.java
@@ -0,0 +1,44 @@
+/*
+ *  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 order by expression
+ *
+ * @since 4.0.0
+ */
+public class OrderExpression extends AbstractGinqExpression {
+    private final Expression ordersExpr;
+
+    public OrderExpression(Expression ordersExpr) {
+        this.ordersExpr = ordersExpr;
+    }
+
+    @Override
+    public <R> R accept(GinqVisitor<R> visitor) {
+        return visitor.visitOrderExpression(this);
+    }
+
+    public Expression getOrdersExpr() {
+        return ordersExpr;
+    }
+}
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/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/collection/GinqAstWalker.groovy b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/provider/collection/GinqAstWalker.groovy
new file mode 100644
index 0000000..98315a8
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/provider/collection/GinqAstWalker.groovy
@@ -0,0 +1,490 @@
+/*
+ *  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.collection
+
+import groovy.transform.CompileDynamic
+import groovy.transform.CompileStatic
+import org.apache.groovy.linq.dsl.GinqSyntaxError
+import org.apache.groovy.linq.dsl.GinqVisitor
+import org.apache.groovy.linq.dsl.SyntaxErrorReportable
+import org.apache.groovy.linq.dsl.expression.AbstractGinqExpression
+import org.apache.groovy.linq.dsl.expression.DataSourceExpression
+import org.apache.groovy.linq.dsl.expression.FromExpression
+import org.apache.groovy.linq.dsl.expression.GinqExpression
+import org.apache.groovy.linq.dsl.expression.GroupExpression
+import org.apache.groovy.linq.dsl.expression.JoinExpression
+import org.apache.groovy.linq.dsl.expression.OnExpression
+import org.apache.groovy.linq.dsl.expression.OrderExpression
+import org.apache.groovy.linq.dsl.expression.SelectExpression
+import org.apache.groovy.linq.dsl.expression.WhereExpression
+import org.codehaus.groovy.GroovyBugError
+import org.codehaus.groovy.ast.ClassHelper
+import org.codehaus.groovy.ast.expr.ArgumentListExpression
+import org.codehaus.groovy.ast.expr.BinaryExpression
+import org.codehaus.groovy.ast.expr.CastExpression
+import org.codehaus.groovy.ast.expr.ClassExpression
+import org.codehaus.groovy.ast.expr.ConstantExpression
+import org.codehaus.groovy.ast.expr.ConstructorCallExpression
+import org.codehaus.groovy.ast.expr.EmptyExpression
+import org.codehaus.groovy.ast.expr.Expression
+import org.codehaus.groovy.ast.expr.ExpressionTransformer
+import org.codehaus.groovy.ast.expr.GStringExpression
+import org.codehaus.groovy.ast.expr.LambdaExpression
+import org.codehaus.groovy.ast.expr.ListExpression
+import org.codehaus.groovy.ast.expr.MethodCallExpression
+import org.codehaus.groovy.ast.expr.PropertyExpression
+import org.codehaus.groovy.ast.expr.TupleExpression
+import org.codehaus.groovy.ast.expr.VariableExpression
+import org.codehaus.groovy.control.SourceUnit
+import org.codehaus.groovy.syntax.Types
+
+import java.util.stream.Collectors
+
+import static org.codehaus.groovy.ast.tools.GeneralUtils.args
+import static org.codehaus.groovy.ast.tools.GeneralUtils.callX
+import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorX
+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
+    private GinqExpression currentGinqExpression
+
+    GinqAstWalker(SourceUnit sourceUnit) {
+        this.sourceUnit = sourceUnit
+    }
+
+    @Override
+    MethodCallExpression visitGinqExpression(GinqExpression ginqExpression) {
+        this.currentGinqExpression = ginqExpression
+        FromExpression fromExpression = ginqExpression.getFromExpression()
+        MethodCallExpression fromMethodCallExpression = this.visitFromExpression(fromExpression)
+
+        MethodCallExpression selectMethodReceiver = fromMethodCallExpression
+
+        JoinExpression lastJoinExpression = null
+        MethodCallExpression lastJoinMethodCallExpression = null
+        for (JoinExpression joinExpression : ginqExpression.getJoinExpressionList()) {
+            joinExpression.putNodeMetaData(__METHOD_CALL_RECEIVER, lastJoinMethodCallExpression ?: fromMethodCallExpression)
+            joinExpression.putNodeMetaData(__DATA_SOURCE_EXPRESSION, lastJoinExpression ?: fromExpression)
+
+            lastJoinExpression = joinExpression
+            lastJoinMethodCallExpression = this.visitJoinExpression(lastJoinExpression)
+        }
+
+        if (lastJoinMethodCallExpression) {
+            selectMethodReceiver = lastJoinMethodCallExpression
+        }
+
+        SelectExpression selectExpression = ginqExpression.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 resultMethodCallExpression
+        MethodCallExpression fromMethodCallExpression = constructFromMethodCallExpression(fromExpression.dataSourceExpr)
+        resultMethodCallExpression = fromMethodCallExpression
+
+        WhereExpression whereExpression = fromExpression.whereExpression
+
+        return decorateDataSourceMethodCallExpression(resultMethodCallExpression, fromExpression, whereExpression)
+    }
+
+    private MethodCallExpression decorateDataSourceMethodCallExpression(MethodCallExpression dataSourceMethodCallExpression,
+                                                                        DataSourceExpression dataSourceExpression, WhereExpression whereExpression) {
+        if (whereExpression) {
+            whereExpression.putNodeMetaData(__DATA_SOURCE_EXPRESSION, dataSourceExpression)
+            whereExpression.putNodeMetaData(__METHOD_CALL_RECEIVER, dataSourceMethodCallExpression)
+
+            MethodCallExpression whereMethodCallExpression = visitWhereExpression(whereExpression)
+            dataSourceMethodCallExpression = whereMethodCallExpression
+        }
+
+        GroupExpression groupExpression = dataSourceExpression.groupExpression
+        if (groupExpression) {
+            groupExpression.putNodeMetaData(__DATA_SOURCE_EXPRESSION, dataSourceExpression)
+            groupExpression.putNodeMetaData(__METHOD_CALL_RECEIVER, dataSourceMethodCallExpression)
+
+            MethodCallExpression groupMethodCallExpression = visitGroupExpression(groupExpression)
+            dataSourceMethodCallExpression = groupMethodCallExpression
+        }
+
+        OrderExpression orderExpression = dataSourceExpression.orderExpression
+        if (orderExpression) {
+            orderExpression.putNodeMetaData(__DATA_SOURCE_EXPRESSION, dataSourceExpression)
+            orderExpression.putNodeMetaData(__METHOD_CALL_RECEIVER, dataSourceMethodCallExpression)
+
+            MethodCallExpression orderMethodCallExpression = visitOrderExpression(orderExpression)
+            dataSourceMethodCallExpression = orderMethodCallExpression
+        }
+
+        return dataSourceMethodCallExpression
+    }
+
+    @Override
+    MethodCallExpression visitJoinExpression(JoinExpression joinExpression) {
+        Expression receiver = joinExpression.getNodeMetaData(__METHOD_CALL_RECEIVER)
+        OnExpression onExpression = joinExpression.onExpression
+
+        if (!onExpression && !joinExpression.crossJoin) {
+            this.collectSyntaxError(
+                    new GinqSyntaxError(
+                            "`on` clause is expected for `" + joinExpression.joinName + "`",
+                            joinExpression.getLineNumber(), joinExpression.getColumnNumber()
+                    )
+            )
+        }
+
+        WhereExpression whereExpression = joinExpression.whereExpression
+
+        MethodCallExpression joinMethodCallExpression = constructJoinMethodCallExpression(receiver, joinExpression, onExpression, whereExpression)
+
+        return joinMethodCallExpression
+    }
+
+    @Override
+    MethodCallExpression visitOnExpression(OnExpression onExpression) {
+        return null // do nothing
+    }
+
+    @CompileDynamic
+    private MethodCallExpression constructFromMethodCallExpression(Expression dataSourceExpr) {
+        MethodCallExpression fromMethodCallExpression = macro {
+            $v{ makeQueryableCollectionClassExpression() }.from($v {
+                if (dataSourceExpr instanceof AbstractGinqExpression) {
+                    return this.visit((AbstractGinqExpression) dataSourceExpr)
+                } else {
+                    return dataSourceExpr
+                }
+            })
+        }
+
+        return fromMethodCallExpression
+    }
+
+    private MethodCallExpression constructJoinMethodCallExpression(
+            Expression receiver, JoinExpression joinExpression,
+            OnExpression onExpression, WhereExpression whereExpression) {
+
+        DataSourceExpression otherDataSourceExpression = joinExpression.getNodeMetaData(__DATA_SOURCE_EXPRESSION)
+        Expression otherAliasExpr = otherDataSourceExpression.aliasExpr
+
+        String otherParamName = otherAliasExpr.text
+        Expression filterExpr = EmptyExpression.INSTANCE
+        if (onExpression) {
+            filterExpr = onExpression.getFilterExpr()
+            Tuple2<String, Expression> paramNameAndLambdaCode = correctVariablesOfLambdaExpression(otherDataSourceExpression, filterExpr)
+            otherParamName = paramNameAndLambdaCode.v1
+            filterExpr = paramNameAndLambdaCode.v2
+        }
+
+        MethodCallExpression resultMethodCallExpression
+        MethodCallExpression joinMethodCallExpression = callX(receiver, joinExpression.joinName.replace('join', 'Join'),
+                args(
+                        constructFromMethodCallExpression(joinExpression.dataSourceExpr),
+                        null == onExpression ? EmptyExpression.INSTANCE : lambdaX(
+                                params(
+                                        param(ClassHelper.DYNAMIC_TYPE, otherParamName),
+                                        param(ClassHelper.DYNAMIC_TYPE, joinExpression.aliasExpr.text)
+                                ),
+                                stmt(filterExpr)
+                        )
+                )
+        )
+        resultMethodCallExpression = joinMethodCallExpression
+
+        if (joinExpression.crossJoin) {
+            // cross join does not need `on` clause
+            Expression lastArgumentExpression = ((ArgumentListExpression) joinMethodCallExpression.arguments).getExpressions().removeLast()
+            if (EmptyExpression.INSTANCE !== lastArgumentExpression) {
+                throw new GroovyBugError("Wrong argument removed")
+            }
+        }
+
+        return decorateDataSourceMethodCallExpression(resultMethodCallExpression, joinExpression, whereExpression)
+    }
+
+    @Override
+    MethodCallExpression visitWhereExpression(WhereExpression whereExpression) {
+        DataSourceExpression dataSourceExpression = whereExpression.getNodeMetaData(__DATA_SOURCE_EXPRESSION)
+        Expression fromMethodCallExpression = whereExpression.getNodeMetaData(__METHOD_CALL_RECEIVER)
+        Expression filterExpr = whereExpression.getFilterExpr()
+
+        if (filterExpr instanceof BinaryExpression && ((BinaryExpression) filterExpr).operation.type == Types.KEYWORD_IN) {
+            filterExpr = filterExpr.transformExpression(new ExpressionTransformer() {
+                @Override
+                Expression transform(Expression expression) {
+                    if (expression instanceof AbstractGinqExpression) {
+                        return callX((Expression) GinqAstWalker.this.visit((AbstractGinqExpression) expression), "toList")
+                    }
+
+                    return expression.transformExpression(this)
+                }
+            })
+        }
+
+        return callXWithLambda(fromMethodCallExpression, "where", dataSourceExpression, filterExpr)
+    }
+
+    @Override
+    MethodCallExpression visitGroupExpression(GroupExpression groupExpression) {
+        DataSourceExpression dataSourceExpression = groupExpression.getNodeMetaData(__DATA_SOURCE_EXPRESSION)
+        Expression groupMethodCallReceiver = groupExpression.getNodeMetaData(__METHOD_CALL_RECEIVER)
+        Expression classifierExpr = groupExpression.classifierExpr
+
+        List<Expression> argumentExpressionList = ((ArgumentListExpression) classifierExpr).getExpressions()
+        ConstructorCallExpression namedListCtorCallExpression = constructNamedListCtorCallExpression(argumentExpressionList)
+
+        MethodCallExpression groupMethodCallExpression = callXWithLambda(groupMethodCallReceiver, "groupBy2", dataSourceExpression, namedListCtorCallExpression)
+
+        this.currentGinqExpression.putNodeMetaData(__GROUP_BY, true)
+        return groupMethodCallExpression
+    }
+
+    @Override
+    MethodCallExpression visitOrderExpression(OrderExpression orderExpression) {
+        DataSourceExpression dataSourceExpression = orderExpression.getNodeMetaData(__DATA_SOURCE_EXPRESSION)
+        Expression orderMethodCallReceiver = orderExpression.getNodeMetaData(__METHOD_CALL_RECEIVER)
+        Expression ordersExpr = orderExpression.ordersExpr
+
+        List<Expression> argumentExpressionList = ((ArgumentListExpression) ordersExpr).getExpressions()
+        List<Expression> orderCtorCallExpressions = argumentExpressionList.stream().map(e -> {
+            Expression target = e
+            boolean asc = true
+            if (e instanceof BinaryExpression && e.operation.type == Types.KEYWORD_IN) {
+                target = e.leftExpression
+                asc = 'asc' == e.rightExpression.text
+            }
+
+            LambdaExpression lambdaExpression = constructLambdaExpression(dataSourceExpression, target)
+
+            return ctorX(ClassHelper.make(Queryable.Order.class), args(lambdaExpression, new ConstantExpression(asc)))
+        }).collect(Collectors.toList())
+
+        return callX(orderMethodCallReceiver, "orderBy", args(orderCtorCallExpressions))
+    }
+
+    @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) {
+            ConstructorCallExpression namedListCtorCallExpression = constructNamedListCtorCallExpression(expressionList)
+            lambdaCode = namedListCtorCallExpression
+        } else {
+            lambdaCode = expressionList.get(0)
+        }
+
+        return callXWithLambda(selectMethodReceiver, "select", dataSourceExpression, lambdaCode)
+    }
+
+    private static ConstructorCallExpression constructNamedListCtorCallExpression(List<Expression> expressionList) {
+        int expressionListSize = expressionList.size()
+        List<Expression> elementExpressionList = new ArrayList<>(expressionListSize)
+        List<Expression> nameExpressionList = new ArrayList<>(expressionListSize)
+        for (Expression e : expressionList) {
+            Expression elementExpression = e
+            Expression nameExpression = new ConstantExpression(e.text)
+
+            if (e instanceof CastExpression) {
+                elementExpression = e.expression
+                nameExpression = new ConstantExpression(e.type.text)
+            } else if (e instanceof PropertyExpression) {
+                if (e.property instanceof ConstantExpression) {
+                    elementExpression = e
+                    nameExpression = new ConstantExpression(e.property.text)
+                } else if (e.property instanceof GStringExpression) {
+                    elementExpression = e
+                    nameExpression = e.property
+                }
+            }
+            elementExpressionList << elementExpression
+            nameExpressionList << nameExpression
+        }
+
+        ConstructorCallExpression namedListCtorCallExpression = ctorX(ClassHelper.make(NamedTuple.class), args(new ListExpression(elementExpressionList), new ListExpression(nameExpressionList)))
+        return namedListCtorCallExpression
+    }
+
+    private Expression correctVariablesOfGinqExpression(DataSourceExpression dataSourceExpression, Expression expr) {
+        boolean isGroup = isGroupByVisited()
+        boolean isJoin = dataSourceExpression instanceof JoinExpression
+
+        def correctVars = { Expression expression ->
+            if (expression instanceof VariableExpression) {
+                Expression transformedExpression = null
+
+                if (isGroup) { //  groupby
+                    // in #1, we will correct receiver of built-in aggregate functions
+                    // the correct receiver is `__t.v2`, so we should not replace `__t` here
+                    if (__T != expression.text) {
+                        // replace `gk` in the groupby with `__t.v1.gk`, note: __t.v1 stores the group key
+                        transformedExpression = propX(propX(new VariableExpression(__T), 'v1'), expression.text)
+                    }
+                } else if (isJoin) {
+                    /*
+                     * `n1`(from node) join `n2` join `n3`  will construct a join tree:
+                     *
+                     *  __t (join node)
+                     *    |__ v2 (n3)
+                     *    |__ v1 (join node)
+                     *         |__ v2 (n2)
+                     *         |__ v1 (n1) (from node)
+                     *
+                     * Note: `__t` is a tuple with 2 elements
+                     * so  `n3`'s access path is `__t.v2`
+                     * and `n2`'s access path is `__t.v1.v2`
+                     * and `n1`'s access path is `__t.v1.v1`
+                     *
+                     * The following code shows how to construct the access path for variables
+                     */
+                    def prop = new VariableExpression(__T)
+                    for (DataSourceExpression dse = dataSourceExpression;
+                         null == transformedExpression && dse instanceof JoinExpression;
+                         dse = dse.getNodeMetaData(__DATA_SOURCE_EXPRESSION)) {
+
+                        DataSourceExpression otherDataSourceExpression = dse.getNodeMetaData(__DATA_SOURCE_EXPRESSION)
+                        Expression firstAliasExpr = otherDataSourceExpression?.aliasExpr ?: EmptyExpression.INSTANCE
+                        Expression secondAliasExpr = dse.aliasExpr
+
+                        if (firstAliasExpr.text == expression.text && otherDataSourceExpression !instanceof JoinExpression) {
+                            transformedExpression = propX(prop, 'v1')
+                        } else if (secondAliasExpr.text == expression.text) {
+                            transformedExpression = propX(prop, 'v2')
+                        } else { // not found
+                            prop = propX(prop, 'v1')
+                        }
+                    }
+                }
+                if (null != transformedExpression) {
+                    return transformedExpression
+                }
+            } else if (expression instanceof MethodCallExpression) {
+                // #1
+                if (isGroup) { // groupby
+                    if (expression.implicitThis) {
+                        String methodName = expression.methodAsString
+                        if ('count' == methodName && ((TupleExpression) expression.arguments).getExpressions().isEmpty()) {
+                            expression.objectExpression = propX(new VariableExpression(__T), 'v2')
+                            return expression
+                        }
+                    }
+                }
+            }
+
+            return expression
+        }
+
+        // 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) {
+                Expression transformedExpression = correctVars(expression)
+                if (transformedExpression !== expression) {
+                    return transformedExpression
+                }
+
+                return expression.transformExpression(this)
+            }
+        })
+
+        return correctVars(expr)
+    }
+
+    @Override
+    Object visit(AbstractGinqExpression expression) {
+        return expression.accept(this)
+    }
+
+    private MethodCallExpression callXWithLambda(Expression receiver, String methodName, DataSourceExpression dataSourceExpression, Expression lambdaCode) {
+        LambdaExpression lambdaExpression = constructLambdaExpression(dataSourceExpression, lambdaCode)
+
+        callXWithLambda(receiver, methodName, lambdaExpression)
+    }
+
+    private LambdaExpression constructLambdaExpression(DataSourceExpression dataSourceExpression, Expression lambdaCode) {
+        Tuple2<String, Expression> paramNameAndLambdaCode = correctVariablesOfLambdaExpression(dataSourceExpression, lambdaCode)
+
+        lambdaX(
+                params(param(ClassHelper.DYNAMIC_TYPE, paramNameAndLambdaCode.v1)),
+                stmt(paramNameAndLambdaCode.v2)
+        )
+    }
+
+    private Tuple2<String, Expression> correctVariablesOfLambdaExpression(DataSourceExpression dataSourceExpression, Expression lambdaCode) {
+        boolean isGroup = isGroupByVisited()
+
+        String lambdaParamName
+        if (dataSourceExpression instanceof JoinExpression || isGroup) {
+            lambdaParamName = __T
+            lambdaCode = correctVariablesOfGinqExpression(dataSourceExpression, lambdaCode)
+        } else {
+            lambdaParamName = dataSourceExpression.aliasExpr.text
+        }
+
+        return Tuple.tuple(lambdaParamName, lambdaCode)
+    }
+
+    private boolean isGroupByVisited() {
+        return currentGinqExpression.getNodeMetaData(__GROUP_BY) ?: false
+    }
+
+    private static MethodCallExpression callXWithLambda(Expression receiver, String methodName, LambdaExpression lambdaExpression) {
+        callX(
+                receiver,
+                methodName,
+                lambdaExpression
+        )
+    }
+
+    private static makeQueryableCollectionClassExpression() {
+        new ClassExpression(ClassHelper.make(Queryable.class))
+    }
+
+    @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"
+    private static final String __GROUP_BY = "__GROUP_BY"
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/provider/collection/NamedTuple.groovy b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/provider/collection/NamedTuple.groovy
new file mode 100644
index 0000000..f0d5910
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/provider/collection/NamedTuple.groovy
@@ -0,0 +1,58 @@
+/*
+ *  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.collection
+
+
+import groovy.transform.CompileStatic
+
+/**
+ * Immutable named list to represent list result of GINQ
+ *
+ * @since 4.0.0
+ */
+@CompileStatic
+class NamedTuple<E> extends Tuple<E> {
+    private final List<String> nameList
+
+    NamedTuple(List<E> elementList, List<String> nameList) {
+        super(elementList as E[])
+        this.nameList = nameList
+    }
+
+    E getAt(String name) {
+        final int index = nameList.indexOf(name)
+
+        if (-1 == index) {
+            throw new IndexOutOfBoundsException("Failed to find element with name: $name")
+        }
+
+        return get(index)
+    }
+
+    E get(String name) {
+        return getAt(name)
+    }
+
+    @Override
+    String toString() {
+        '(' + nameList.withIndex()
+                .collect((String n, int i) -> { "${n}:${this[i]}" })
+                .join(', ') + ')'
+    }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/provider/collection/Queryable.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/provider/collection/Queryable.java
new file mode 100644
index 0000000..39fa431
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/provider/collection/Queryable.java
@@ -0,0 +1,135 @@
+/*
+ *  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.collection;
+
+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> {
+    static <T> Queryable<T> from(Iterable<T> sourceIterable) {
+        return new QueryableCollection<>(sourceIterable);
+    }
+
+    static <T> Queryable<T> from(Stream<? extends T> sourceStream) {
+        return new QueryableCollection<>(sourceStream);
+    }
+
+    <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, q) -> 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);
+    <R> R agg(Function<? super Queryable<? extends T>, ? extends R> 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/provider/collection/QueryableCollection.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/provider/collection/QueryableCollection.java
new file mode 100644
index 0000000..1e55141
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/provider/collection/QueryableCollection.java
@@ -0,0 +1,263 @@
+/*
+ *  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.collection;
+
+import groovy.lang.Tuple;
+import groovy.lang.Tuple2;
+import groovy.transform.Internal;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.Iterator;
+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;
+
+import static org.apache.groovy.linq.provider.collection.Queryable.from;
+
+/**
+ * Represents the queryable collections
+ *
+ * @param <T> the type of Queryable element
+ * @since 4.0.0
+ */
+@Internal
+class QueryableCollection<T> implements Queryable<T>, Iterable<T> {
+    private final Iterable<T> sourceIterable;
+
+    QueryableCollection(Iterable<T> sourceIterable) {
+        if (sourceIterable instanceof QueryableCollection) {
+            QueryableCollection<T> queryableCollection = (QueryableCollection<T>) sourceIterable;
+            this.sourceIterable = queryableCollection.sourceIterable;
+        } else {
+            this.sourceIterable = sourceIterable;
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    QueryableCollection(Stream<? extends T> sourceStream) {
+        this((Iterable<T>) toIterable(sourceStream));
+    }
+
+    @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);
+    }
+
+    /**
+     * Same to {@link #groupBy(Function, BiPredicate)}, workaround for the conflicts with DGM
+     */
+    public <K> Queryable<Tuple2<K, Queryable<T>>> groupBy2(Function<? super T, ? extends K> classifier, BiPredicate<? super K, ? super Queryable<? extends T>> having) {
+        return this.groupBy(classifier, having);
+    }
+
+    /**
+     * Same to {@link #groupBy(Function)}, workaround for the conflicts with DGM
+     */
+    public <K> Queryable<Tuple2<K, Queryable<T>>> groupBy2(Function<? super T, ? extends K> classifier) {
+        return this.groupBy(classifier);
+    }
+
+    @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() {
+        return toStream(sourceIterable);  // we have to create new stream every time because Java stream can not be reused
+    }
+
+    @Override
+    public int count() {
+        return agg(q -> q.toList().size());
+    }
+
+    @Override
+    public BigDecimal sum(Function<? super T, BigDecimal> mapper) {
+        return agg(q -> this.stream().map(mapper).reduce(BigDecimal.ZERO, BigDecimal::add));
+    }
+
+    @Override
+    public <R> R agg(Function<? super Queryable<? extends T>, ? extends R> mapper) {
+        return mapper.apply(this);
+    }
+
+    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 ArrayList<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.collect(Collectors.toList());
+    }
+
+    @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..8074604
--- /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..f1e654b
--- /dev/null
+++ b/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/GinqTest.groovy
@@ -0,0 +1,1445 @@
+/*
+ *  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.json.JsonSlurper
+import groovy.transform.CompileDynamic
+import groovy.transform.CompileStatic
+import org.junit.Test
+
+import static groovy.test.GroovyAssert.assertScript
+
+@CompileStatic
+class GinqTest {
+    @Test
+    void "testGinq - from select - 0"() {
+        assertScript '''
+            assert [0, 1, 2] == GINQ {
+                from n in [0, 1, 2]
+                select n
+            }.toList()
+        '''
+    }
+
+    @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 select - 9"() {
+        assertScript '''
+            assert [1, 2, 3] == GINQ {
+                from n in [1, 2, 3]
+                innerjoin k in [2, 3, 4] on n + 1 == k
+                select n
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from innerjoin select - 10"() {
+        assertScript '''
+            assert [2, 3, 4] == GINQ {
+                from n in [1, 2, 3]
+                innerjoin k in [2, 3, 4] on n + 1 == k
+                select k
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from innerjoin select - 11"() {
+        assertScript '''
+            def nums1 = [1, 2, 3]
+            def nums2 = [2, 3, 4]
+            def nums3 = [3, 4, 5]
+            assert [[3, 3, 3]] == GINQ {
+                from n1 in nums1
+                innerjoin n2 in nums2 on n2 == n1
+                innerjoin n3 in nums3 on n3 == n2
+                select n1, n2, n3
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from innerjoin select - 12"() {
+        assertScript '''
+            def nums1 = [1, 2, 3]
+            def nums2 = [2, 3, 4]
+            def nums3 = [3, 4, 5]
+            assert [[3, 3, 3]] == GINQ {
+                from n1 in nums1
+                innerjoin n2 in nums2 on n2 == n1
+                innerjoin n3 in nums3 on n3 == n1
+                select n1, n2, n3
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from innerjoin select - 13"() {
+        assertScript '''
+            def nums1 = [1, 2, 3]
+            def nums2 = [2, 3, 4]
+            def nums3 = [3, 4, 5]
+            assert [[3, 3, 3]] == GINQ {
+                from v in (
+                    from n1 in nums1
+                    innerjoin n2 in nums2 on n1 == n2
+                    select n1, n2
+                )
+                innerjoin n3 in nums3 on v.n2 == n3
+                select v.n1, v.n2, n3
+            }.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()
+        '''
+    }
+
+    @Test
+    void "testGinq - nested from - 0"() {
+        assertScript '''
+            assert [1, 2, 3] == GINQ {
+                from v in (
+                    from n in [1, 2, 3]
+                    select n
+                )
+                select v
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - nested from - 1"() {
+        assertScript '''
+            def numbers = [1, 2, 3]
+            assert [1, 2, 3] == GINQ {
+                from v in (
+                    from n in numbers
+                    select n
+                )
+                select v
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - nested from - 2"() {
+        assertScript '''
+            def numbers = [1, 2, 3]
+            assert [1, 2] == GINQ {
+                from v in (
+                    from n in numbers
+                    where n < 3
+                    select n
+                )
+                select v
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - nested from - 3"() {
+        assertScript '''
+            def numbers = [1, 2, 3]
+            assert [2] == GINQ {
+                from v in (
+                    from n in numbers
+                    where n < 3
+                    select n
+                )
+                where v > 1
+                select v
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - nested from - 4"() {
+        assertScript '''
+            def nums1 = [1, 2, 3, 4, 5]
+            def nums2 = [1, 2, 3, 4, 5]
+            assert [[3, 3], [5, 5]] == GINQ {
+                from v in (
+                    from n1 in nums1
+                    innerjoin n2 in nums2 on n1 == n2
+                    where n1 > 1 && n2 <= 5
+                    select n1, n2
+                )
+                where v.n1 >= 3 && v.n2 in [3, 5]
+                select v
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - nested from - 5"() {
+        assertScript '''
+            def nums1 = [1, 2, 3, 4, 5]
+            def nums2 = [1, 2, 3, 4, 5]
+            assert [[3, 3], [5, 5]] == GINQ {
+                from v in (
+                    from n1 in nums1
+                    innerjoin n2 in nums2 on n1 == n2
+                    where n1 > 1 && n2 <= 5
+                    select n1, n2
+                )
+                where v['n1'] >= 3 && v['n2'] in [3, 5]
+                select v
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - nested from - 6"() {
+        assertScript '''
+            def nums1 = [1, 2, 3, 4, 5]
+            def nums2 = [1, 2, 3, 4, 5]
+            assert [[3, 3], [5, 5]] == GINQ {
+                from v in (
+                    from n1 in nums1
+                    innerjoin n2 in nums2 on n1 == n2
+                    where n1 > 1 && n2 <= 5
+                    select n1, n2
+                )
+                where v[0] >= 3 && v[1] in [3, 5] // v[0] references column1 n1, and v[1] references column2 n2
+                select v
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - nested from - 7"() {
+        assertScript '''
+            def nums1 = [1, 2, 3, 4, 5]
+            def nums2 = [1, 2, 3, 4, 5]
+            assert [[3, 3], [5, 5]] == GINQ {
+                from v in (
+                    from n1 in nums1
+                    innerjoin n2 in nums2 on n1 == n2
+                    where n1 > 1 && n2 <= 5
+                    select n1 as vn1, n2 as vn2 // rename column names
+                )
+                where v.vn1 >= 3 && v.vn2 in [3, 5]
+                select v
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - nested from - 8"() {
+        assertScript '''
+            def nums1 = [1, 2, 3, 4, 5]
+            def nums2 = [1, 2, 3, 4, 5]
+            assert [[3, 3], [5, 5]] == GINQ {
+                from v in (
+                    from n1 in nums1
+                    innerjoin n2 in nums2 on n1 == n2
+                    where n1 > 1 && n2 <= 5
+                    select ((n1 as Integer) as vn1), ((n2 as Integer) as vn2)
+                )
+                where v.vn1 >= 3 && v.vn2 in [3, 5]
+                select v
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - nested from - 9"() {
+        assertScript '''
+            assert [2, 6] == GINQ {
+                from v in (
+                    from n in (
+                        from m in [1, 2, 3]
+                        select m as v1, (m + 1) as v2
+                    )
+                    where n.v2 < 4
+                    select n.v1 * n.v2
+                )
+                select v
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - nested from - 10"() {
+        assertScript '''
+            assert [2, 6] == GINQ {
+                from v in (
+                    from n in (
+                        from m in [1, 2, 3]
+                        select m, (m + 1) as v2
+                    )
+                    where n.v2 < 4
+                    select n.m * n.v2
+                )
+                select v
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - nested from - 11"() {
+        assertScript '''
+            assert [[1, 2], [2, 3]] == GINQ {
+                from v in (
+                    from n in (
+                        from m in [1, 2, 3]
+                        select m, (m + 1) as v2
+                    )
+                    where n.v2 < 4
+                    select n.m, n.v2   // its column names are: m, v2
+                )
+                select v.m, v.v2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - nested from - 12"() {
+        assertScript '''
+            assert [[1, 2], [2, 3]] == GINQ {
+                from v in (
+                    from n in (
+                        from m in [1, 2, 3]
+                        select m, (m + 1) as v2
+                    )
+                    where n.v2 < 4
+                    select n.m, n.v2
+                )
+                select v."${'m'}", v.v2   // dynamic column name
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - nested from - 13"() {
+        assertScript '''
+            assert [2, 6] == GINQ {
+                from v in (
+                    from n in (
+                        from m in [1, 2, 3]
+                        select m as v1, (m + 1) as v2
+                    )
+                    innerjoin k in [2, 3, 4] on n.v2 == k
+                    where n.v2 < 4
+                    select n.v1 * k
+                )
+                select v
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - nested from - 14"() {
+        assertScript '''
+            assert [2, 3] == GINQ {
+                from n in [1, 2, 3]
+                innerjoin k in (
+                    from m in [2, 3, 4]
+                    select m
+                ) on n == k
+                select n
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - nested from - 15"() {
+        assertScript '''
+            assert [1, 2] == GINQ {
+                from n in [0, 1, 2]
+                where n in (
+                    from m in [1, 2]
+                    select m
+                )
+                select n
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - nested from - 16"() {
+        assertScript '''
+            assert [[2, 2]] == GINQ {
+                from t in [[0, 0], [1, 1], [2, 2]]
+                where t in (
+                    from m in [1, 2]
+                    innerjoin k in [2, 3] on k == m
+                    select m, k
+                )
+                select t
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - nested from - 17"() {
+        assertScript '''
+            import static groovy.lang.Tuple.*
+            
+            @groovy.transform.EqualsAndHashCode
+            class Person {
+                String firstName
+                String lastName
+                int age
+                String gender
+                
+                Person(String firstName, String lastName, int age, String gender) {
+                    this.firstName = firstName
+                    this.lastName = lastName
+                    this.age = age
+                    this.gender = gender
+                }
+            }
+            @groovy.transform.EqualsAndHashCode
+            class LuckyInfo {
+                String lastName
+                String gender
+                boolean valid
+                
+                LuckyInfo(String lastName, String gender, boolean valid) {
+                    this.lastName = lastName
+                    this.gender = gender
+                    this.valid = valid
+                }
+            }
+            
+            def persons = [new Person('Daniel', 'Sun', 35, 'Male'), new Person('Linda', 'Yang', 21, 'Female'), 
+                          new Person('Peter', 'Yang', 30, 'Male'), new Person('Rose', 'Yang', 30, 'Female')]
+            def luckyInfoList = [new LuckyInfo('Sun', 'Male', true), new LuckyInfo('Yang', 'Female', true), 
+                                 new LuckyInfo('Yang', 'Male', false)]        
+
+            assert ['Daniel', 'Linda', 'Rose'] == GINQ {
+                from p in persons
+                where tuple(p.lastName, p.gender) in (
+                    from luckyInfo in luckyInfoList
+                    where luckyInfo.valid == true
+                    select luckyInfo.lastName, luckyInfo.gender
+                )
+                select p.firstName
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from leftjoin 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
+                leftjoin n2 in nums2 on n1 == n2
+                select n1, n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from leftjoin select - 2"() {
+        assertScript '''
+            def nums1 = [1, 2, 3]
+            def nums2 = [2, 3, 4]
+            assert [[1, null], [2, 2], [3, 3]] == GINQ {
+                from n1 in nums1
+                leftjoin n2 in nums2 on n1 == n2
+                select n1, n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from leftjoin select - 3"() {
+        assertScript '''
+            def nums1 = [1, 2, 3, null]
+            def nums2 = [2, 3, 4]
+            assert [[1, null], [2, 2], [3, 3], [null, null]] == GINQ {
+                from n1 in nums1
+                leftjoin n2 in nums2 on n1 == n2
+                select n1, n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from leftjoin select - 4"() {
+        assertScript '''
+            def nums1 = [1, 2, 3, null]
+            def nums2 = [2, 3, 4, null]
+            assert [[1, null], [2, 2], [3, 3], [null, null]] == GINQ {
+                from n1 in nums1
+                leftjoin n2 in nums2 on n1 == n2
+                select n1, n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from leftjoin select - 5"() {
+        assertScript '''
+            def nums1 = [1, 2, 3]
+            def nums2 = [2, 3, 4, null]
+            assert [[1, null], [2, 2], [3, 3]] == GINQ {
+                from n1 in nums1
+                leftjoin n2 in nums2 on n1 == n2
+                select n1, n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from leftjoin select - 6"() {
+        assertScript '''
+            def nums1 = [1, 2, 3, null, null]
+            def nums2 = [2, 3, 4]
+            assert [[1, null], [2, 2], [3, 3], [null, null], [null, null]] == GINQ {
+                from n1 in nums1
+                leftjoin n2 in nums2 on n1 == n2
+                select n1, n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from leftjoin select - 7"() {
+        assertScript '''
+            def nums1 = [1, 2, 3, null, null]
+            def nums2 = [2, 3, 4, null]
+            assert [[1, null], [2, 2], [3, 3], [null, null], [null, null]] == GINQ {
+                from n1 in nums1
+                leftjoin n2 in nums2 on n1 == n2
+                select n1, n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from leftjoin select - 8"() {
+        assertScript '''
+            def nums1 = [1, 2, 3, null, null]
+            def nums2 = [2, 3, 4, null, null]
+            assert [[1, null], [2, 2], [3, 3], [null, null], [null, null]] == GINQ {
+                from n1 in nums1
+                leftjoin n2 in nums2 on n1 == n2
+                select n1, n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from leftjoin select - 9"() {
+        assertScript '''
+            def nums1 = [1, 2, 3, null]
+            def nums2 = [2, 3, 4, null, null]
+            assert [[1, null], [2, 2], [3, 3], [null, null]] == GINQ {
+                from n1 in nums1
+                leftjoin n2 in nums2 on n1 == n2
+                select n1, n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from leftjoin select - 10"() {
+        assertScript '''
+            def nums1 = [1, 2, 3]
+            def nums2 = [2, 3, 4, null, null]
+            assert [[1, null], [2, 2], [3, 3]] == GINQ {
+                from n1 in nums1
+                leftjoin n2 in nums2 on n1 == n2
+                select n1, n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from leftjoin where select - 1"() {
+        assertScript '''
+            def nums1 = [1, 2, 3]
+            def nums2 = [2, 3, 4, null, null]
+            assert [[2, 2], [3, 3]] == GINQ {
+                from n1 in nums1
+                leftjoin n2 in nums2 on n1 == n2
+                where n2 != null
+                select n1, n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from rightjoin 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
+                rightjoin n2 in nums2 on n1 == n2
+                select n1, n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from rightjoin select - 2"() {
+        assertScript '''
+            def nums2 = [1, 2, 3]
+            def nums1 = [2, 3, 4]
+            assert [[null, 1], [2, 2], [3, 3]] == GINQ {
+                from n1 in nums1
+                rightjoin n2 in nums2 on n1 == n2
+                select n1, n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from rightjoin select - 3"() {
+        assertScript '''
+            def nums2 = [1, 2, 3, null]
+            def nums1 = [2, 3, 4]
+            assert [[null, 1], [2, 2], [3, 3], [null, null]] == GINQ {
+                from n1 in nums1
+                rightjoin n2 in nums2 on n1 == n2
+                select n1, n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from rightjoin select - 4"() {
+        assertScript '''
+            def nums2 = [1, 2, 3, null]
+            def nums1 = [2, 3, 4, null]
+            assert [[null, 1], [2, 2], [3, 3], [null, null]] == GINQ {
+                from n1 in nums1
+                rightjoin n2 in nums2 on n1 == n2
+                select n1, n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from rightjoin select - 5"() {
+        assertScript '''
+            def nums2 = [1, 2, 3]
+            def nums1 = [2, 3, 4, null]
+            assert [[null, 1], [2, 2], [3, 3]] == GINQ {
+                from n1 in nums1
+                rightjoin n2 in nums2 on n1 == n2
+                select n1, n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from rightjoin select - 6"() {
+        assertScript '''
+            def nums2 = [1, 2, 3, null, null]
+            def nums1 = [2, 3, 4]
+            assert [[null, 1], [2, 2], [3, 3], [null, null], [null, null]] == GINQ {
+                from n1 in nums1
+                rightjoin n2 in nums2 on n1 == n2
+                select n1, n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from rightjoin select - 7"() {
+        assertScript '''
+            def nums2 = [1, 2, 3, null, null]
+            def nums1 = [2, 3, 4, null]
+            assert [[null, 1], [2, 2], [3, 3], [null, null], [null, null]] == GINQ {
+                from n1 in nums1
+                rightjoin n2 in nums2 on n1 == n2
+                select n1, n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from rightjoin select - 8"() {
+        assertScript '''
+            def nums2 = [1, 2, 3, null, null]
+            def nums1 = [2, 3, 4, null, null]
+            assert [[null, 1], [2, 2], [3, 3], [null, null], [null, null]] == GINQ {
+                from n1 in nums1
+                rightjoin n2 in nums2 on n1 == n2
+                select n1, n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from rightjoin select - 9"() {
+        assertScript '''
+            def nums2 = [1, 2, 3, null]
+            def nums1 = [2, 3, 4, null, null]
+            assert [[null, 1], [2, 2], [3, 3], [null, null]] == GINQ {
+                from n1 in nums1
+                rightjoin n2 in nums2 on n1 == n2
+                select n1, n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from rightjoin select - 10"() {
+        assertScript '''
+            def nums2 = [1, 2, 3]
+            def nums1 = [2, 3, 4, null, null]
+            assert [[null, 1], [2, 2], [3, 3]] == GINQ {
+                from n1 in nums1
+                rightjoin n2 in nums2 on n1 == n2
+                select n1, n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from rightjoin where select - 1"() {
+        assertScript '''
+            def nums2 = [1, 2, 3]
+            def nums1 = [2, 3, 4, null, null]
+            assert [[2, 2], [3, 3]] == GINQ {
+                from n1 in nums1
+                rightjoin n2 in nums2 on n1 == n2
+                where n1 != null
+                select n1, n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from fulljoin select - 1"() {
+        assertScript '''
+            def nums1 = [1, 2, 3]
+            def nums2 = [2, 3, 4]
+            assert [[1, null], [2, 2], [3, 3], [null, 4]] == GINQ {
+                from n1 in nums1
+                fulljoin n2 in nums2 on n1 == n2
+                select n1, n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from fulljoin where select - 1"() {
+        assertScript '''
+            def nums1 = [1, 2, 3]
+            def nums2 = [2, 3, 4]
+            assert [[2, 2], [3, 3]] == GINQ {
+                from n1 in nums1
+                fulljoin n2 in nums2 on n1 == n2
+                where n1 != null && n2 != null 
+                select n1, n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from crossjoin select - 1"() {
+        assertScript '''
+            def nums1 = [1, 2, 3]
+            def nums2 = [3, 4, 5]
+            assert [[1, 3], [1, 4], [1, 5], [2, 3], [2, 4], [2, 5], [3, 3], [3, 4], [3, 5]] == GINQ {
+                from n1 in nums1
+                crossjoin n2 in nums2
+                select n1, n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from crossjoin where select - 1"() {
+        assertScript '''
+            def nums1 = [1, 2, 3]
+            def nums2 = [3, 4, 5]
+            assert [[3, 3], [3, 5]] == GINQ {
+                from n1 in nums1
+                crossjoin n2 in nums2
+                where n1 > 2 && n2 != 4
+                select n1, n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from orderby select - 1"() {
+        assertScript '''
+            assert [1, 2, 5, 6] == GINQ {
+                from n in [1, 5, 2, 6]
+                orderby n
+                select n
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from orderby select - 2"() {
+        assertScript '''
+            @groovy.transform.EqualsAndHashCode
+            class Person {
+                String name
+                int age
+                
+                Person(String name, int age) {
+                    this.name = name
+                    this.age = age
+                }
+            }
+            def persons = [new Person('Linda', 21), new Person('Daniel', 35), new Person('David', 21)]
+            assert [new Person('Daniel', 35), new Person('Linda', 21), new Person('David', 21)] == GINQ {
+                from p in persons
+                orderby p.age in desc
+                select p
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from orderby select - 3"() {
+        assertScript '''
+            @groovy.transform.EqualsAndHashCode
+            class Person {
+                String name
+                int age
+                
+                Person(String name, int age) {
+                    this.name = name
+                    this.age = age
+                }
+            }
+            def persons = [new Person('Linda', 21), new Person('Daniel', 35), new Person('David', 21)]
+            assert [new Person('Daniel', 35), new Person('David', 21), new Person('Linda', 21)] == GINQ {
+                from p in persons
+                orderby p.age in desc, p.name
+                select p
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from orderby select - 4"() {
+        assertScript '''
+            @groovy.transform.EqualsAndHashCode
+            class Person {
+                String name
+                int age
+                
+                Person(String name, int age) {
+                    this.name = name
+                    this.age = age
+                }
+            }
+            def persons = [new Person('Linda', 21), new Person('Daniel', 35), new Person('David', 21)]
+            assert [new Person('Daniel', 35), new Person('David', 21), new Person('Linda', 21)] == GINQ {
+                from p in persons
+                orderby p.age in desc, p.name in asc
+                select p
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from orderby select - 5"() {
+        assertScript '''
+            @groovy.transform.EqualsAndHashCode
+            class Person {
+                String name
+                int age
+                
+                Person(String name, int age) {
+                    this.name = name
+                    this.age = age
+                }
+            }
+            def persons = [new Person('Linda', 21), new Person('Daniel', 35), new Person('David', 21)]
+            assert [new Person('Daniel', 35), new Person('Linda', 21), new Person('David', 21)] == GINQ {
+                from p in persons
+                orderby p.age in desc, p.name in desc
+                select p
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from orderby select - 6"() {
+        assertScript '''
+            @groovy.transform.EqualsAndHashCode
+            class Person {
+                String name
+                int age
+                
+                Person(String name, int age) {
+                    this.name = name
+                    this.age = age
+                }
+            }
+            def persons = [new Person('Linda', 21), new Person('Daniel', 35), new Person('David', 21)]
+            assert [new Person('Linda', 21), new Person('David', 21), new Person('Daniel', 35)] == GINQ {
+                from p in persons
+                orderby p.age, p.name in desc
+                select p
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from orderby select - 7"() {
+        assertScript '''
+            @groovy.transform.EqualsAndHashCode
+            class Person {
+                String name
+                int age
+                
+                Person(String name, int age) {
+                    this.name = name
+                    this.age = age
+                }
+            }
+            def persons = [new Person('Linda', 21), new Person('Daniel', 35), new Person('David', 21)]
+            assert [new Person('Linda', 21), new Person('David', 21), new Person('Daniel', 35)] == GINQ {
+                from p in persons
+                orderby p.age in asc, p.name in desc
+                select p
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from orderby select - 8"() {
+        assertScript '''
+            assert [1, 2, 5, 6] == GINQ {
+                from n in [1, 5, 2, 6]
+                orderby 1 / n in desc
+                select n
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from orderby select - 9"() {
+        assertScript '''
+            assert [1, 2, 5, 6] == GINQ {
+                from n in [1, 5, 2, 6]
+                orderby Math.pow(1 / n, 1) in desc
+                select n
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from innerjoin orderby select - 1"() {
+        assertScript '''
+            assert [2, 3] == GINQ {
+                from n1 in [1, 2, 3]
+                innerjoin n2 in [2, 3, 4] on n1 == n2
+                orderby n1
+                select n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from innerjoin orderby select - 2"() {
+        assertScript '''
+            assert [3, 2] == GINQ {
+                from n1 in [1, 2, 3]
+                innerjoin n2 in [2, 3, 4] on n1 == n2
+                orderby n1 in desc
+                select n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from innerjoin orderby select - 3"() {
+        assertScript '''
+            assert [[3, 3], [2, 2]] == GINQ {
+                from n1 in [1, 2, 3]
+                innerjoin n2 in [2, 3, 4] on n1 == n2
+                orderby n1 in desc, n2 in desc
+                select n1, n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from groupby select - 1"() {
+        assertScript '''
+            assert [[1, 2], [3, 2], [6, 3]] == GINQ {
+                from n in [1, 1, 3, 3, 6, 6, 6]
+                groupby n
+                select n, count() // reference the column `n` in the groupby clause, and `count()` is a built-in aggregate function
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from where groupby select - 1"() {
+        assertScript '''
+            assert [[1, 2], [6, 3]] == GINQ {
+                from n in [1, 1, 3, 3, 6, 6, 6]
+                where n != 3
+                groupby n
+                select n, count()
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from where groupby orderby select - 1"() {
+        assertScript '''
+            assert [[6, 3], [1, 2]] == GINQ {
+                from n in [1, 1, 3, 3, 6, 6, 6]
+                where n != 3
+                groupby n
+                orderby n in desc
+                select n, count()
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from innerjoin where groupby orderby select - 1"() {
+        assertScript '''
+            assert [[6, 9], [1, 4]] == GINQ {
+                from n in [1, 1, 3, 3, 6, 6, 6]
+                innerjoin m in [1, 1, 3, 3, 6, 6, 6] on n == m
+                where n != 3
+                groupby n
+                orderby n in desc
+                select n, count()
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from innerjoin where groupby orderby select - 2"() {
+        assertScript '''
+            assert [[1, 4], [6, 9]] == GINQ {
+                from n in [1, 1, 3, 3, 6, 6, 6]
+                innerjoin m in [1, 1, 3, 3, 6, 6, 6] on n == m
+                where n != 3
+                groupby n
+                orderby count() in asc
+                select n, count()
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from innerjoin where groupby orderby select - 3"() {
+        assertScript '''
+            assert [[2, 3, 1], [1, 2, 1]] == GINQ {
+                from n in [1, 2, 3]
+                innerjoin m in [2, 3, 4] on n + 1 == m
+                where n != 3
+                groupby n, m
+                orderby n in desc
+                select n, m, count()
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from innerjoin where groupby orderby select - 4"() {
+        assertScript '''
+            assert [[1, 2, 1], [2, 3, 1]] == GINQ {
+                from n in [1, 2, 3]
+                innerjoin m in [2, 3, 4] on n + 1 == m
+                where n != 3
+                groupby n, m
+                orderby m in asc
+                select n, m, count()
+            }.toList()
+        '''
+    }
+
+    @CompileDynamic
+    @Test
+    void "testGinq - query json - 1"() {
+        def parser = new JsonSlurper()
+        def json = parser.parseText('''
+            {
+                "persons": [
+                    {"id": 1, "name": "Daniel"},
+                    {"id": 2, "name": "Paul"},
+                    {"id": 3, "name": "Eric"}
+                ],
+                "tasks": [
+                    {"id": 1, "assignee": 1, "content": "task1", "manDay": 6},
+                    {"id": 2, "assignee": 1, "content": "task2", "manDay": 1},
+                    {"id": 3, "assignee": 2, "content": "task3", "manDay": 3},
+                    {"id": 4, "assignee": 3, "content": "task4", "manDay": 5}
+                ]
+            }
+        ''')
+
+        def expected = [
+                [taskId: 1, taskContent: 'task1', assignee: 'Daniel', manDay: 6],
+                [taskId: 4, taskContent: 'task4', assignee: 'Eric', manDay: 5],
+                [taskId: 3, taskContent: 'task3', assignee: 'Paul', manDay: 3]
+        ]
+
+        assert expected == GINQ {
+            from p in json.persons
+            innerjoin t in json.tasks on t.assignee == p.id
+            where t.id in [1, 3, 4]
+            orderby t.manDay in desc
+            select (taskId: t.id, taskContent: t.content, assignee: p.name, manDay: t.manDay)
+        }.toList()
+    }
+}
diff --git a/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/provider/collection/NamedTupleTest.groovy b/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/provider/collection/NamedTupleTest.groovy
new file mode 100644
index 0000000..e3f9147
--- /dev/null
+++ b/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/provider/collection/NamedTupleTest.groovy
@@ -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.
+ */
+package org.apache.groovy.linq.provider.collection
+
+
+import groovy.transform.CompileStatic
+import org.junit.Test
+
+@CompileStatic
+class NamedTupleTest {
+    @Test
+    void testToString() {
+        assert '(a:1, b:2, c:3)' == new NamedTuple([1, 2, 3], ['a', 'b', 'c']).toString()
+    }
+}
diff --git a/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/provider/collection/QueryableCollectionTest.groovy b/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/provider/collection/QueryableCollectionTest.groovy
new file mode 100644
index 0000000..0c31357
--- /dev/null
+++ b/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/provider/collection/QueryableCollectionTest.groovy
@@ -0,0 +1,406 @@
+/*
+ *  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.collection
+
+
+import groovy.transform.CompileDynamic
+import groovy.transform.CompileStatic
+import groovy.transform.EqualsAndHashCode
+import groovy.transform.ToString
+import org.junit.Test
+
+import java.util.stream.Collectors
+import java.util.stream.Stream
+
+import static org.apache.groovy.linq.provider.collection.Queryable.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
+        }
+    }
+}