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/25 21:50:58 UTC

[groovy] branch GROOVY-8258 updated (1d5c893 -> b3c6367)

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

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


 discard 1d5c893  GROOVY-8258: try `@POJO`
 discard 8b77e60  GROOVY-8258: [GEP] Create a LINQ-like DSL
     new b3c6367  GROOVY-8258: [GEP] Create a LINQ-like DSL

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

 * -- * -- B -- O -- O -- O   (1d5c893)
            \
             N -- N -- N   refs/heads/GROOVY-8258 (b3c6367)

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

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

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


Summary of changes:
 .../src/main/groovy/org/apache/groovy/ginq/GinqGroovyMethods.groovy | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)


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

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

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

commit b3c6367df03a4156b05ed650596168551f930059
Author: Daniel Sun <su...@apache.org>
AuthorDate: Mon Oct 26 05:49:58 2020 +0800

    GROOVY-8258: [GEP] Create a LINQ-like DSL
---
 settings.gradle                                    |    1 +
 .../codehaus/groovy/ast/tools/GeneralUtils.java    |    9 +
 subprojects/groovy-ginq/build.gradle               |   36 +
 .../apache/groovy/ginq/GinqGroovyMethods.groovy    |   54 +
 .../org/apache/groovy/ginq/dsl/GinqAstBuilder.java |  240 +++
 .../apache/groovy/ginq/dsl/GinqSyntaxError.java    |   50 +
 .../org/apache/groovy/ginq/dsl/GinqVisitor.java    |   51 +
 .../groovy/ginq/dsl/SyntaxErrorReportable.java     |   43 +
 .../dsl/expression/AbstractGinqExpression.java     |   38 +
 .../ginq/dsl/expression/DataSourceExpression.java  |   79 +
 .../ginq/dsl/expression/DataSourceHolder.java      |   29 +
 .../ginq/dsl/expression/FilterExpression.java      |   42 +
 .../groovy/ginq/dsl/expression/FromExpression.java |   57 +
 .../groovy/ginq/dsl/expression/GinqExpression.java |   72 +
 .../ginq/dsl/expression/GroupExpression.java       |   53 +
 .../ginq/dsl/expression/HavingExpression.java      |   38 +
 .../groovy/ginq/dsl/expression/JoinExpression.java |   78 +
 .../ginq/dsl/expression/LimitExpression.java       |   44 +
 .../groovy/ginq/dsl/expression/OnExpression.java   |   38 +
 .../ginq/dsl/expression/OrderExpression.java       |   44 +
 .../ginq/dsl/expression/ProcessExpression.java     |   38 +
 .../ginq/dsl/expression/SelectExpression.java      |   51 +
 .../ginq/dsl/expression/WhereExpression.java       |   38 +
 .../provider/collection/AsciiTableMaker.groovy     |  153 ++
 .../ginq/provider/collection/GinqAstWalker.groovy  |  616 +++++++
 .../ginq/provider/collection/NamedRecord.groovy    |   62 +
 .../ginq/provider/collection/NamedTuple.groovy     |   64 +
 .../groovy/ginq/provider/collection/Queryable.java |  376 ++++
 .../provider/collection/QueryableCollection.java   |  287 +++
 .../org/apache/groovy/ginq/GinqErrorTest.groovy    |  163 ++
 .../groovy/org/apache/groovy/ginq/GinqTest.groovy  | 1879 ++++++++++++++++++++
 .../ginq/provider/collection/NamedTupleTest.groovy |   31 +
 .../collection/QueryableCollectionTest.groovy      |  447 +++++
 .../apache/groovy/ginq/tools/GinqTestRig.groovy    |   40 +
 34 files changed, 5341 insertions(+)

diff --git a/settings.gradle b/settings.gradle
index d4a4425..36fcf16 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -49,6 +49,7 @@ def subprojects = [
         'groovy-jmx',
         'groovy-json',
         'groovy-jsr223',
+        'groovy-ginq',
         '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-ginq/build.gradle b/subprojects/groovy-ginq/build.gradle
new file mode 100644
index 0000000..aeaec8d
--- /dev/null
+++ b/subprojects/groovy-ginq/build.gradle
@@ -0,0 +1,36 @@
+/*
+ *  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')
+    testImplementation project(':groovy-console')
+}
+
+groovyLibrary {
+    withoutBinaryCompatibilityChecks()
+    moduleDescriptor {
+        extensionClasses = 'org.apache.groovy.ginq.GinqGroovyMethods'
+    }
+}
diff --git a/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/GinqGroovyMethods.groovy b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/GinqGroovyMethods.groovy
new file mode 100644
index 0000000..daf1cf1
--- /dev/null
+++ b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/GinqGroovyMethods.groovy
@@ -0,0 +1,54 @@
+/*
+ *  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.ginq
+
+import groovy.transform.CompileStatic
+import org.apache.groovy.ginq.dsl.GinqAstBuilder
+import org.apache.groovy.ginq.dsl.expression.GinqExpression
+import org.apache.groovy.ginq.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
+
+/**
+ * Declare GINQ macro methods
+ *
+ * @since 4.0.0
+ */
+@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 ginqAstWalker = new GinqAstWalker(ctx.getSourceUnit())
+        MethodCallExpression selectMethodCallExpression = ginqAstWalker.visitGinqExpression(ginqExpression)
+
+        return selectMethodCallExpression
+    }
+
+    private GinqGroovyMethods() {}
+}
diff --git a/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/GinqAstBuilder.java b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/GinqAstBuilder.java
new file mode 100644
index 0000000..cabbfa9
--- /dev/null
+++ b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/GinqAstBuilder.java
@@ -0,0 +1,240 @@
+/*
+ *  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.ginq.dsl;
+
+import org.apache.groovy.ginq.dsl.expression.AbstractGinqExpression;
+import org.apache.groovy.ginq.dsl.expression.DataSourceExpression;
+import org.apache.groovy.ginq.dsl.expression.FilterExpression;
+import org.apache.groovy.ginq.dsl.expression.FromExpression;
+import org.apache.groovy.ginq.dsl.expression.GinqExpression;
+import org.apache.groovy.ginq.dsl.expression.GroupExpression;
+import org.apache.groovy.ginq.dsl.expression.HavingExpression;
+import org.apache.groovy.ginq.dsl.expression.JoinExpression;
+import org.apache.groovy.ginq.dsl.expression.LimitExpression;
+import org.apache.groovy.ginq.dsl.expression.OnExpression;
+import org.apache.groovy.ginq.dsl.expression.OrderExpression;
+import org.apache.groovy.ginq.dsl.expression.SelectExpression;
+import org.apache.groovy.ginq.dsl.expression.WhereExpression;
+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 final 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 ginq 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) || "having".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;
+            if ("where".equals(methodName)) {
+                filterExpression = new WhereExpression(filterExpr);
+            } else if ("on".equals(methodName)) {
+                filterExpression = new OnExpression(filterExpr);
+            } else {
+                filterExpression = new HavingExpression(filterExpr);
+            }
+
+            filterExpression.setSourcePosition(call);
+
+            if (latestGinqExpressionClause instanceof JoinExpression && filterExpression instanceof OnExpression) {
+                ((JoinExpression) latestGinqExpressionClause).setOnExpression((OnExpression) filterExpression);
+            } else if (latestGinqExpressionClause instanceof DataSourceExpression && filterExpression instanceof WhereExpression) {
+                final DataSourceExpression dataSourceExpression = (DataSourceExpression) latestGinqExpressionClause;
+
+                if (null != dataSourceExpression.getGroupExpression() || null != dataSourceExpression.getOrderExpression() || null != dataSourceExpression.getLimitExpression()) {
+                    this.collectSyntaxError(new GinqSyntaxError(
+                            "The preceding clause of `" + methodName + "` should be `from`/" + "join clause",
+                            call.getLineNumber(), call.getColumnNumber()
+                    ));
+                }
+                dataSourceExpression.setWhereExpression((WhereExpression) filterExpression);
+            } else if (latestGinqExpressionClause instanceof GroupExpression && filterExpression instanceof HavingExpression) {
+                ((GroupExpression) latestGinqExpressionClause).setHavingExpression((HavingExpression) filterExpression);
+            } else {
+                this.collectSyntaxError(new GinqSyntaxError(
+                        "The preceding clause of `" + methodName + "` should be " + ("on".equals(methodName) ? "" : "`from`/") + "join clause",
+                        call.getLineNumber(), call.getColumnNumber()
+                ));
+            }
+
+            return;
+        }
+
+        if ("groupby".equals(methodName)) {
+            GroupExpression groupExpression = new GroupExpression(call.getArguments());
+            groupExpression.setSourcePosition(call);
+
+            if (latestGinqExpressionClause instanceof DataSourceExpression) {
+                final DataSourceExpression latestDataSourceExpression = (DataSourceExpression) latestGinqExpressionClause;
+                (latestDataSourceExpression).setGroupExpression(groupExpression);
+                groupExpression.setDataSourceExpression(latestDataSourceExpression);
+            } else {
+                throw new GinqSyntaxError("The preceding expression is not a DataSourceExpression: " + latestGinqExpressionClause, call);
+            }
+
+            setLatestGinqExpressionClause(groupExpression);
+
+            return;
+        }
+
+        if ("orderby".equals(methodName)) {
+            OrderExpression orderExpression = new OrderExpression(call.getArguments());
+            orderExpression.setSourcePosition(call);
+
+            if (latestGinqExpressionClause instanceof DataSourceExpression) {
+                ((DataSourceExpression) latestGinqExpressionClause).setOrderExpression(orderExpression);
+            } else if (latestGinqExpressionClause instanceof GroupExpression) {
+                ((GroupExpression) latestGinqExpressionClause).getDataSourceExpression().setOrderExpression(orderExpression);
+            } else {
+                throw new GinqSyntaxError("The preceding expression is not a DataSourceExpression: " + latestGinqExpressionClause, call);
+            }
+
+            return;
+        }
+
+        if ("limit".equals(methodName)) {
+            LimitExpression limitExpression = new LimitExpression(call.getArguments());
+            limitExpression.setSourcePosition(call);
+
+            if (latestGinqExpressionClause instanceof DataSourceExpression) {
+                ((DataSourceExpression) latestGinqExpressionClause).setLimitExpression(limitExpression);
+            } else {
+                throw new GinqSyntaxError("The preceding expression is not a DataSourceExpression: " + latestGinqExpressionClause, call);
+            }
+
+            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-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/GinqSyntaxError.java b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/GinqSyntaxError.java
new file mode 100644
index 0000000..335a00f
--- /dev/null
+++ b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/GinqSyntaxError.java
@@ -0,0 +1,50 @@
+/*
+ *  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.ginq.dsl;
+
+import org.codehaus.groovy.ast.ASTNode;
+import org.codehaus.groovy.ast.expr.Expression;
+
+/**
+ * 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, Expression expression) {
+        this(message, expression.getLineNumber(), expression.getColumnNumber());
+    }
+
+    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-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/GinqVisitor.java b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/GinqVisitor.java
new file mode 100644
index 0000000..628d4e6
--- /dev/null
+++ b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/GinqVisitor.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.ginq.dsl;
+
+import org.apache.groovy.ginq.dsl.expression.AbstractGinqExpression;
+import org.apache.groovy.ginq.dsl.expression.FromExpression;
+import org.apache.groovy.ginq.dsl.expression.GinqExpression;
+import org.apache.groovy.ginq.dsl.expression.GroupExpression;
+import org.apache.groovy.ginq.dsl.expression.HavingExpression;
+import org.apache.groovy.ginq.dsl.expression.JoinExpression;
+import org.apache.groovy.ginq.dsl.expression.LimitExpression;
+import org.apache.groovy.ginq.dsl.expression.OnExpression;
+import org.apache.groovy.ginq.dsl.expression.OrderExpression;
+import org.apache.groovy.ginq.dsl.expression.SelectExpression;
+import org.apache.groovy.ginq.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 visitHavingExpression(HavingExpression havingExpression);
+    R visitOrderExpression(OrderExpression orderExpression);
+    R visitLimitExpression(LimitExpression limitExpression);
+    R visitSelectExpression(SelectExpression selectExpression);
+    R visit(AbstractGinqExpression expression);
+}
diff --git a/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/SyntaxErrorReportable.java b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/SyntaxErrorReportable.java
new file mode 100644
index 0000000..e160107
--- /dev/null
+++ b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/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.ginq.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-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/AbstractGinqExpression.java b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/AbstractGinqExpression.java
new file mode 100644
index 0000000..7788306
--- /dev/null
+++ b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/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.ginq.dsl.expression;
+
+import org.apache.groovy.ginq.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-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/DataSourceExpression.java b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/DataSourceExpression.java
new file mode 100644
index 0000000..054bcb8
--- /dev/null
+++ b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/DataSourceExpression.java
@@ -0,0 +1,79 @@
+/*
+ *  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.ginq.dsl.expression;
+
+import org.codehaus.groovy.ast.expr.Expression;
+
+/**
+ * Represents data source expression
+ *
+ * @since 4.0.0
+ */
+public abstract class DataSourceExpression extends AbstractGinqExpression implements DataSourceHolder {
+    protected final Expression aliasExpr;
+    protected final Expression dataSourceExpr;
+    protected WhereExpression whereExpression;
+    protected GroupExpression groupExpression;
+    protected OrderExpression orderExpression;
+    protected LimitExpression limitExpression;
+
+    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;
+    }
+
+    public LimitExpression getLimitExpression() {
+        return limitExpression;
+    }
+
+    public void setLimitExpression(LimitExpression limitExpression) {
+        this.limitExpression = limitExpression;
+    }
+}
diff --git a/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/DataSourceHolder.java b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/DataSourceHolder.java
new file mode 100644
index 0000000..9eb30d6
--- /dev/null
+++ b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/DataSourceHolder.java
@@ -0,0 +1,29 @@
+/*
+ *  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.ginq.dsl.expression;
+
+/**
+ * Represents data source holder, e.g. from, joins, where, groupby, etc.
+ *
+ * @since 4.0.0
+ */
+public interface DataSourceHolder {
+    DataSourceExpression getDataSourceExpression();
+    void setDataSourceExpression(DataSourceExpression dataSourceExpression);
+}
diff --git a/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/FilterExpression.java b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/FilterExpression.java
new file mode 100644
index 0000000..2fea982
--- /dev/null
+++ b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/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.ginq.dsl.expression;
+
+import org.codehaus.groovy.ast.expr.Expression;
+
+/**
+ * Represents filter expression
+ *
+ * @since 4.0.0
+ */
+public abstract class FilterExpression extends ProcessExpression {
+    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-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/FromExpression.java b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/FromExpression.java
new file mode 100644
index 0000000..0afd724
--- /dev/null
+++ b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/FromExpression.java
@@ -0,0 +1,57 @@
+/*
+ *  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.ginq.dsl.expression;
+
+import org.apache.groovy.ginq.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 +
+                '}';
+    }
+
+    @Override
+    public DataSourceExpression getDataSourceExpression() {
+        return this;
+    }
+
+    @Override
+    public void setDataSourceExpression(DataSourceExpression dataSourceExpression) {
+        throw new UnsupportedOperationException(FromExpression.class.getName() + " can not be set to another DataSourceExpression instance");
+    }
+}
diff --git a/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/GinqExpression.java b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/GinqExpression.java
new file mode 100644
index 0000000..711b677
--- /dev/null
+++ b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/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.ginq.dsl.expression;
+
+import org.apache.groovy.ginq.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-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/GroupExpression.java b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/GroupExpression.java
new file mode 100644
index 0000000..d041949
--- /dev/null
+++ b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/GroupExpression.java
@@ -0,0 +1,53 @@
+/*
+ *  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.ginq.dsl.expression;
+
+import org.apache.groovy.ginq.dsl.GinqVisitor;
+import org.codehaus.groovy.ast.expr.Expression;
+
+/**
+ * Represents group by expression
+ *
+ * @since 4.0.0
+ */
+public class GroupExpression extends ProcessExpression {
+    private final Expression classifierExpr;
+    private HavingExpression havingExpression;
+
+    public GroupExpression(Expression classifierExpr) {
+        this.classifierExpr = classifierExpr;
+    }
+
+    @Override
+    public <R> R accept(GinqVisitor<R> visitor) {
+        return visitor.visitGroupExpression(this);
+    }
+
+    public Expression getClassifierExpr() {
+        return classifierExpr;
+    }
+
+    public HavingExpression getHavingExpression() {
+        return havingExpression;
+    }
+
+    public void setHavingExpression(HavingExpression havingExpression) {
+        this.havingExpression = havingExpression;
+    }
+}
diff --git a/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/HavingExpression.java b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/HavingExpression.java
new file mode 100644
index 0000000..39a6081
--- /dev/null
+++ b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/HavingExpression.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.ginq.dsl.expression;
+
+import org.apache.groovy.ginq.dsl.GinqVisitor;
+import org.codehaus.groovy.ast.expr.Expression;
+
+/**
+ * Represents having expression
+ *
+ * @since 4.0.0
+ */
+public class HavingExpression extends FilterExpression {
+    public HavingExpression(Expression filterExpr) {
+        super(filterExpr);
+    }
+
+    @Override
+    public <R> R accept(GinqVisitor<R> visitor) {
+        return visitor.visitHavingExpression(this);
+    }
+}
diff --git a/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/JoinExpression.java b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/JoinExpression.java
new file mode 100644
index 0000000..eeed7d1
--- /dev/null
+++ b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/JoinExpression.java
@@ -0,0 +1,78 @@
+/*
+ *  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.ginq.dsl.expression;
+
+import org.apache.groovy.ginq.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 implements DataSourceHolder {
+    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;
+    private DataSourceExpression dataSourceExpression;
+
+    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;
+    }
+
+    @Override
+    public DataSourceExpression getDataSourceExpression() {
+        return dataSourceExpression;
+    }
+
+    @Override
+    public void setDataSourceExpression(DataSourceExpression dataSourceExpression) {
+        this.dataSourceExpression = dataSourceExpression;
+    }
+}
diff --git a/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/LimitExpression.java b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/LimitExpression.java
new file mode 100644
index 0000000..dd86b28
--- /dev/null
+++ b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/LimitExpression.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.ginq.dsl.expression;
+
+import org.apache.groovy.ginq.dsl.GinqVisitor;
+import org.codehaus.groovy.ast.expr.Expression;
+
+/**
+ * Represents limit expression
+ *
+ * @since 4.0.0
+ */
+public class LimitExpression extends ProcessExpression {
+    private final Expression offsetAndSizeExpr;
+
+    public LimitExpression(Expression offsetAndSizeExpr) {
+        this.offsetAndSizeExpr = offsetAndSizeExpr;
+    }
+
+    @Override
+    public <R> R accept(GinqVisitor<R> visitor) {
+        return visitor.visitLimitExpression(this);
+    }
+
+    public Expression getOffsetAndSizeExpr() {
+        return offsetAndSizeExpr;
+    }
+}
diff --git a/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/OnExpression.java b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/OnExpression.java
new file mode 100644
index 0000000..21ab0a7
--- /dev/null
+++ b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/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.ginq.dsl.expression;
+
+import org.apache.groovy.ginq.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-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/OrderExpression.java b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/OrderExpression.java
new file mode 100644
index 0000000..b379874
--- /dev/null
+++ b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/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.ginq.dsl.expression;
+
+import org.apache.groovy.ginq.dsl.GinqVisitor;
+import org.codehaus.groovy.ast.expr.Expression;
+
+/**
+ * Represents order by expression
+ *
+ * @since 4.0.0
+ */
+public class OrderExpression extends ProcessExpression {
+    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-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/ProcessExpression.java b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/ProcessExpression.java
new file mode 100644
index 0000000..bc4cb5c
--- /dev/null
+++ b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/ProcessExpression.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.ginq.dsl.expression;
+
+/**
+ * Represents process expression, e.g. where, groupby, orderby, limit, select
+ *
+ * @since 4.0.0
+ */
+public abstract class ProcessExpression extends AbstractGinqExpression implements DataSourceHolder {
+    private DataSourceExpression dataSourceExpression;
+
+    @Override
+    public DataSourceExpression getDataSourceExpression() {
+        return dataSourceExpression;
+    }
+
+    @Override
+    public void setDataSourceExpression(DataSourceExpression dataSourceExpression) {
+        this.dataSourceExpression = dataSourceExpression;
+    }
+}
diff --git a/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/SelectExpression.java b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/SelectExpression.java
new file mode 100644
index 0000000..4ac27b2
--- /dev/null
+++ b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/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.ginq.dsl.expression;
+
+import org.apache.groovy.ginq.dsl.GinqVisitor;
+import org.codehaus.groovy.ast.expr.Expression;
+
+/**
+ * Represents the select expression
+ *
+ * @since 4.0.0
+ */
+public class SelectExpression extends ProcessExpression {
+    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-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/WhereExpression.java b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/WhereExpression.java
new file mode 100644
index 0000000..bf4f2c5
--- /dev/null
+++ b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/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.ginq.dsl.expression;
+
+import org.apache.groovy.ginq.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-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/AsciiTableMaker.groovy b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/AsciiTableMaker.groovy
new file mode 100644
index 0000000..dddf0a6
--- /dev/null
+++ b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/AsciiTableMaker.groovy
@@ -0,0 +1,153 @@
+/*
+ *  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.ginq.provider.collection
+
+import groovy.transform.CompileStatic
+import groovy.transform.stc.POJO
+
+import java.util.stream.IntStream
+
+/**
+ * Make ascii table
+ *
+ * @since 4.0.0
+ */
+@POJO
+@CompileStatic
+class AsciiTableMaker {
+    private static final int DEFAULT_MAX_WIDTH = 64
+
+    /**
+     * make ascii table for list whose elements are of type {@link NamedRecord}
+     *
+     * @param tableData
+     * @return ascii table
+     */
+    static <T> String makeAsciiTable(List<T> tableData) {
+        if (!tableData.isEmpty()) {
+            List<String[]> list = new ArrayList<>(tableData.size() + 1)
+            def firstRecord = tableData.get(0)
+            if (firstRecord instanceof NamedRecord) {
+                list.add(((NamedRecord) firstRecord).nameList as String[])
+                tableData.stream().forEach(e -> {
+                    if (e instanceof NamedRecord) {
+                        String[] record = ((List) e).stream().map(c -> c?.toString()).toArray(String[]::new)
+                        list.add(record)
+                    }
+                })
+
+                return makeAsciiTable(list, DEFAULT_MAX_WIDTH, true)
+            }
+        }
+
+        return tableData.toString()
+    }
+
+    /**
+     * Create a ascii table
+     *
+     * @param table table data
+     * @param maxWidth Maximum allowed width. Line will be wrapped beyond this width.
+     * @param leftJustifiedRows If true, it will add "-" as a flag to format string to make it left justified. Otherwise right justified.
+     */
+    static String makeAsciiTable(List<String[]> table, int maxWidth, boolean leftJustifiedRows) {
+        StringBuilder result = new StringBuilder()
+
+        if (0 == maxWidth) maxWidth = DEFAULT_MAX_WIDTH
+
+        // Create new table array with wrapped rows
+        List<String[]> tableList = new ArrayList<>(table)
+        List<String[]> finalTableList = new ArrayList<>(tableList.size() + 1)
+        for (String[] row : tableList) {
+            // If any cell data is more than max width, then it will need extra row.
+            boolean needExtraRow = false
+            // Count of extra split row.
+            int splitRow = 0
+            do {
+                needExtraRow = false
+                String[] newRow = new String[row.length]
+                for (int i = 0; i < row.length; i++) {
+                    // If data is less than max width, use that as it is.
+                    def col = row[i] ?: ''
+                    if (col.length() < maxWidth) {
+                        newRow[i] = splitRow == 0 ? col : ""
+                    } else if ((col.length() > (splitRow * maxWidth))) {
+                        // If data is more than max width, then crop data at maxwidth.
+                        // Remaining cropped data will be part of next row.
+                        int end = Math.min(col.length(), ((splitRow * maxWidth) + maxWidth))
+                        newRow[i] = col.substring((splitRow * maxWidth), end)
+                        needExtraRow = true
+                    } else {
+                        newRow[i] = ""
+                    }
+                }
+                finalTableList.add(newRow)
+                if (needExtraRow) {
+                    splitRow++
+                }
+            } while (needExtraRow)
+        }
+        String[] firstElem = finalTableList.get(0)
+        String[][] finalTable = new String[finalTableList.size()][firstElem.length]
+        for (int i = 0; i < finalTable.length; i++) {
+            finalTable[i] = finalTableList.get(i)
+        }
+
+        // Calculate appropriate Length of each column by looking at width of data in each column.
+        // Map columnLengths is <column_number, column_length>
+        Map<Integer, Integer> columnLengths = new HashMap<>()
+        Arrays.stream(finalTable).forEach((String[] a) -> IntStream.range(0, a.length).forEach((int i) -> {
+            columnLengths.putIfAbsent(i, 0)
+            if (columnLengths.get(i) < a[i].length()) {
+                columnLengths.put(i, a[i].length())
+            }
+        }))
+
+        // Prepare format String
+        final StringBuilder formatString = new StringBuilder(256)
+        String flag = leftJustifiedRows ? "-" : ""
+        columnLengths.entrySet().stream().forEach(e -> formatString.append("| %" + flag + e.getValue() + "s "))
+        formatString.append("|\n")
+
+        // Prepare line for top, bottom & below header row.
+        String line = columnLengths.entrySet().stream().reduce("", (ln, b) -> {
+            String templn = "+-"
+            templn = templn + IntStream.range(0, b.getValue()).boxed().reduce("", (ln1, b1) -> ln1 + "-",
+                    (a1, b1) -> a1 + b1)
+            templn = templn + "-"
+            return ln + templn
+        }, (a, b) -> a + b)
+        line = line + "+\n"
+
+        // Print table
+        result.append(line)
+        Arrays.stream(finalTable)
+                .limit(1)
+                .forEach((String[] a) -> result.append(String.format(formatString.toString(), (Object[]) a)))
+        result.append(line)
+
+        IntStream.range(1, finalTable.length)
+                .forEach((int a) -> result.append(String.format(formatString.toString(), (Object[]) finalTable[a])))
+        result.append(line)
+
+        return result.toString()
+    }
+
+    private AsciiTableMaker() {}
+}
diff --git a/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/GinqAstWalker.groovy b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/GinqAstWalker.groovy
new file mode 100644
index 0000000..5e31768
--- /dev/null
+++ b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/GinqAstWalker.groovy
@@ -0,0 +1,616 @@
+/*
+ *  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.ginq.provider.collection
+
+import groovy.transform.CompileDynamic
+import groovy.transform.CompileStatic
+import org.apache.groovy.ginq.dsl.GinqSyntaxError
+import org.apache.groovy.ginq.dsl.GinqVisitor
+import org.apache.groovy.ginq.dsl.SyntaxErrorReportable
+import org.apache.groovy.ginq.dsl.expression.AbstractGinqExpression
+import org.apache.groovy.ginq.dsl.expression.DataSourceExpression
+import org.apache.groovy.ginq.dsl.expression.FromExpression
+import org.apache.groovy.ginq.dsl.expression.GinqExpression
+import org.apache.groovy.ginq.dsl.expression.GroupExpression
+import org.apache.groovy.ginq.dsl.expression.HavingExpression
+import org.apache.groovy.ginq.dsl.expression.JoinExpression
+import org.apache.groovy.ginq.dsl.expression.LimitExpression
+import org.apache.groovy.ginq.dsl.expression.OnExpression
+import org.apache.groovy.ginq.dsl.expression.OrderExpression
+import org.apache.groovy.ginq.dsl.expression.SelectExpression
+import org.apache.groovy.ginq.dsl.expression.WhereExpression
+import org.codehaus.groovy.GroovyBugError
+import org.codehaus.groovy.ast.ClassHelper
+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.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.dataSourceExpression = lastJoinExpression ?: fromExpression
+
+            lastJoinExpression = joinExpression
+            lastJoinMethodCallExpression = this.visitJoinExpression(lastJoinExpression)
+        }
+
+        if (lastJoinMethodCallExpression) {
+            selectMethodReceiver = lastJoinMethodCallExpression
+        }
+
+        SelectExpression selectExpression = ginqExpression.getSelectExpression()
+        selectExpression.putNodeMetaData(__METHOD_CALL_RECEIVER, selectMethodReceiver)
+        selectExpression.dataSourceExpression = 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.dataSourceExpression = dataSourceExpression
+            whereExpression.putNodeMetaData(__METHOD_CALL_RECEIVER, dataSourceMethodCallExpression)
+
+            MethodCallExpression whereMethodCallExpression = visitWhereExpression(whereExpression)
+            dataSourceMethodCallExpression = whereMethodCallExpression
+        }
+
+        GroupExpression groupExpression = dataSourceExpression.groupExpression
+        if (groupExpression) {
+            groupExpression.dataSourceExpression = dataSourceExpression
+            groupExpression.putNodeMetaData(__METHOD_CALL_RECEIVER, dataSourceMethodCallExpression)
+
+            MethodCallExpression groupMethodCallExpression = visitGroupExpression(groupExpression)
+            dataSourceMethodCallExpression = groupMethodCallExpression
+        }
+
+        OrderExpression orderExpression = dataSourceExpression.orderExpression
+        if (orderExpression) {
+            orderExpression.dataSourceExpression = dataSourceExpression
+            orderExpression.putNodeMetaData(__METHOD_CALL_RECEIVER, dataSourceMethodCallExpression)
+
+            MethodCallExpression orderMethodCallExpression = visitOrderExpression(orderExpression)
+            dataSourceMethodCallExpression = orderMethodCallExpression
+        }
+
+        LimitExpression limitExpression = dataSourceExpression.limitExpression
+        if (limitExpression) {
+            limitExpression.dataSourceExpression = dataSourceExpression
+            limitExpression.putNodeMetaData(__METHOD_CALL_RECEIVER, dataSourceMethodCallExpression)
+
+            MethodCallExpression limitMethodCallExpression = visitLimitExpression(limitExpression)
+            dataSourceMethodCallExpression = limitMethodCallExpression
+        }
+
+        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.dataSourceExpression
+        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.dataSourceExpression
+        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.dataSourceExpression
+        Expression groupMethodCallReceiver = groupExpression.getNodeMetaData(__METHOD_CALL_RECEIVER)
+        Expression classifierExpr = groupExpression.classifierExpr
+
+        List<Expression> argumentExpressionList = ((ArgumentListExpression) classifierExpr).getExpressions()
+        ConstructorCallExpression namedListCtorCallExpression = constructNamedRecordCtorCallExpression(argumentExpressionList)
+
+        LambdaExpression classifierLambdaExpression = constructLambdaExpression(dataSourceExpression, namedListCtorCallExpression)
+
+        List<Expression> argList = new ArrayList<>()
+        argList << classifierLambdaExpression
+
+        this.currentGinqExpression.putNodeMetaData(__GROUPBY_VISITED, true)
+
+        HavingExpression havingExpression = groupExpression.havingExpression
+        if (havingExpression) {
+            Expression filterExpr = havingExpression.filterExpr
+            LambdaExpression havingLambdaExpression = constructLambdaExpression(dataSourceExpression, filterExpr)
+            argList << havingLambdaExpression
+        }
+
+        MethodCallExpression groupMethodCallExpression = callX(groupMethodCallReceiver, "groupBy", args(argList))
+
+        return groupMethodCallExpression
+    }
+
+    @Override
+    Object visitHavingExpression(HavingExpression havingExpression) {
+        return null // do nothing
+    }
+
+    @Override
+    MethodCallExpression visitOrderExpression(OrderExpression orderExpression) {
+        DataSourceExpression dataSourceExpression = orderExpression.dataSourceExpression
+        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 visitLimitExpression(LimitExpression limitExpression) {
+        Expression limitMethodCallReceiver = limitExpression.getNodeMetaData(__METHOD_CALL_RECEIVER)
+        Expression offsetAndSizeExpr = limitExpression.offsetAndSizeExpr
+
+        return callX(limitMethodCallReceiver, "limit", offsetAndSizeExpr)
+    }
+
+    @Override
+    MethodCallExpression visitSelectExpression(SelectExpression selectExpression) {
+        Expression selectMethodReceiver = selectExpression.getNodeMetaData(__METHOD_CALL_RECEIVER)
+        DataSourceExpression dataSourceExpression = selectExpression.dataSourceExpression
+        Expression projectionExpr = selectExpression.getProjectionExpr()
+
+        List<Expression> expressionList = ((TupleExpression) projectionExpr).getExpressions()
+        Expression lambdaCode
+        if (expressionList.size() > 1) {
+            ConstructorCallExpression namedListCtorCallExpression = constructNamedRecordCtorCallExpression(expressionList)
+            lambdaCode = namedListCtorCallExpression
+        } else {
+            lambdaCode = expressionList.get(0)
+        }
+
+        return callXWithLambda(selectMethodReceiver, "select", dataSourceExpression, lambdaCode)
+    }
+
+    private static ConstructorCallExpression constructNamedRecordCtorCallExpression(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
+        }
+
+        List<String> sourceNameList = []
+        expressionList.each { Expression expr ->
+            expr.visit(new CodeVisitorSupport() {
+                @Override
+                void visitPropertyExpression(PropertyExpression expression) {
+                    if (expression.objectExpression instanceof VariableExpression) {
+                        sourceNameList << expression.objectExpression.text
+                        return
+                    }
+                    super.visitPropertyExpression(expression)
+                }
+            })
+        }
+
+        List<Expression> sourceNameExpressionList =
+                sourceNameList.stream().map(e -> new ConstantExpression(e)).collect(Collectors.toList())
+
+        ConstructorCallExpression namedListCtorCallExpression = ctorX(ClassHelper.make(NamedRecord.class), args(new ListExpression(elementExpressionList), new ListExpression(nameExpressionList), new ListExpression(sourceNameExpressionList)))
+        return namedListCtorCallExpression
+    }
+
+    private Expression correctVariablesOfGinqExpression(DataSourceExpression dataSourceExpression, Expression expr) {
+        String lambdaParamName = expr.getNodeMetaData(__LAMBDA_PARAM_NAME)
+        if (null == lambdaParamName) {
+            throw new GroovyBugError("lambdaParamName is null. dataSourceExpression:${dataSourceExpression}, expr:${expr}")
+        }
+
+        // (1) correct itself
+        expr = correctVars(dataSourceExpression, lambdaParamName, expr)
+
+        // (2) correct its children nodes
+        // 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(dataSourceExpression, lambdaParamName, expression)
+                if (transformedExpression !== expression) {
+                    return transformedExpression
+                }
+
+                return expression.transformExpression(this)
+            }
+        })
+
+        return expr
+    }
+
+    private Expression correctVars(DataSourceExpression dataSourceExpression, String lambdaParamName, Expression expression) {
+        boolean isGroup = isGroupByVisited()
+        boolean isJoin = dataSourceExpression instanceof JoinExpression
+
+        Expression transformedExpression = null
+        if (expression instanceof VariableExpression) {
+            if (expression.isThisExpression()) return expression
+            if (expression.text && Character.isUpperCase(expression.text.charAt(0))) return expression // type should be transformed
+
+            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 (lambdaParamName != expression.text) {
+                    if (visitingAggregateFunction) {
+                        if (_Q == expression.text) {
+                            transformedExpression = new VariableExpression(lambdaParamName)
+                        } else {
+                            transformedExpression = isJoin
+                                    ? correctVarsForJoin(dataSourceExpression, expression, new VariableExpression(lambdaParamName))
+                                    : new VariableExpression(lambdaParamName)
+                        }
+                    } else {
+                        // replace `gk` in the groupby with `__t.v1.gk`, note: __t.v1 stores the group key
+                        transformedExpression = propX(propX(new VariableExpression(lambdaParamName), 'v1'), expression.text)
+                    }
+                }
+            } else if (isJoin) {
+                transformedExpression = correctVarsForJoin(dataSourceExpression, expression, new VariableExpression(lambdaParamName))
+            }
+        } else if (expression instanceof MethodCallExpression) {
+            // #1
+            if (isGroup) { // groupby
+                if (expression.implicitThis) {
+                    String methodName = expression.methodAsString
+                    if ('count' == methodName && ((TupleExpression) expression.arguments).getExpressions().isEmpty()) { // Similar to count(*) in SQL
+                        visitingAggregateFunction = true
+                        expression.objectExpression = propX(new VariableExpression(lambdaParamName), 'v2')
+                        transformedExpression = expression
+                        visitingAggregateFunction = false
+                    } else if (methodName in ['count', 'min', 'max', 'sum', 'agg'] && 1 == ((TupleExpression) expression.arguments).getExpressions().size()) {
+                        visitingAggregateFunction = true
+                        Expression lambdaCode = ((TupleExpression) expression.arguments).getExpression(0)
+                        lambdaCode.putNodeMetaData(__LAMBDA_PARAM_NAME, findRootObjectExpression(lambdaCode).text)
+                        transformedExpression =
+                                callXWithLambda(
+                                        propX(new VariableExpression(lambdaParamName), 'v2'), methodName,
+                                        dataSourceExpression, lambdaCode)
+                        visitingAggregateFunction = false
+                    }
+                }
+            }
+        }
+
+        if (null != transformedExpression) {
+            return transformedExpression
+        }
+
+        return expression
+    }
+
+    private Expression correctVarsForJoin(DataSourceExpression dataSourceExpression, Expression expression, Expression prop) {
+        boolean isJoin = dataSourceExpression instanceof JoinExpression
+        if (!isJoin) return expression
+
+        Expression transformedExpression = null
+        /*
+                 * `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
+                 */
+        for (DataSourceExpression dse = dataSourceExpression;
+             null == transformedExpression && dse instanceof JoinExpression;
+             dse = dse.dataSourceExpression) {
+
+            DataSourceExpression otherDataSourceExpression = dse.dataSourceExpression
+            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')
+            }
+        }
+
+        return transformedExpression
+    }
+
+    private static Expression findRootObjectExpression(Expression expression) {
+        if (expression instanceof PropertyExpression) {
+            Expression expr = expression
+            for (; expr instanceof PropertyExpression; expr = ((PropertyExpression) expr).objectExpression) {}
+            return expr
+        }
+
+        return expression
+    }
+
+    private boolean visitingAggregateFunction
+
+    @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 static String generateLambdaParamName() {
+        "__t_${System.nanoTime()}_${new Random().nextInt(1000)}"
+    }
+
+    private Tuple2<String, Expression> correctVariablesOfLambdaExpression(DataSourceExpression dataSourceExpression, Expression lambdaCode) {
+        boolean isGroup = isGroupByVisited()
+
+        String lambdaParamName
+        if (dataSourceExpression instanceof JoinExpression || isGroup) {
+            lambdaParamName = lambdaCode.getNodeMetaData(__LAMBDA_PARAM_NAME)
+            if (!lambdaParamName || visitingAggregateFunction) {
+                lambdaParamName = generateLambdaParamName()
+            }
+
+            lambdaCode.putNodeMetaData(__LAMBDA_PARAM_NAME, lambdaParamName)
+            lambdaCode = correctVariablesOfGinqExpression(dataSourceExpression, lambdaCode)
+        } else {
+            lambdaParamName = dataSourceExpression.aliasExpr.text
+            lambdaCode.putNodeMetaData(__LAMBDA_PARAM_NAME, lambdaParamName)
+        }
+
+        if (lambdaCode instanceof ConstructorCallExpression) {
+            if (NamedRecord.class == lambdaCode.type.getTypeClass()) {
+                // store the source record
+                lambdaCode = callX(lambdaCode, 'sourceRecord', new VariableExpression(lambdaParamName))
+            }
+        }
+
+        return Tuple.tuple(lambdaParamName, lambdaCode)
+    }
+
+    private boolean isGroupByVisited() {
+        return currentGinqExpression.getNodeMetaData(__GROUPBY_VISITED) ?: 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 __METHOD_CALL_RECEIVER = "__methodCallReceiver"
+    private static final String __GROUPBY_VISITED = "__groupByVisited"
+    private static final String __LAMBDA_PARAM_NAME = "__LAMBDA_PARAM_NAME"
+    private static final String _Q = '_q' // the implicit variable representing grouped `Queryable` object
+}
diff --git a/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/NamedRecord.groovy b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/NamedRecord.groovy
new file mode 100644
index 0000000..bb873ab
--- /dev/null
+++ b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/NamedRecord.groovy
@@ -0,0 +1,62 @@
+/*
+ *  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.ginq.provider.collection
+
+
+import groovy.transform.CompileStatic
+import groovy.transform.stc.POJO
+
+/**
+ * Represents named record.
+ * Given {@code p.name, t.manDay}, the {@code sourceRecord} stores the source record, which is record from source, e.g. join/from/"group by key",
+ * {@code nameList} stores {@code 'name', 'manDay'}, and {@code sourceNameList} stores {@code 'p', 't'}
+ *
+ * @since 4.0.0
+ */
+@POJO
+@CompileStatic
+class NamedRecord<E, T> extends NamedTuple<E> {
+    private T sourceRecord
+    private List<String> sourceNameList
+
+    NamedRecord(List<E> elementList, List<String> nameList, List<String> sourceNameList = Collections.emptyList()) {
+        super(elementList, nameList)
+        this.sourceNameList = sourceNameList
+    }
+
+    @Override
+    def getAt(String name) {
+        if (name in nameList) return super.getAt(name)
+
+        if (name !in sourceNameList) {
+            throw new IndexOutOfBoundsException("Failed to find: $name")
+        }
+
+        return sourceRecord
+    }
+
+    T sourceRecord() {
+        return sourceRecord
+    }
+
+    NamedRecord<E, T> sourceRecord(T sourceRecord) {
+        this.sourceRecord = sourceRecord
+        return this
+    }
+}
diff --git a/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/NamedTuple.groovy b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/NamedTuple.groovy
new file mode 100644
index 0000000..b08608c
--- /dev/null
+++ b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/NamedTuple.groovy
@@ -0,0 +1,64 @@
+/*
+ *  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.ginq.provider.collection
+
+
+import groovy.transform.CompileStatic
+import groovy.transform.stc.POJO
+
+/**
+ * Immutable named list to represent list result of GINQ
+ *
+ * @since 4.0.0
+ */
+@POJO
+@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
+    }
+
+    def getAt(String name) {
+        final int index = nameList.indexOf(name)
+
+        if (-1 == index) {
+            throw new IndexOutOfBoundsException("Failed to find: $name")
+        }
+
+        return get(index)
+    }
+
+    def get(String name) {
+        return getAt(name)
+    }
+
+    List<String> getNameList() {
+        return Collections.unmodifiableList(nameList)
+    }
+
+    @Override
+    String toString() {
+        '(' + nameList.withIndex()
+                .collect((String n, int i) -> { "${n}:${this[i]}" })
+                .join(', ') + ')'
+    }
+}
diff --git a/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/Queryable.java b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/Queryable.java
new file mode 100644
index 0000000..bf1f33c
--- /dev/null
+++ b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/Queryable.java
@@ -0,0 +1,376 @@
+/*
+ *  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.ginq.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> {
+    /**
+     * Factory method to create {@link Queryable} instance
+     *
+     * @param iterable iterable object, e.g. {@link List}
+     * @param <T> the type of element
+     * @return the {@link Queryable} instance
+     * @since 4.0.0
+     */
+    static <T> Queryable<T> from(Iterable<T> iterable) {
+        return new QueryableCollection<>(iterable);
+    }
+
+    /**
+     * Returns the original {@link Queryable} instance directly
+     *
+     * @param queryable queryable object
+     * @param <T> the type of element
+     * @return the {@link Queryable} instance
+     * @since 4.0.0
+     */
+    static <T> Queryable<T> from(Queryable<T> queryable) {
+        return queryable;
+    }
+
+    /**
+     * Factory method to create {@link Queryable} instance
+     *
+     * @param sourceStream stream object
+     * @param <T> the type of element
+     * @return the {@link Queryable} instance
+     * @since 4.0.0
+     */
+    static <T> Queryable<T> from(Stream<? extends T> sourceStream) {
+        return new QueryableCollection<>(sourceStream);
+    }
+
+    /**
+     * Inner join another {@link Queryable} instance, similar to SQL's {@code inner join}
+     *
+     * @param queryable another {@link Queryable} instance
+     * @param joiner join condition
+     * @param <U> the type of element from another {@link Queryable} instance
+     * @return the join result
+     * @since 4.0.0
+     */
+    <U> Queryable<Tuple2<T, U>> innerJoin(Queryable<? extends U> queryable, BiPredicate<? super T, ? super U> joiner);
+
+    /**
+     * Left join another {@link Queryable} instance, similar to SQL's {@code left join}
+     *
+     * @param queryable another {@link Queryable} instance
+     * @param joiner join condition
+     * @param <U> the type of element from another {@link Queryable} instance
+     * @return the join result
+     * @since 4.0.0
+     */
+    <U> Queryable<Tuple2<T, U>> leftJoin(Queryable<? extends U> queryable, BiPredicate<? super T, ? super U> joiner);
+
+    /**
+     * Right join another {@link Queryable} instance, similar to SQL's {@code right join}
+     *
+     * @param queryable another {@link Queryable} instance
+     * @param joiner join condition
+     * @param <U> the type of element from another {@link Queryable} instance
+     * @return the join result
+     * @since 4.0.0
+     */
+    <U> Queryable<Tuple2<T, U>> rightJoin(Queryable<? extends U> queryable, BiPredicate<? super T, ? super U> joiner);
+
+    /**
+     * Full join another {@link Queryable} instance, similar to SQL's {@code full join}
+     *
+     * @param queryable another {@link Queryable} instance
+     * @param joiner join condition
+     * @param <U> the type of element from another {@link Queryable} instance
+     * @return the join result
+     * @since 4.0.0
+     */
+    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);
+    }
+
+    /**
+     * Cross join another {@link Queryable} instance, similar to SQL's {@code cross join}
+     *
+     * @param queryable another {@link Queryable} instance
+     * @param <U> the type of element from another {@link Queryable} instance
+     * @return the join result
+     * @since 4.0.0
+     */
+    <U> Queryable<Tuple2<T, U>> crossJoin(Queryable<? extends U> queryable);
+
+    /**
+     * Filter {@link Queryable} instance via some condition, similar to SQL's {@code where}
+     *
+     * @param filter the filter condition
+     * @return filter result
+     * @since 4.0.0
+     */
+    Queryable<T> where(Predicate<? super T> filter);
+
+    /**
+     * Group by {@link Queryable} instance, similar to SQL's {@code group by}
+     *
+     * @param classifier the classifier for group by
+     * @param having the filter condition
+     * @param <K> the type of group key
+     * @return the result of group by
+     * @since 4.0.0
+     */
+    <K> Queryable<Tuple2<K, Queryable<T>>> groupBy(Function<? super T, ? extends K> classifier, Predicate<? super Tuple2<? extends K, Queryable<? extends T>>> having);
+
+    /**
+     * Group by {@link Queryable} instance without {@code having} clause, similar to SQL's {@code group by}
+     *
+     * @param classifier the classifier for group by
+     * @param <K> the type of group key
+     * @return the result of group by
+     * @since 4.0.0
+     */
+    default <K> Queryable<Tuple2<K, Queryable<T>>> groupBy(Function<? super T, ? extends K> classifier) {
+        return groupBy(classifier, null);
+    }
+
+    /**
+     * Sort {@link Queryable} instance, similar to SQL's {@code order by}
+     *
+     * @param orders the order rules for sorting
+     * @param <U> the type of field to sort
+     * @return the result of order by
+     * @since 4.0.0
+     */
+    <U extends Comparable<? super U>> Queryable<T> orderBy(Order<? super T, ? extends U>... orders);
+
+    /**
+     * Paginate {@link Queryable} instance, similar to MySQL's {@code limit}
+     *
+     * @param offset the start position
+     * @param size the size to take
+     * @return the result of paginating
+     * @since 4.0.0
+     */
+    Queryable<T> limit(long offset, long size);
+
+    /**
+     * Paginate {@link Queryable} instance, similar to MySQL's {@code limit}
+     *
+     * @param size the size to take
+     * @return the result of paginating
+     * @since 4.0.0
+     */
+    default Queryable<T> limit(long size) {
+        return limit(0, size);
+    }
+
+    /**
+     * Project {@link Queryable} instance, similar to SQL's {@code select}
+     *
+     * @param mapper project fields
+     * @param <U> the type of project record
+     * @return the result of projecting
+     * @since 4.0.0
+     */
+    <U> Queryable<U> select(Function<? super T, ? extends U> mapper);
+
+    /**
+     * Eliminate duplicated records, similar to SQL's {@code distinct}
+     *
+     * @return the distinct result
+     * @since 4.0.0
+     */
+    Queryable<T> distinct();
+
+    /**
+     * Union another {@link Queryable} instance, similar to SQL's {@code union}
+     *
+     * @param queryable the other {@link Queryable} instance
+     * @return the union result
+     * @since 4.0.0
+     */
+    default Queryable<T> union(Queryable<? extends T> queryable) {
+        return this.unionAll(queryable).distinct();
+    }
+
+    /**
+     * Union all another {@link Queryable} instance, similar to SQL's {@code union all}
+     *
+     * @param queryable the other {@link Queryable} instance
+     * @return the union all result
+     * @since 4.0.0
+     */
+    Queryable<T> unionAll(Queryable<? extends T> queryable);
+
+    /**
+     * Intersect another {@link Queryable} instance, similar to SQL's {@code intersect}
+     *
+     * @param queryable the other {@link Queryable} instance
+     * @return the intersect result
+     * @since 4.0.0
+     */
+    Queryable<T> intersect(Queryable<? extends T> queryable);
+
+    /**
+     * Minus another {@link Queryable} instance, similar to SQL's {@code minus}
+     *
+     * @param queryable the other {@link Queryable} instance
+     * @return the minus result
+     * @since 4.0.0
+     */
+    Queryable<T> minus(Queryable<? extends T> queryable);
+
+    /**
+     * Convert the {@link Queryable} instance to {@link List<T>} instance
+     *
+     * @return the result list
+     * @since 4.0.0
+     */
+    List<T> toList();
+
+    /**
+     * Returns the count of elements of the {@link Queryable} instance
+     *
+     * @return the count of elements of the {@link Queryable} instance
+     * @since 4.0.0
+     */
+    long size();
+
+    /**
+     * Create {@link Stream<T>} object for the {@link Queryable} instance
+     *
+     * @return the result stream
+     * @since 4.0.0
+     */
+    default Stream<T> stream() {
+        return toList().stream();
+    }
+
+    //  Built-in aggregate functions {
+    /**
+     * Aggreate function {@code count}, similar to SQL's {@code count}
+     *
+     * @return count result
+     * @since 4.0.0
+     */
+    Long count();
+
+    /**
+     * Aggregate function {@code count}, similar to SQL's {@code count}
+     * Note: if the chosen field is {@code null}, the field will not be counted
+     *
+     * @param mapper choose the field to count
+     * @return count result
+     * @since 4.0.0
+     */
+    <U> Long count(Function<? super T, ? extends U> mapper);
+
+    /**
+     * Aggregate function {@code sum}, similar to SQL's {@code sum}
+     *
+     * @param mapper choose the field to sum
+     * @return sum result
+     * @since 4.0.0
+     */
+    BigDecimal sum(Function<? super T, ? extends Number> mapper);
+
+    /**
+     * Aggregate function {@code min}, similar to SQL's {@code min}
+     *
+     * @param mapper choose the field to find the minimum
+     * @param <U> the field type
+     * @return min result
+     * @since 4.0.0
+     */
+    <U extends Comparable<? super U>> U min(Function<? super T, ? extends U> mapper);
+
+    /**
+     * Aggregate function {@code max}, similar to SQL's {@code max}
+     *
+     * @param mapper choose the field to find the maximum
+     * @param <U> the field type
+     * @return min result
+     * @since 4.0.0
+     */
+    <U extends Comparable<? super U>> U max(Function<? super T, ? extends U> mapper);
+
+    /**
+     * The most powerful aggregate function in GINQ, it will receive the grouped result({@link Queryable} instance) and apply any processing
+     *
+     * @param mapper map the grouped result({@link Queryable} instance) to aggregate result
+     * @param <U> the type aggregate result
+     * @return aggregate result
+     * @since 4.0.0
+     */
+    <U> U agg(Function<? super Queryable<? extends T>, ? extends U> mapper);
+    // } Built-in aggregate functions
+
+    /**
+     * Represents an order rule
+     *
+     * @param <T> the type of element from {@link Queryable} instance
+     * @param <U> the type of field to sort
+     * @since 4.0.0
+     */
+    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-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/QueryableCollection.java b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/QueryableCollection.java
new file mode 100644
index 0000000..c8cc068
--- /dev/null
+++ b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/QueryableCollection.java
@@ -0,0 +1,287 @@
+/*
+ *  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.ginq.provider.collection;
+
+import groovy.lang.Tuple;
+import groovy.lang.Tuple2;
+import groovy.transform.Internal;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.Iterator;
+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.Collectors;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+import static org.apache.groovy.ginq.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>, Serializable {
+    private static final long serialVersionUID = -5067092453136522893L;
+    private final Iterable<T> sourceIterable;
+
+    QueryableCollection(Iterable<T> iterable) {
+        this.sourceIterable = iterable;
+    }
+
+    QueryableCollection(Queryable<T> queryable) {
+        this(queryable.toList());
+    }
+
+    @SuppressWarnings("unchecked")
+    QueryableCollection(Stream<? extends T> sourceStream) {
+        this((Iterable<T>) toIterable(sourceStream));
+    }
+
+    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);
+
+        return from(stream);
+    }
+
+    @Override
+    public <K> Queryable<Tuple2<K, Queryable<T>>> groupBy(Function<? super T, ? extends K> classifier, Predicate<? super Tuple2<? extends K, Queryable<? extends T>>> having) {
+        Stream<Tuple2<K, Queryable<T>>> stream =
+                this.stream()
+                        .collect(Collectors.groupingBy(classifier, Collectors.toList()))
+                        .entrySet().stream()
+                        .filter(m -> null == having || having.test(Tuple.tuple(m.getKey(), from(m.getValue()))))
+                        .map(m -> Tuple.tuple(m.getKey(), from(m.getValue())));
+
+        return from(stream);
+    }
+
+    @Override
+    public <U extends Comparable<? super U>> Queryable<T> orderBy(Order<? super T, ? extends U>... orders) {
+        Comparator<T> comparator = null;
+        for (int i = 0, n = orders.length; i < n; i++) {
+            Order<? super T, ? extends U> order = orders[i];
+            Comparator<U> ascOrDesc = order.isAsc() ? Comparator.naturalOrder() : Comparator.reverseOrder();
+            comparator =
+                    0 == i
+                            ? Comparator.comparing(order.getKeyExtractor(), ascOrDesc)
+                            : comparator.thenComparing(order.getKeyExtractor(), ascOrDesc);
+        }
+
+        if (null == comparator) {
+            return this;
+        }
+
+        return from(this.stream().sorted(comparator));
+    }
+
+    @Override
+    public Queryable<T> limit(long offset, long 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 long size() {
+        return stream().count();
+    }
+
+    @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 Long count() {
+        return agg(q -> q.stream().count());
+    }
+
+    @Override
+    public <U> Long count(Function<? super T, ? extends U> mapper) {
+        return agg(q -> q.stream()
+                .map(mapper)
+                .filter(Objects::nonNull)
+                .count());
+    }
+
+    @Override
+    public BigDecimal sum(Function<? super T, ? extends Number> mapper) {
+        return agg(q -> this.stream()
+                .map(e -> {
+                    Number n = mapper.apply(e);
+                    if (null == n) return BigDecimal.ZERO;
+
+                    return n instanceof BigDecimal ? (BigDecimal) n : new BigDecimal(n.toString());
+                }).reduce(BigDecimal.ZERO, BigDecimal::add));
+    }
+
+    @Override
+    public <U extends Comparable<? super U>> U min(Function<? super T, ? extends U> mapper) {
+        return agg(q -> q.stream()
+                .map(mapper)
+                .filter(Objects::nonNull)
+                .min(Comparator.comparing(Function.identity()))
+                .orElse(null));
+    }
+
+    @Override
+    public <U extends Comparable<? super U>> U max(Function<? super T, ? extends U> mapper) {
+        return agg(q -> q.stream()
+                .map(mapper)
+                .filter(Objects::nonNull)
+                .max(Comparator.comparing(Function.identity()))
+                .orElse(null));
+    }
+
+    @Override
+    public <U> U agg(Function<? super Queryable<? extends T>, ? extends U> 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 AsciiTableMaker.makeAsciiTable(toList());
+    }
+}
diff --git a/subprojects/groovy-ginq/src/test/groovy/org/apache/groovy/ginq/GinqErrorTest.groovy b/subprojects/groovy-ginq/src/test/groovy/org/apache/groovy/ginq/GinqErrorTest.groovy
new file mode 100644
index 0000000..b923680
--- /dev/null
+++ b/subprojects/groovy-ginq/src/test/groovy/org/apache/groovy/ginq/GinqErrorTest.groovy
@@ -0,0 +1,163 @@
+/*
+ *  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.ginq
+
+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.')
+    }
+
+    @Test
+    void "testGinq - from on select - 1"() {
+        def err = shouldFail '''\
+            GINQ {
+                from n in [1, 2, 3]
+                on n > 1
+                select n
+            }
+        '''
+
+        assert err.toString().contains('The preceding clause of `on` should be join clause @ line 3, column 17.')
+    }
+
+    @Test
+    void "testGinq - from groupby where - 1"() {
+        def err = shouldFail '''\
+            GINQ {
+                from n in [1, 2, 3]
+                groupby n
+                where n > 1
+                select n
+            }
+        '''
+
+        assert err.toString().contains('The preceding clause of `where` should be `from`/join clause @ line 4, column 17.')
+    }
+
+    @Test
+    void "testGinq - from orderby where - 1"() {
+        def err = shouldFail '''\
+            GINQ {
+                from n in [1, 2, 3]
+                orderby n
+                where n > 1
+                select n
+            }
+        '''
+
+        assert err.toString().contains('The preceding clause of `where` should be `from`/join clause @ line 4, column 17.')
+    }
+
+    @Test
+    void "testGinq - from limit where - 1"() {
+        def err = shouldFail '''\
+            GINQ {
+                from n in [1, 2, 3]
+                limit 1
+                where n > 1
+                select n
+            }
+        '''
+
+        assert err.toString().contains('The preceding clause of `where` should be `from`/join clause @ line 4, column 17.')
+    }
+
+    @Test
+    void "testGinq - from groupby select - 3"() {
+        def err = shouldFail '''
+            @groovy.transform.EqualsAndHashCode
+            class Person {
+                String name
+                int weight
+                String gender
+                
+                Person(String name, int weight, String gender) {
+                    this.name = name
+                    this.weight = weight
+                    this.gender = gender
+                }
+            }
+            def persons = [new Person('Linda', 100, 'Female'), new Person('Daniel', 135, 'Male'), new Person('David', 121, 'Male')]
+            assert [['Female', 1], ['Male', 2]] == GINQ {
+                from p in persons
+                groupby p.gender
+                orderby count()
+                select x.gender, count()
+            }.toList()
+        '''
+
+        assert err.toString().contains('Failed to find: x')
+    }
+}
diff --git a/subprojects/groovy-ginq/src/test/groovy/org/apache/groovy/ginq/GinqTest.groovy b/subprojects/groovy-ginq/src/test/groovy/org/apache/groovy/ginq/GinqTest.groovy
new file mode 100644
index 0000000..f1b4a38
--- /dev/null
+++ b/subprojects/groovy-ginq/src/test/groovy/org/apache/groovy/ginq/GinqTest.groovy
@@ -0,0 +1,1879 @@
+/*
+ *  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.ginq
+
+
+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 - nested from - 18"() {
+        assertScript '''
+            assert [1, 2] == GINQ {
+                from n in (
+                    from m in [0, 1, 2]
+                    select m
+                )
+                where n in (
+                    from m in [1, 2]
+                    select m
+                )
+                select n
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - nested from - 19"() {
+        assertScript '''
+            assert [2] == GINQ {
+                from n in (
+                    from m in [0, 1, 2]
+                    where m > 0
+                    select m
+                )
+                where n in (
+                    from m in [1, 2]
+                    where m > 1
+                    select m
+                )
+                select n
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - nested from - 20"() {
+        assertScript '''
+            assert [[1, 1], [2, 4], [3, 9]] == GINQ {
+                from v in (
+                    from n in [1, 2, 3]
+                    select n, Math.pow(n, 2) as powerOfN
+                )
+                select v.n, v.powerOfN
+            }.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 limit select - 1"() {
+        assertScript '''
+            assert [1, 2, 3] == GINQ {
+                from n in [1, 2, 3, 4, 5]
+                limit 3
+                select n
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from limit select - 2"() {
+        assertScript '''
+            assert [2, 3, 4] == GINQ {
+                from n in [1, 2, 3, 4, 5]
+                limit 1, 3
+                select n
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from orderby limit select - 3"() {
+        assertScript '''
+            assert [5, 4, 3] == GINQ {
+                from n in [1, 2, 3, 4, 5]
+                orderby n in desc
+                limit 3
+                select n
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from orderby limit select - 4"() {
+        assertScript '''
+            assert [4, 3, 2] == GINQ {
+                from n in [1, 2, 3, 4, 5]
+                orderby n in desc
+                limit 1, 3
+                select n
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from where orderby limit select - 1"() {
+        assertScript '''
+            assert [5, 4, 3] == GINQ {
+                from n in [1, 2, 3, 4, 5]
+                where n >= 2
+                orderby n in desc
+                limit 3
+                select n
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from where orderby limit select - 2"() {
+        assertScript '''
+            assert [4, 3, 2] == GINQ {
+                from n in [1, 2, 3, 4, 5]
+                where n >= 2
+                orderby n in desc
+                limit 1, 3
+                select n
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from where orderby limit select - 3"() {
+        assertScript '''
+            assert [5, 4] == GINQ {
+                from n in [1, 2, 3, 4, 5]
+                where n > 3
+                orderby n in desc
+                limit 3
+                select n
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from where orderby limit select - 4"() {
+        assertScript '''
+            assert [4] == GINQ {
+                from n in [1, 2, 3, 4, 5]
+                where n > 3
+                orderby n in desc
+                limit 1, 3
+                select n
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from innerjoin where orderby limit select - 1"() {
+        assertScript '''
+            assert [[4, 4], [3, 3]] == GINQ {
+                from n1 in [1, 2, 3, 4, 5]
+                innerjoin n2 in [2, 3, 4, 5, 6] on n2 == n1
+                where n1 > 2
+                orderby n2 in desc
+                limit 1, 3
+                select n1, n2
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from innerjoin where orderby limit select - 2"() {
+        assertScript '''
+            assert [[4, 4, 4]] == GINQ {
+                from n1 in [1, 2, 3, 4, 5]
+                innerjoin n2 in [2, 3, 4, 5, 6] on n2 == n1
+                innerjoin n3 in [3, 4, 5, 6, 7] on n3 == n2
+                where n1 > 3
+                orderby n2 in desc
+                limit 1, 3
+                select n1, n2, n3
+            }.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(n) // reference the column `n` in the groupby clause, and `count` is a built-in aggregate function
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from groupby select - 2"() {
+        assertScript '''
+            assert [[1, 2], [3, 6], [6, 18]] == GINQ {
+                from n in [1, 1, 3, 3, 6, 6, 6]
+                groupby n
+                select n, sum(n)
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from groupby select - 3"() {
+        assertScript '''
+            @groovy.transform.EqualsAndHashCode
+            class Person {
+                String name
+                int weight
+                String gender
+                
+                Person(String name, int weight, String gender) {
+                    this.name = name
+                    this.weight = weight
+                    this.gender = gender
+                }
+            }
+            def persons = [new Person('Linda', 100, 'Female'), new Person('Daniel', 135, 'Male'), new Person('David', 121, 'Male')]
+            assert [['Female', 1], ['Male', 2]] == GINQ {
+                from p in persons
+                groupby p.gender
+                orderby count()
+                select p.gender, count()
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from groupby select - 4"() {
+        assertScript '''
+            @groovy.transform.EqualsAndHashCode
+            class Person {
+                String name
+                int weight
+                String gender
+                
+                Person(String name, int weight, String gender) {
+                    this.name = name
+                    this.weight = weight
+                    this.gender = gender
+                }
+            }
+            def persons = [new Person('Linda', 100, 'Female'), new Person('Daniel', 135, 'Male'), new Person('David', 121, 'Male')]
+            assert [['Female', 1], ['Male', 2]] == GINQ {
+                from p in persons
+                groupby p.gender
+                orderby count(p)
+                select p.gender, count(p)
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from groupby select - 5"() {
+        assertScript '''
+            @groovy.transform.EqualsAndHashCode
+            class Person {
+                String name
+                int weight
+                String gender
+                
+                Person(String name, int weight, String gender) {
+                    this.name = name
+                    this.weight = weight
+                    this.gender = gender
+                }
+            }
+            def persons = [new Person('Linda', 100, 'Female'), new Person('Daniel', 135, 'Male'), new Person('David', 121, 'Male')]
+            assert [['Female', 1], ['Male', 2]] == GINQ {
+                from p in persons
+                groupby p.gender
+                orderby count(p.gender)
+                select p.gender, count(p.gender)
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from groupby select - 6"() {
+        assertScript '''
+            @groovy.transform.EqualsAndHashCode
+            class Person {
+                String name
+                int weight
+                String gender
+                
+                Person(String name, int weight, String gender) {
+                    this.name = name
+                    this.weight = weight
+                    this.gender = gender
+                }
+            }
+            def persons = [new Person('Linda', 100, 'Female'), new Person('Daniel', 135, 'Male'), new Person('David', 121, 'Male')]
+            assert [['Female', 1], ['Male', 2]] == GINQ {
+                from p in persons
+                groupby p.gender
+                orderby count(p.name)
+                select p.gender, count(p.name)
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from groupby select - 7"() {
+        assertScript '''
+            assert [[1, 2], [3, 6], [6, 18]] == GINQ {
+                from n in [1, 1, 3, 3, 6, 6, 6]
+                groupby n
+                select n, agg(_q.stream().map(e -> e).reduce(BigDecimal.ZERO, BigDecimal::add)) // the most powerful aggregate function, `_q` represents the grouped Queryable object
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from groupby select - 8"() {
+        assertScript '''
+            @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
+                }
+            }
+            
+            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')]
+                          
+            assert [['Male', 30, 35], ['Female', 21, 30]] == GINQ {
+                from p in persons
+                groupby p.gender
+                select p.gender, min(p.age), max(p.age)
+            }.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(n)
+            }.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(n)
+            }.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(n)
+            }.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(n) in asc
+                select n, count(n)
+            }.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(n)
+            }.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(n)
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from groupby having select - 1"() {
+        assertScript '''
+            assert [[3, 2], [6, 3]] == GINQ {
+                from n in [1, 1, 3, 3, 6, 6, 6]
+                groupby n
+                having n >= 3
+                select n, count(n)
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from innerjoin where groupby having orderby select - 1"() {
+        assertScript '''
+            assert [[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
+                having n >= 1 && m < 3
+                orderby m in asc
+                select n, m, count(n)
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from innerjoin where groupby having orderby select - 2"() {
+        assertScript '''
+            assert [[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
+                having count(m) > 4
+                orderby count(n) in asc
+                select n, count(n)
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from innerjoin where groupby having orderby select - 3"() {
+        assertScript '''
+            assert [[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
+                having count(n) > 4
+                orderby count(n) in asc
+                select n, count(n)
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - from innerjoin where groupby having orderby select - 4"() {
+        assertScript '''
+            assert [[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
+                having count() > 4
+                orderby count(n) in asc
+                select n, count(n)
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - query json - 1"() {
+        assertScript """
+            import groovy.json.JsonSlurper
+            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()
+        """
+    }
+
+    @Test
+    void "testGinq - ascii table - 1"() {
+        assertScript '''
+            def q = GINQ {
+                from n in [1, 2, 3]
+                select n as first_col, n + 1 as second_col
+            }
+    
+            def result = q.toString()
+            println result
+    
+            def expected = '+-----------+------------+\\n| first_col | second_col |\\n+-----------+------------+\\n| 1         | 2          |\\n| 2         | 3          |\\n| 3         | 4          |\\n+-----------+------------+\\n\'
+            assert expected == result
+        '''
+    }
+
+    @Test
+    void "testGinq - as List - 1"() {
+        assertScript '''
+            assert [4, 16, 36, 64, 100] == GINQ {from n in 1..<11 where n % 2 == 0 select n ** 2} as List
+        '''
+    }
+
+    @Test
+    void "testGinq - as List - 2"() {
+        assertScript '''
+            @groovy.transform.CompileStatic
+            def x() {
+                GINQ {from n in 1..<11 where n % 2 == 0 select n ** 2} as List
+            }
+            assert [4, 16, 36, 64, 100] == x()
+        '''
+    }
+}
diff --git a/subprojects/groovy-ginq/src/test/groovy/org/apache/groovy/ginq/provider/collection/NamedTupleTest.groovy b/subprojects/groovy-ginq/src/test/groovy/org/apache/groovy/ginq/provider/collection/NamedTupleTest.groovy
new file mode 100644
index 0000000..7ca07a0
--- /dev/null
+++ b/subprojects/groovy-ginq/src/test/groovy/org/apache/groovy/ginq/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.ginq.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-ginq/src/test/groovy/org/apache/groovy/ginq/provider/collection/QueryableCollectionTest.groovy b/subprojects/groovy-ginq/src/test/groovy/org/apache/groovy/ginq/provider/collection/QueryableCollectionTest.groovy
new file mode 100644
index 0000000..5f2b267
--- /dev/null
+++ b/subprojects/groovy-ginq/src/test/groovy/org/apache/groovy/ginq/provider/collection/QueryableCollectionTest.groovy
@@ -0,0 +1,447 @@
+/*
+ *  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.ginq.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.ginq.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()
+        assert [1, 2, 3] == from(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
+    @CompileDynamic
+    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, g -> g.v1 > 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 testGroupBySelect4() {
+        def persons = [new Person2('Linda', 100, 'Female'),
+                       new Person2('Daniel', 135, 'Male'),
+                       new Person2('David', 121, 'Male')]
+
+        def result = from(persons).groupBy(p -> p.gender)
+                .select(e -> Tuple.tuple(e.v1, e.v2.count())).toList()
+
+        assert [['Male', 2], ['Female', 1]] == result
+    }
+
+    @Test
+    @CompileDynamic
+    void testGroupBySelect5() {
+        def persons = [new Person2('Linda', 100, 'Female'),
+                       new Person2('Daniel', 135, 'Male'),
+                       new Person2('David', 121, 'Male')]
+
+        def result = from(persons).groupBy(p -> new NamedTuple<>([p.gender], ['gender']))
+                .select(e -> Tuple.tuple(e.v1.gender, e.v2.min(p -> p.weight), e.v2.max(p -> p.weight))).toList()
+
+        assert [['Male', 121, 135], ['Female', 100, 100]] == 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
+        }
+    }
+
+    @ToString
+    @EqualsAndHashCode
+    class Person2 {
+        String name
+        int weight
+        String gender
+
+        Person2(String name, int weight, String gender) {
+            this.name = name
+            this.weight = weight
+            this.gender = gender
+        }
+    }
+}
diff --git a/subprojects/groovy-ginq/src/test/groovy/org/apache/groovy/ginq/tools/GinqTestRig.groovy b/subprojects/groovy-ginq/src/test/groovy/org/apache/groovy/ginq/tools/GinqTestRig.groovy
new file mode 100644
index 0000000..a8cc945
--- /dev/null
+++ b/subprojects/groovy-ginq/src/test/groovy/org/apache/groovy/ginq/tools/GinqTestRig.groovy
@@ -0,0 +1,40 @@
+/*
+ *  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.ginq.tools
+
+import groovy.console.ui.AstNodeToScriptAdapter
+import org.codehaus.groovy.control.CompilerConfiguration
+import org.codehaus.groovy.control.Phases
+
+/**
+ * The test rig for GINQ
+ *
+ * @since 4.0.0
+ */
+class GinqTestRig {
+    static String inspect(String src) {
+        String result = new AstNodeToScriptAdapter()
+                .compileToScript(src, Phases.SEMANTIC_ANALYSIS,
+                        new GroovyClassLoader(), false, true,
+                        new CompilerConfiguration(CompilerConfiguration.DEFAULT))
+
+        return result
+    }
+}