You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by su...@apache.org on 2020/10/06 17:37:30 UTC
[groovy] 01/01: GROOVY-8258: [GEP] Create a LINQ-like DSL(very
basic version)
This is an automated email from the ASF dual-hosted git repository.
sunlan pushed a commit to branch GROOVY-8258
in repository https://gitbox.apache.org/repos/asf/groovy.git
commit 638464a0f668978d51cba2a733949db1f6bd14eb
Author: Daniel Sun <su...@apache.org>
AuthorDate: Wed Oct 7 01:37:01 2020 +0800
GROOVY-8258: [GEP] Create a LINQ-like DSL(very basic version)
---
settings.gradle | 1 +
.../codehaus/groovy/ast/tools/GeneralUtils.java | 9 +
subprojects/groovy-linq/build.gradle | 31 ++
.../apache/groovy/linq/GinqGroovyMethods.groovy | 47 +++
.../groovy/org/apache/groovy/linq/Queryable.java | 126 +++++++
.../org/apache/groovy/linq/dsl/GinqAstBuilder.java | 141 +++++++
.../apache/groovy/linq/dsl/GinqAstWalker.groovy | 286 +++++++++++++++
.../apache/groovy/linq/dsl/GinqSyntaxError.java | 43 +++
.../org/apache/groovy/linq/dsl/GinqVisitor.java | 43 +++
.../groovy/linq/dsl/SyntaxErrorReportable.java | 43 +++
.../dsl/expression/AbstractGinqExpression.java | 69 ++++
.../linq/dsl/expression/DataSourceExpression.java | 43 +++
.../linq/dsl/expression/FilterExpression.java | 42 +++
.../linq/dsl/expression/FilterableExpression.java | 39 ++
.../groovy/linq/dsl/expression/FromExpression.java | 47 +++
.../groovy/linq/dsl/expression/GinqExpression.java | 30 ++
.../linq/dsl/expression/InnerJoinExpression.java | 39 ++
.../groovy/linq/dsl/expression/JoinExpression.java | 42 +++
.../groovy/linq/dsl/expression/OnExpression.java | 38 ++
.../linq/dsl/expression/SelectExpression.java | 51 +++
.../linq/dsl/expression/SimpleGinqExpression.java | 72 ++++
.../linq/dsl/expression/WhereExpression.java | 38 ++
.../groovy/linq/provider/QueryableCollection.java | 251 +++++++++++++
.../org/apache/groovy/linq/GinqErrorTest.groovy | 82 +++++
.../groovy/org/apache/groovy/linq/GinqTest.groovy | 380 +++++++++++++++++++
.../linq/provider/QueryableCollectionTest.groovy | 407 +++++++++++++++++++++
26 files changed, 2440 insertions(+)
diff --git a/settings.gradle b/settings.gradle
index b8a3622..3aca670 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -45,6 +45,7 @@ def subprojects = ['groovy-ant',
'groovy-jmx',
'groovy-json',
'groovy-jsr223',
+ 'groovy-linq',
'groovy-macro',
'groovy-macro-library',
'groovy-nio',
diff --git a/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java b/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java
index 6abbe61..15e4db6 100644
--- a/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java
+++ b/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java
@@ -43,6 +43,7 @@ import org.codehaus.groovy.ast.expr.ConstructorCallExpression;
import org.codehaus.groovy.ast.expr.DeclarationExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.FieldExpression;
+import org.codehaus.groovy.ast.expr.LambdaExpression;
import org.codehaus.groovy.ast.expr.ListExpression;
import org.codehaus.groovy.ast.expr.MapEntryExpression;
import org.codehaus.groovy.ast.expr.MapExpression;
@@ -251,6 +252,14 @@ public class GeneralUtils {
return closureX(Parameter.EMPTY_ARRAY, code);
}
+ public static LambdaExpression lambdaX(final Parameter[] params, final Statement code) {
+ return new LambdaExpression(params, code);
+ }
+
+ public static LambdaExpression lambdaX(final Statement code) {
+ return lambdaX(Parameter.EMPTY_ARRAY, code);
+ }
+
/**
* Builds a binary expression that compares two values.
*
diff --git a/subprojects/groovy-linq/build.gradle b/subprojects/groovy-linq/build.gradle
new file mode 100644
index 0000000..cc476c4
--- /dev/null
+++ b/subprojects/groovy-linq/build.gradle
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+dependencies {
+ implementation rootProject
+ implementation project(':groovy-macro')
+ testImplementation rootProject.sourceSets.test.runtimeClasspath
+ testImplementation project(':groovy-test')
+}
+
+task moduleDescriptor(type: org.codehaus.groovy.gradle.WriteExtensionDescriptorTask) {
+ extensionClasses = 'org.apache.groovy.linq.GinqGroovyMethods'
+}
+
+compileJava.dependsOn moduleDescriptor
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/GinqGroovyMethods.groovy b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/GinqGroovyMethods.groovy
new file mode 100644
index 0000000..534810b
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/GinqGroovyMethods.groovy
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq
+
+import groovy.transform.CompileStatic
+import org.apache.groovy.linq.dsl.GinqAstBuilder
+import org.apache.groovy.linq.dsl.GinqAstWalker
+import org.apache.groovy.linq.dsl.expression.SimpleGinqExpression
+import org.codehaus.groovy.ast.expr.ClosureExpression
+import org.codehaus.groovy.ast.expr.Expression
+import org.codehaus.groovy.ast.expr.MethodCallExpression
+import org.codehaus.groovy.ast.stmt.Statement
+import org.codehaus.groovy.macro.runtime.Macro
+import org.codehaus.groovy.macro.runtime.MacroContext
+
+@CompileStatic
+class GinqGroovyMethods {
+ @Macro
+ static Expression GINQ(MacroContext ctx, final ClosureExpression closureExpression) {
+ Statement code = closureExpression.getCode()
+
+ GinqAstBuilder ginqAstBuilder = new GinqAstBuilder(ctx.getSourceUnit())
+ code.visit(ginqAstBuilder)
+ SimpleGinqExpression simpleGinqExpression = ginqAstBuilder.getSimpleGinqExpression()
+
+ GinqAstWalker ginqBuilder = new GinqAstWalker(ctx.getSourceUnit())
+ MethodCallExpression selectMethodCallExpression = ginqBuilder.visitSimpleGinqExpression(simpleGinqExpression)
+
+ return selectMethodCallExpression
+ }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/Queryable.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/Queryable.java
new file mode 100644
index 0000000..56803ac
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/Queryable.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq;
+
+import groovy.lang.Tuple2;
+import groovy.transform.Internal;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.BiPredicate;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+
+/**
+ * Represents the queryable objects, e.g. Java collections
+ *
+ * @param <T> the type of Queryable element
+ * @since 4.0.0
+ */
+@Internal
+public interface Queryable<T> {
+ <U> Queryable<Tuple2<T, U>> innerJoin(Queryable<? extends U> queryable, BiPredicate<? super T, ? super U> joiner);
+
+ <U> Queryable<Tuple2<T, U>> leftJoin(Queryable<? extends U> queryable, BiPredicate<? super T, ? super U> joiner);
+
+ <U> Queryable<Tuple2<T, U>> rightJoin(Queryable<? extends U> queryable, BiPredicate<? super T, ? super U> joiner);
+
+ default <U> Queryable<Tuple2<T, U>> fullJoin(Queryable<? extends U> queryable, BiPredicate<? super T, ? super U> joiner) {
+ Queryable<Tuple2<T, U>> lj = this.leftJoin(queryable, joiner);
+ Queryable<Tuple2<T, U>> rj = this.rightJoin(queryable, joiner);
+ return lj.union(rj);
+ }
+
+ <U> Queryable<Tuple2<T, U>> crossJoin(Queryable<? extends U> queryable);
+
+ Queryable<T> where(Predicate<? super T> filter);
+
+ <K> Queryable<Tuple2<K, Queryable<T>>> groupBy(Function<? super T, ? extends K> classifier, BiPredicate<? super K, ? super Queryable<? extends T>> having);
+
+ default <K> Queryable<Tuple2<K, Queryable<T>>> groupBy(Function<? super T, ? extends K> classifier) {
+ return groupBy(classifier, (k, l) -> true);
+ }
+
+ <U extends Comparable<? super U>> Queryable<T> orderBy(Order<? super T, ? extends U>... orders);
+
+ Queryable<T> limit(int offset, int size);
+
+ default Queryable<T> limit(int size) {
+ return limit(0, size);
+ }
+
+ <U> Queryable<U> select(Function<? super T, ? extends U> mapper);
+
+ Queryable<T> distinct();
+
+ default Queryable<T> union(Queryable<? extends T> queryable) {
+ return this.unionAll(queryable).distinct();
+ }
+
+ Queryable<T> unionAll(Queryable<? extends T> queryable);
+
+ Queryable<T> intersect(Queryable<? extends T> queryable);
+
+ Queryable<T> minus(Queryable<? extends T> queryable);
+
+ List<T> toList();
+
+ default Stream<T> stream() {
+ return toList().stream();
+ }
+
+ // Built-in aggregate functions {
+ int count();
+ BigDecimal sum(Function<? super T, BigDecimal> mapper);
+ // } Built-in aggregate functions
+
+ class Order<T, U extends Comparable<? super U>> {
+ private final Function<? super T, ? extends U> keyExtractor;
+ private final boolean asc;
+
+ public Order(Function<? super T, ? extends U> keyExtractor, boolean asc) {
+ this.keyExtractor = keyExtractor;
+ this.asc = asc;
+ }
+
+ public Function<? super T, ? extends U> getKeyExtractor() {
+ return keyExtractor;
+ }
+
+ public boolean isAsc() {
+ return asc;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Order)) return false;
+ Order<?, ?> order = (Order<?, ?>) o;
+ return asc == order.asc &&
+ keyExtractor.equals(order.keyExtractor);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(keyExtractor, asc);
+ }
+ }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqAstBuilder.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqAstBuilder.java
new file mode 100644
index 0000000..ee37ce5
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqAstBuilder.java
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.dsl;
+
+import org.apache.groovy.linq.dsl.expression.FilterExpression;
+import org.apache.groovy.linq.dsl.expression.FilterableExpression;
+import org.apache.groovy.linq.dsl.expression.FromExpression;
+import org.apache.groovy.linq.dsl.expression.GinqExpression;
+import org.apache.groovy.linq.dsl.expression.InnerJoinExpression;
+import org.apache.groovy.linq.dsl.expression.JoinExpression;
+import org.apache.groovy.linq.dsl.expression.OnExpression;
+import org.apache.groovy.linq.dsl.expression.SelectExpression;
+import org.apache.groovy.linq.dsl.expression.SimpleGinqExpression;
+import org.apache.groovy.linq.dsl.expression.WhereExpression;
+import org.codehaus.groovy.GroovyBugError;
+import org.codehaus.groovy.ast.CodeVisitorSupport;
+import org.codehaus.groovy.ast.expr.ArgumentListExpression;
+import org.codehaus.groovy.ast.expr.BinaryExpression;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.expr.MethodCallExpression;
+import org.codehaus.groovy.control.SourceUnit;
+import org.codehaus.groovy.syntax.Types;
+
+/**
+ * Build the AST for GINQ
+ *
+ * @since 4.0.0
+ */
+public class GinqAstBuilder extends CodeVisitorSupport implements SyntaxErrorReportable {
+ private SimpleGinqExpression simpleGinqExpression = new SimpleGinqExpression(); // store the result
+ private GinqExpression ginqExpression; // store the return value
+ private final SourceUnit sourceUnit;
+
+ public GinqAstBuilder(SourceUnit sourceUnit) {
+ this.sourceUnit = sourceUnit;
+ }
+
+ public SimpleGinqExpression getSimpleGinqExpression() {
+ return simpleGinqExpression;
+ }
+
+ @Override
+ public void visitMethodCallExpression(MethodCallExpression call) {
+ super.visitMethodCallExpression(call);
+ final String methodName = call.getMethodAsString();
+
+ if ("from".equals(methodName) || "innerJoin".equals(methodName)) {
+ ArgumentListExpression arguments = (ArgumentListExpression) call.getArguments();
+ if (arguments.getExpressions().size() != 1) {
+ this.collectSyntaxError(
+ new GinqSyntaxError(
+ "Only 1 argument expected for `" + methodName + "`, e.g. `" + methodName + " n in nums`",
+ call.getLineNumber(), call.getColumnNumber()
+ )
+ );
+ }
+ final Expression expression = arguments.getExpression(0);
+ if (!(expression instanceof BinaryExpression
+ && ((BinaryExpression) expression).getOperation().getType() == Types.KEYWORD_IN)) {
+ this.collectSyntaxError(
+ new GinqSyntaxError(
+ "`in` is expected for `" + methodName + "`, e.g. `" + methodName + " n in nums`",
+ call.getLineNumber(), call.getColumnNumber()
+ )
+ );
+ }
+ BinaryExpression binaryExpression = (BinaryExpression) expression;
+ Expression aliasExpr = binaryExpression.getLeftExpression();
+ Expression dataSourceExpr = binaryExpression.getRightExpression();
+
+ FilterableExpression filterableExpression = null;
+ if ("from".equals(methodName)) {
+ filterableExpression = new FromExpression(aliasExpr, dataSourceExpr);
+ simpleGinqExpression.setFromExpression((FromExpression) filterableExpression);
+ } else if ("innerJoin".equals(methodName)) {
+ filterableExpression = new InnerJoinExpression(aliasExpr, dataSourceExpr);
+ simpleGinqExpression.addJoinExpression((JoinExpression) filterableExpression);
+ }
+ filterableExpression.setSourcePosition(call);
+ ginqExpression = filterableExpression;
+
+ return;
+ }
+
+ if ("where".equals(methodName) || "on".equals(methodName)) {
+ Expression filterExpr = ((ArgumentListExpression) call.getArguments()).getExpression(0);
+
+ FilterExpression filterExpression = null;
+ if ("where".equals(methodName)) {
+ filterExpression = new WhereExpression(filterExpr);
+ } else if ("on".equals(methodName)) {
+ filterExpression = new OnExpression(filterExpr);
+ }
+
+ if (null == filterExpression) {
+ throw new GroovyBugError("Unknown method: " + methodName);
+ }
+
+ filterExpression.setSourcePosition(call);
+
+ if (ginqExpression instanceof FilterableExpression) { // TODO more strict check
+ ((FilterableExpression) ginqExpression).addFilterExpression(filterExpression);
+ } else {
+ throw new GroovyBugError("The preceding expression is not a FilterableExpression: " + ginqExpression);
+ }
+
+ return;
+ }
+
+ if ("select".equals(methodName)) {
+ SelectExpression selectExpression = new SelectExpression(call.getArguments());
+ selectExpression.setSourcePosition(call);
+
+ simpleGinqExpression.setSelectExpression(selectExpression);
+ ginqExpression = selectExpression;
+
+ return;
+ }
+ }
+
+ @Override
+ public SourceUnit getSourceUnit() {
+ return sourceUnit;
+ }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqAstWalker.groovy b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqAstWalker.groovy
new file mode 100644
index 0000000..ad0f720
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqAstWalker.groovy
@@ -0,0 +1,286 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.dsl
+
+import groovy.transform.CompileDynamic
+import groovy.transform.CompileStatic
+import org.apache.groovy.linq.dsl.expression.DataSourceExpression
+import org.apache.groovy.linq.dsl.expression.FilterExpression
+import org.apache.groovy.linq.dsl.expression.FromExpression
+import org.apache.groovy.linq.dsl.expression.GinqExpression
+import org.apache.groovy.linq.dsl.expression.InnerJoinExpression
+import org.apache.groovy.linq.dsl.expression.JoinExpression
+import org.apache.groovy.linq.dsl.expression.OnExpression
+import org.apache.groovy.linq.dsl.expression.SelectExpression
+import org.apache.groovy.linq.dsl.expression.SimpleGinqExpression
+import org.apache.groovy.linq.dsl.expression.WhereExpression
+import org.codehaus.groovy.ast.ClassHelper
+import org.codehaus.groovy.ast.expr.ArgumentListExpression
+import org.codehaus.groovy.ast.expr.Expression
+import org.codehaus.groovy.ast.expr.ExpressionTransformer
+import org.codehaus.groovy.ast.expr.ListExpression
+import org.codehaus.groovy.ast.expr.MethodCallExpression
+import org.codehaus.groovy.ast.expr.TupleExpression
+import org.codehaus.groovy.ast.expr.VariableExpression
+import org.codehaus.groovy.control.SourceUnit
+
+import static org.codehaus.groovy.ast.tools.GeneralUtils.callX
+import static org.codehaus.groovy.ast.tools.GeneralUtils.lambdaX
+import static org.codehaus.groovy.ast.tools.GeneralUtils.param
+import static org.codehaus.groovy.ast.tools.GeneralUtils.params
+import static org.codehaus.groovy.ast.tools.GeneralUtils.propX
+import static org.codehaus.groovy.ast.tools.GeneralUtils.stmt
+
+/**
+ * Visit AST of GINQ and generate target method calls for GINQ
+ *
+ * @since 4.0.0
+ */
+@CompileStatic
+class GinqAstWalker implements GinqVisitor<Object>, SyntaxErrorReportable {
+
+ private final SourceUnit sourceUnit;
+
+ public GinqAstWalker(SourceUnit sourceUnit) {
+ this.sourceUnit = sourceUnit;
+ }
+
+ @Override
+ MethodCallExpression visitSimpleGinqExpression(SimpleGinqExpression simpleGinqExpression) {
+ FromExpression fromExpression = simpleGinqExpression.getFromExpression()
+ MethodCallExpression fromMethodCallExpression = this.visitFromExpression(fromExpression)
+
+ MethodCallExpression selectMethodReceiver = fromMethodCallExpression
+
+ JoinExpression lastJoinExpression = null
+ MethodCallExpression lastJoinMethodCallExpression = null
+ for (JoinExpression joinExpression : simpleGinqExpression.getJoinExpressionList()) {
+ joinExpression.putNodeMetaData(__METHOD_CALL_RECEIVER, lastJoinMethodCallExpression ?: fromMethodCallExpression)
+ joinExpression.putNodeMetaData(__DATA_SOURCE_EXPRESSION, lastJoinExpression ?: fromExpression)
+
+ lastJoinExpression = joinExpression
+ lastJoinMethodCallExpression = this.visitInnerJoinExpression((InnerJoinExpression) lastJoinExpression)
+ }
+
+ if (lastJoinMethodCallExpression) {
+ selectMethodReceiver = lastJoinMethodCallExpression
+ }
+
+ SelectExpression selectExpression = simpleGinqExpression.getSelectExpression()
+ selectExpression.putNodeMetaData(__METHOD_CALL_RECEIVER, selectMethodReceiver)
+ selectExpression.putNodeMetaData(__DATA_SOURCE_EXPRESSION, lastJoinExpression ?: fromExpression)
+
+ MethodCallExpression selectMethodCallExpression = this.visitSelectExpression(selectExpression)
+
+
+ return selectMethodCallExpression
+ }
+
+ @Override
+ MethodCallExpression visitFromExpression(FromExpression fromExpression) {
+ MethodCallExpression fromMethodCallExpression = constructFromMethodCallExpression(fromExpression)
+
+ List<FilterExpression> filterExpressionList = fromExpression.getFilterExpressionList()
+ if (filterExpressionList) {
+ WhereExpression whereExpression = (WhereExpression) filterExpressionList.get(0)
+ whereExpression.putNodeMetaData(__DATA_SOURCE_EXPRESSION, fromExpression)
+ whereExpression.putNodeMetaData(__METHOD_CALL_RECEIVER, fromMethodCallExpression)
+
+ return visitWhereExpression(whereExpression)
+ }
+
+ return fromMethodCallExpression
+ }
+
+ @Override
+ MethodCallExpression visitInnerJoinExpression(InnerJoinExpression innerJoinExpression) {
+ Expression receiver = innerJoinExpression.getNodeMetaData(__METHOD_CALL_RECEIVER)
+ DataSourceExpression dataSourceExpression = innerJoinExpression.getNodeMetaData(__DATA_SOURCE_EXPRESSION)
+ Expression receiverAliasExpr = dataSourceExpression.aliasExpr
+ List<FilterExpression> filterExpressionList = innerJoinExpression.getFilterExpressionList()
+ int filterExpressionListSize = filterExpressionList.size()
+
+ if (0 == filterExpressionListSize) {
+ this.collectSyntaxError(
+ new GinqSyntaxError(
+ "`on` clause is expected for `innerJoin`",
+ innerJoinExpression.getLineNumber(), innerJoinExpression.getColumnNumber()
+ )
+ );
+ }
+
+ OnExpression onExpression = (OnExpression) filterExpressionList.get(0)
+
+ WhereExpression whereExpression = null
+ if (filterExpressionListSize > 1) {
+ whereExpression = (WhereExpression) filterExpressionList.get(1)
+ }
+
+ MethodCallExpression innerJoinMethodCallExpression = constructInnerJoinMethodCallExpression(receiver, receiverAliasExpr, innerJoinExpression, onExpression, whereExpression)
+
+ return innerJoinMethodCallExpression
+ }
+
+ @Override
+ MethodCallExpression visitOnExpression(OnExpression onExpression) {
+ return null // do nothing
+ }
+
+ @CompileDynamic
+ private MethodCallExpression constructFromMethodCallExpression(FromExpression fromExpression) {
+ macro {
+ org.apache.groovy.linq.provider.QueryableCollection.from($v { fromExpression.dataSourceExpr })
+ }
+ }
+
+ @CompileDynamic
+ private MethodCallExpression constructInnerJoinMethodCallExpression(
+ Expression receiver, Expression receiverAliasExpr, InnerJoinExpression innerJoinExpression,
+ OnExpression onExpression, WhereExpression whereExpression) {
+
+ MethodCallExpression innerJoinMethodCallExpression = macro {
+ $v{receiver}.innerJoin(org.apache.groovy.linq.provider.QueryableCollection.from($v { innerJoinExpression.dataSourceExpr }))
+ }
+
+ ((ArgumentListExpression) innerJoinMethodCallExpression.getArguments()).getExpressions().add(
+ lambdaX(
+ params(
+ param(ClassHelper.DYNAMIC_TYPE, receiverAliasExpr.text),
+ param(ClassHelper.DYNAMIC_TYPE, innerJoinExpression.aliasExpr.text)
+ ),
+ stmt(onExpression.getFilterExpr())
+ )
+ )
+
+ if (whereExpression) {
+ whereExpression.putNodeMetaData(__DATA_SOURCE_EXPRESSION, innerJoinExpression)
+ whereExpression.putNodeMetaData(__METHOD_CALL_RECEIVER, innerJoinMethodCallExpression)
+ return visitWhereExpression(whereExpression)
+ }
+
+ return innerJoinMethodCallExpression
+ }
+
+ @Override
+ MethodCallExpression visitWhereExpression(WhereExpression whereExpression) {
+ DataSourceExpression dataSourceExpression = whereExpression.getNodeMetaData(__DATA_SOURCE_EXPRESSION)
+ Expression fromMethodCallExpression = whereExpression.getNodeMetaData(__METHOD_CALL_RECEIVER)
+ Expression filterExpr = whereExpression.getFilterExpr()
+
+ return callXWithLambda(fromMethodCallExpression, "where", dataSourceExpression, filterExpr)
+ }
+
+ @Override
+ MethodCallExpression visitSelectExpression(SelectExpression selectExpression) {
+ Expression selectMethodReceiver = selectExpression.getNodeMetaData(__METHOD_CALL_RECEIVER)
+ DataSourceExpression dataSourceExpression = selectExpression.getNodeMetaData(__DATA_SOURCE_EXPRESSION)
+ Expression projectionExpr = selectExpression.getProjectionExpr()
+
+ List<Expression> expressionList = ((TupleExpression) projectionExpr).getExpressions()
+ Expression lambdaCode
+ if (expressionList.size() > 1) {
+ lambdaCode = new ListExpression(expressionList)
+ } else {
+ lambdaCode = expressionList.get(0)
+ }
+
+ return callXWithLambda(selectMethodReceiver, "select", dataSourceExpression, lambdaCode)
+ }
+
+ private static Expression correctVariablesOfGinqExpression(JoinExpression joinExpression, Expression expr) {
+ DataSourceExpression dataSourceExpression = joinExpression.getNodeMetaData(__DATA_SOURCE_EXPRESSION)
+ final Expression firstAliasExpr = dataSourceExpression.aliasExpr
+ final Expression secondAliasExpr = joinExpression.aliasExpr
+
+ // The synthetic lambda parameter `__t` represents the element from the result datasource of joining, e.g. `n1` innerJoin `n2`
+ // The element from first datasource(`n1`) is referenced via `_t.v1`
+ // and the element from second datasource(`n2`) is referenced via `_t.v2`
+ expr = expr.transformExpression(new ExpressionTransformer() {
+ @Override
+ Expression transform(Expression expression) {
+ if (expression instanceof VariableExpression) {
+ Expression transformedExpression = null
+ if (firstAliasExpr.text == expression.text) {
+ // replace `n1` with `__t.v1`
+ transformedExpression = constructFirstAliasVariableAccess()
+ } else if (secondAliasExpr.text == expression.text) {
+ // replace `n2` with `__t.v2`
+ transformedExpression = constructSecondAliasVariableAccess()
+ }
+
+ if (null != transformedExpression) {
+ return transformedExpression
+ }
+ }
+
+ return expression.transformExpression(this)
+ }
+ })
+ return expr
+ }
+
+ @Override
+ Object visit(GinqExpression expression) {
+ return expression.accept(this)
+ }
+
+ private static MethodCallExpression callXWithLambda(Expression receiver, String methodName, DataSourceExpression dataSourceExpression, Expression lambdaCode) {
+ String lambdaParamName
+ if (dataSourceExpression instanceof JoinExpression) {
+ lambdaParamName = __T
+ lambdaCode = correctVariablesOfGinqExpression((JoinExpression) dataSourceExpression, lambdaCode)
+ } else {
+ lambdaParamName = dataSourceExpression.aliasExpr.text
+ }
+
+ callXWithLambda(receiver, methodName, lambdaParamName, lambdaCode)
+ }
+
+ private static MethodCallExpression callXWithLambda(Expression receiver, String methodName, String lambdaParamName, Expression lambdaCode) {
+ callX(
+ receiver,
+ methodName,
+ lambdaX(
+ params(param(ClassHelper.DYNAMIC_TYPE, lambdaParamName)),
+ stmt(lambdaCode)
+ )
+ )
+ }
+
+ private static Expression constructFirstAliasVariableAccess() {
+ constructAliasVariableAccess('v1')
+ }
+
+ private static Expression constructSecondAliasVariableAccess() {
+ constructAliasVariableAccess('v2')
+ }
+
+ private static Expression constructAliasVariableAccess(String name) {
+ propX(new VariableExpression(__T), name)
+ }
+
+ @Override
+ SourceUnit getSourceUnit() {
+ sourceUnit
+ }
+
+ private static final String __DATA_SOURCE_EXPRESSION = "__dataSourceExpression"
+ private static final String __METHOD_CALL_RECEIVER = "__methodCallReceiver"
+ private static final String __T = "__t"
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqSyntaxError.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqSyntaxError.java
new file mode 100644
index 0000000..652d835
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqSyntaxError.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.dsl;
+
+/**
+ * Represents GINQ syntax error
+ *
+ * @since 4.0.0
+ */
+public class GinqSyntaxError extends AssertionError {
+ private final int line;
+ private final int column;
+
+ public GinqSyntaxError(String message, int line, int column) {
+ super(message, null);
+ this.line = line;
+ this.column = column;
+ }
+
+ public int getLine() {
+ return line;
+ }
+
+ public int getColumn() {
+ return column;
+ }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqVisitor.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqVisitor.java
new file mode 100644
index 0000000..3e5f27e
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqVisitor.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.dsl;
+
+import org.apache.groovy.linq.dsl.expression.FromExpression;
+import org.apache.groovy.linq.dsl.expression.GinqExpression;
+import org.apache.groovy.linq.dsl.expression.InnerJoinExpression;
+import org.apache.groovy.linq.dsl.expression.OnExpression;
+import org.apache.groovy.linq.dsl.expression.SelectExpression;
+import org.apache.groovy.linq.dsl.expression.SimpleGinqExpression;
+import org.apache.groovy.linq.dsl.expression.WhereExpression;
+
+/**
+ * Represents the visitor for AST of GINQ
+ *
+ * @param <R> the type of visit result
+ * @since 4.0.0
+ */
+public interface GinqVisitor<R> {
+ R visitSimpleGinqExpression(SimpleGinqExpression simpleGinqExpression);
+ R visitFromExpression(FromExpression fromExpression);
+ R visitInnerJoinExpression(InnerJoinExpression innerJoinExpression);
+ R visitOnExpression(OnExpression onExpression);
+ R visitWhereExpression(WhereExpression whereExpression);
+ R visitSelectExpression(SelectExpression selectExpression);
+ R visit(GinqExpression expression);
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/SyntaxErrorReportable.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/SyntaxErrorReportable.java
new file mode 100644
index 0000000..5628451
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/SyntaxErrorReportable.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.dsl;
+
+import org.codehaus.groovy.control.SourceUnit;
+import org.codehaus.groovy.control.messages.SyntaxErrorMessage;
+import org.codehaus.groovy.syntax.SyntaxException;
+
+/**
+ * Supports reporting the syntax error of GINQ
+ *
+ * @since 4.0.0
+ */
+public interface SyntaxErrorReportable {
+ SourceUnit getSourceUnit();
+
+ default void collectSyntaxError(GinqSyntaxError ginqSyntaxError) {
+ SourceUnit sourceUnit = getSourceUnit();
+
+ SyntaxException e = new SyntaxException(
+ ginqSyntaxError.getMessage(),
+ ginqSyntaxError,
+ ginqSyntaxError.getLine(),
+ ginqSyntaxError.getColumn());
+ sourceUnit.getErrorCollector().addFatalError(new SyntaxErrorMessage(e, sourceUnit));
+ }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/AbstractGinqExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/AbstractGinqExpression.java
new file mode 100644
index 0000000..5cc3c36
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/AbstractGinqExpression.java
@@ -0,0 +1,69 @@
+package org.apache.groovy.linq.dsl.expression;
+
+import org.codehaus.groovy.ast.ASTNode;
+import org.codehaus.groovy.ast.NodeMetaDataHandler;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Represents GINQ expression which could hold meta data
+ *
+ * @since 4.0.0
+ */
+public abstract class AbstractGinqExpression implements GinqExpression, NodeMetaDataHandler {
+ private Map<?, ?> metaDataMap = new LinkedHashMap<>();
+ private int lineNumber = -1;
+ private int columnNumber = -1;
+ private int lastLineNumber = -1;
+ private int lastColumnNumber = -1;
+
+ @Override
+ public Map<?, ?> getMetaDataMap() {
+ return metaDataMap;
+ }
+
+ @Override
+ public void setMetaDataMap(Map<?, ?> metaDataMap) {
+ this.metaDataMap = metaDataMap;
+ }
+
+ public void setSourcePosition(ASTNode node) {
+ this.lineNumber = node.getLineNumber();
+ this.columnNumber = node.getColumnNumber();
+ this.lastLineNumber = node.getLastLineNumber();
+ this.lastColumnNumber = node.getLastColumnNumber();
+ }
+
+ public int getLineNumber() {
+ return lineNumber;
+ }
+
+ public void setLineNumber(int lineNumber) {
+ this.lineNumber = lineNumber;
+ }
+
+ public int getColumnNumber() {
+ return columnNumber;
+ }
+
+ public void setColumnNumber(int columnNumber) {
+ this.columnNumber = columnNumber;
+ }
+
+ public int getLastLineNumber() {
+ return lastLineNumber;
+ }
+
+ public void setLastLineNumber(int lastLineNumber) {
+ this.lastLineNumber = lastLineNumber;
+ }
+
+ public int getLastColumnNumber() {
+ return lastColumnNumber;
+ }
+
+ public void setLastColumnNumber(int lastColumnNumber) {
+ this.lastColumnNumber = lastColumnNumber;
+ }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/DataSourceExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/DataSourceExpression.java
new file mode 100644
index 0000000..9961f14
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/DataSourceExpression.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.dsl.expression;
+
+import org.codehaus.groovy.ast.expr.Expression;
+
+/**
+ * Represents data source expression
+ *
+ * @since 4.0.0
+ */
+public abstract class DataSourceExpression extends FilterableExpression {
+ protected Expression aliasExpr;
+ protected Expression dataSourceExpr;
+
+ public DataSourceExpression(Expression aliasExpr, Expression dataSourceExpr) {
+ this.aliasExpr = aliasExpr;
+ this.dataSourceExpr = dataSourceExpr;
+ }
+
+ public Expression getAliasExpr() {
+ return aliasExpr;
+ }
+ public Expression getDataSourceExpr() {
+ return dataSourceExpr;
+ }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/FilterExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/FilterExpression.java
new file mode 100644
index 0000000..0fb4dde
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/FilterExpression.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.dsl.expression;
+
+import org.codehaus.groovy.ast.expr.Expression;
+
+/**
+ * Represents filter expression
+ *
+ * @since 4.0.0
+ */
+public abstract class FilterExpression extends AbstractGinqExpression {
+ protected Expression filterExpr;
+
+ public FilterExpression(Expression filterExpr) {
+ this.filterExpr = filterExpr;
+ }
+
+ public Expression getFilterExpr() {
+ return filterExpr;
+ }
+
+ public void setFilterExpr(Expression filterExpr) {
+ this.filterExpr = filterExpr;
+ }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/FilterableExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/FilterableExpression.java
new file mode 100644
index 0000000..2808438
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/FilterableExpression.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.dsl.expression;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents expression which could be filtered via {@code where} expression
+ *
+ * @since 4.0.0
+ */
+public abstract class FilterableExpression extends AbstractGinqExpression {
+ protected List<FilterExpression> filterExpressionList = new ArrayList<>();
+
+ public List<FilterExpression> getFilterExpressionList() {
+ return filterExpressionList;
+ }
+
+ public void addFilterExpression(FilterExpression filterExpression) {
+ this.filterExpressionList.add(filterExpression);
+ }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/FromExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/FromExpression.java
new file mode 100644
index 0000000..4133eec
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/FromExpression.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.dsl.expression;
+
+import org.apache.groovy.linq.dsl.GinqVisitor;
+import org.codehaus.groovy.ast.expr.Expression;
+
+/**
+ * Represents the from expression
+ *
+ * @since 4.0.0
+ */
+public class FromExpression extends DataSourceExpression {
+ public FromExpression(Expression aliasExpr, Expression dataSourceExpr) {
+ super(aliasExpr, dataSourceExpr);
+ }
+
+ @Override
+ public <R> R accept(GinqVisitor<R> visitor) {
+ return visitor.visitFromExpression(this);
+ }
+
+ @Override
+ public String toString() {
+ return "FromExpression{" +
+ "aliasExpr=" + aliasExpr +
+ ", dataSourceExpr=" + dataSourceExpr +
+ ", whereExpression=" + filterExpressionList.get(0) +
+ '}';
+ }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/GinqExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/GinqExpression.java
new file mode 100644
index 0000000..32ae24b
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/GinqExpression.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.dsl.expression;
+
+import org.apache.groovy.linq.dsl.GinqVisitor;
+
+/**
+ * Represents the GINQ expression
+ *
+ * @since 4.0.0
+ */
+public interface GinqExpression {
+ <R> R accept(GinqVisitor<R> visitor);
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/InnerJoinExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/InnerJoinExpression.java
new file mode 100644
index 0000000..8df0377
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/InnerJoinExpression.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.dsl.expression;
+
+import org.apache.groovy.linq.dsl.GinqVisitor;
+import org.codehaus.groovy.ast.expr.Expression;
+
+/**
+ * Represents inner join expression
+ *
+ * @since 4.0.0
+ */
+public class InnerJoinExpression extends JoinExpression {
+
+ public InnerJoinExpression(Expression aliasExpr, Expression dataSourceExpr) {
+ super(aliasExpr, dataSourceExpr);
+ }
+
+ @Override
+ public <R> R accept(GinqVisitor<R> visitor) {
+ return visitor.visitInnerJoinExpression(this);
+ }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/JoinExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/JoinExpression.java
new file mode 100644
index 0000000..4a98b36
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/JoinExpression.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.dsl.expression;
+
+import org.codehaus.groovy.ast.expr.Expression;
+
+/**
+ * Represents join expression
+ *
+ * @since 4.0.0
+ */
+public abstract class JoinExpression extends DataSourceExpression {
+ protected OnExpression onExpression;
+
+ public JoinExpression(Expression aliasExpr, Expression dataSourceExpr) {
+ super(aliasExpr, dataSourceExpr);
+ }
+
+ public OnExpression getOnExpression() {
+ return onExpression;
+ }
+
+ public void setOnExpression(OnExpression onExpression) {
+ this.onExpression = onExpression;
+ }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/OnExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/OnExpression.java
new file mode 100644
index 0000000..98394d7
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/OnExpression.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.dsl.expression;
+
+import org.apache.groovy.linq.dsl.GinqVisitor;
+import org.codehaus.groovy.ast.expr.Expression;
+
+/**
+ * Represents on expression
+ *
+ * @since 4.0.0
+ */
+public class OnExpression extends FilterExpression {
+ public OnExpression(Expression filterExpr) {
+ super(filterExpr);
+ }
+
+ @Override
+ public <R> R accept(GinqVisitor<R> visitor) {
+ return visitor.visitOnExpression(this);
+ }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/SelectExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/SelectExpression.java
new file mode 100644
index 0000000..82ec0c1
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/SelectExpression.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.dsl.expression;
+
+import org.apache.groovy.linq.dsl.GinqVisitor;
+import org.codehaus.groovy.ast.expr.Expression;
+
+/**
+ * Represents the select expression
+ *
+ * @since 4.0.0
+ */
+public class SelectExpression extends AbstractGinqExpression {
+ private final Expression projectionExpr;
+
+ public SelectExpression(Expression projectionExpr) {
+ this.projectionExpr = projectionExpr;
+ }
+
+ @Override
+ public <R> R accept(GinqVisitor<R> visitor) {
+ return visitor.visitSelectExpression(this);
+ }
+
+ public Expression getProjectionExpr() {
+ return projectionExpr;
+ }
+
+ @Override
+ public String toString() {
+ return "SelectExpression{" +
+ "projectionExpr=" + projectionExpr +
+ '}';
+ }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/SimpleGinqExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/SimpleGinqExpression.java
new file mode 100644
index 0000000..43abe75
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/SimpleGinqExpression.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.dsl.expression;
+
+import org.apache.groovy.linq.dsl.GinqVisitor;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represent the root expression of GINQ
+ *
+ * @since 4.0.0
+ */
+public class SimpleGinqExpression extends AbstractGinqExpression {
+ private FromExpression fromExpression;
+ private final List<JoinExpression> joinExpressionList = new ArrayList<>();
+ private SelectExpression selectExpression;
+
+ @Override
+ public <R> R accept(GinqVisitor<R> visitor) {
+ return visitor.visitSimpleGinqExpression(this);
+ }
+
+ public FromExpression getFromExpression() {
+ return fromExpression;
+ }
+
+ public void setFromExpression(FromExpression fromExpression) {
+ this.fromExpression = fromExpression;
+ }
+
+ public List<JoinExpression> getJoinExpressionList() {
+ return joinExpressionList;
+ }
+
+ public void addJoinExpression(JoinExpression joinExpression) {
+ joinExpressionList.add(joinExpression);
+ }
+
+ public SelectExpression getSelectExpression() {
+ return selectExpression;
+ }
+
+ public void setSelectExpression(SelectExpression selectExpression) {
+ this.selectExpression = selectExpression;
+ }
+
+ @Override
+ public String toString() {
+ return "SimpleGinqExpression{" +
+ "fromExpression=" + fromExpression +
+ ", selectExpression=" + selectExpression +
+ '}';
+ }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/WhereExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/WhereExpression.java
new file mode 100644
index 0000000..66bde4f
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/WhereExpression.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.dsl.expression;
+
+import org.apache.groovy.linq.dsl.GinqVisitor;
+import org.codehaus.groovy.ast.expr.Expression;
+
+/**
+ * Represent the where expression
+ *
+ * @since 4.0.0
+ */
+public class WhereExpression extends FilterExpression {
+ public WhereExpression(Expression filterExpr) {
+ super(filterExpr);
+ }
+
+ @Override
+ public <R> R accept(GinqVisitor<R> visitor) {
+ return visitor.visitWhereExpression(this);
+ }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/provider/QueryableCollection.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/provider/QueryableCollection.java
new file mode 100644
index 0000000..9d5d19d
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/provider/QueryableCollection.java
@@ -0,0 +1,251 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.provider;
+
+import groovy.lang.Tuple;
+import groovy.lang.Tuple2;
+import groovy.transform.Internal;
+import org.apache.groovy.linq.Queryable;
+
+import java.math.BigDecimal;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.function.BiPredicate;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+/**
+ * Represents the queryable collections
+ *
+ * @param <T> the type of Queryable element
+ * @since 4.0.0
+ */
+@Internal
+public class QueryableCollection<T> implements Queryable<T>, Iterable<T> {
+ private final Iterable<T> sourceIterable;
+ private Stream<T> sourceStream;
+
+ public static <T> Queryable<T> from(Iterable<T> sourceIterable) {
+ return new QueryableCollection<>(sourceIterable);
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <T> Queryable<T> from(Stream<? extends T> sourceStream) {
+ Iterable<T> sourceIterable = (Iterable<T>) toIterable(sourceStream);
+ return from(sourceIterable);
+ }
+
+ private QueryableCollection(Iterable<T> sourceIterable) {
+ this.sourceIterable = sourceIterable;
+ this.sourceStream = toStream(sourceIterable);
+ }
+
+ @Override
+ public Iterator<T> iterator() {
+ return sourceIterable.iterator();
+ }
+
+ @Override
+ public <U> Queryable<Tuple2<T, U>> innerJoin(Queryable<? extends U> queryable, BiPredicate<? super T, ? super U> joiner) {
+ Stream<Tuple2<T, U>> stream =
+ this.stream()
+ .flatMap(p ->
+ queryable.stream()
+ .filter(c -> joiner.test(p, c))
+ .map(c -> Tuple.tuple(p, c)));
+
+ return from(stream);
+ }
+
+ @Override
+ public <U> Queryable<Tuple2<T, U>> leftJoin(Queryable<? extends U> queryable, BiPredicate<? super T, ? super U> joiner) {
+ return outerJoin(this, queryable, joiner);
+ }
+
+ @Override
+ public <U> Queryable<Tuple2<T, U>> rightJoin(Queryable<? extends U> queryable, BiPredicate<? super T, ? super U> joiner) {
+ return outerJoin(queryable, this, (a, b) -> joiner.test(b, a)).select(e -> Tuple.tuple(e.getV2(), e.getV1()));
+ }
+
+ @Override
+ public <U> Queryable<Tuple2<T, U>> crossJoin(Queryable<? extends U> queryable) {
+ Stream<Tuple2<T, U>> stream =
+ this.stream()
+ .flatMap(p ->
+ queryable.stream()
+ .map(c -> Tuple.tuple(p, c)));
+
+ return from(stream);
+ }
+
+ @Override
+ public Queryable<T> where(Predicate<? super T> filter) {
+ Stream<T> stream = this.stream().filter(filter::test);
+
+ return from(stream);
+ }
+
+ @Override
+ public <K> Queryable<Tuple2<K, Queryable<T>>> groupBy(Function<? super T, ? extends K> classifier, BiPredicate<? super K, ? super Queryable<? extends T>> having) {
+ Stream<Tuple2<K, Queryable<T>>> stream =
+ this.stream()
+ .collect(Collectors.groupingBy(classifier, Collectors.toList()))
+ .entrySet().stream()
+ .filter(m -> having.test(m.getKey(), from(m.getValue())))
+ .map(m -> Tuple.tuple(m.getKey(), from(m.getValue())));
+
+ return from(stream);
+ }
+
+ @Override
+ public <U extends Comparable<? super U>> Queryable<T> orderBy(Order<? super T, ? extends U>... orders) {
+ Comparator<T> comparator = null;
+ for (int i = 0, n = orders.length; i < n; i++) {
+ Order<? super T, ? extends U> order = orders[i];
+ Comparator<U> ascOrDesc = order.isAsc() ? Comparator.naturalOrder() : Comparator.reverseOrder();
+ comparator =
+ 0 == i
+ ? Comparator.comparing(order.getKeyExtractor(), ascOrDesc)
+ : comparator.thenComparing(order.getKeyExtractor(), ascOrDesc);
+ }
+
+ if (null == comparator) {
+ return this;
+ }
+
+ return from(this.stream().sorted(comparator));
+ }
+
+ @Override
+ public Queryable<T> limit(int offset, int size) {
+ Stream<T> stream = this.stream().skip(offset).limit(size);
+
+ return from(stream);
+ }
+
+ @Override
+ public <U> Queryable<U> select(Function<? super T, ? extends U> mapper) {
+ Stream<U> stream = this.stream().map(mapper);
+
+ return from(stream);
+ }
+
+ @Override
+ public Queryable<T> distinct() {
+ Stream<? extends T> stream = this.stream().distinct();
+
+ return from(stream);
+ }
+
+ @Override
+ public Queryable<T> unionAll(Queryable<? extends T> queryable) {
+ Stream<T> stream = Stream.concat(this.stream(), queryable.stream());
+
+ return from(stream);
+ }
+
+ @Override
+ public Queryable<T> intersect(Queryable<? extends T> queryable) {
+ Stream<T> stream = this.stream().filter(a -> queryable.stream().anyMatch(b -> b.equals(a))).distinct();
+
+ return from(stream);
+ }
+
+ @Override
+ public Queryable<T> minus(Queryable<? extends T> queryable) {
+ Stream<T> stream = this.stream().filter(a -> queryable.stream().noneMatch(b -> b.equals(a))).distinct();
+
+ return from(stream);
+ }
+
+ @Override
+ public List<T> toList() {
+ return stream().collect(Collectors.toList());
+ }
+
+ @Override
+ public Stream<T> stream() {
+ try {
+ sourceStream = sourceStream.peek(e -> {}); // check whether the stream is usable
+ } catch (IllegalStateException ex) {
+ sourceStream = toStream(sourceIterable); // we have to create new stream every time because Java stream can not be reused
+ }
+
+ return sourceStream;
+ }
+
+ @Override
+ public int count() {
+ return toList().size();
+ }
+
+ @Override
+ public BigDecimal sum(Function<? super T, BigDecimal> mapper) {
+ return this.stream().map(mapper).reduce(BigDecimal.ZERO, BigDecimal::add);
+ }
+
+ private static <T, U> Queryable<Tuple2<T, U>> outerJoin(Queryable<? extends T> queryable1, Queryable<? extends U> queryable2, BiPredicate<? super T, ? super U> joiner) {
+ Stream<Tuple2<T, U>> stream =
+ queryable1.stream()
+ .flatMap(p ->
+ queryable2.stream()
+ .map(c -> joiner.test(p, c) ? c : null)
+ .reduce(new LinkedList<U>(), (r, e) -> {
+ int size = r.size();
+ if (0 == size) {
+ r.add(e);
+ return r;
+ }
+
+ int lastIndex = size - 1;
+ Object lastElement = r.get(lastIndex);
+
+ if (null != e) {
+ if (null == lastElement) {
+ r.set(lastIndex, e);
+ } else {
+ r.add(e);
+ }
+ }
+
+ return r;
+ }, (i, o) -> o).stream()
+ .map(c -> null == c ? Tuple.tuple(p, null) : Tuple.tuple(p, c)));
+
+ return from(stream);
+ }
+
+ private static <T> Stream<T> toStream(Iterable<T> sourceIterable) {
+ return StreamSupport.stream(sourceIterable.spliterator(), false);
+ }
+
+ private static <T> Iterable<T> toIterable(Stream<T> sourceStream) {
+ return sourceStream::iterator;
+ }
+
+ @Override
+ public String toString() {
+ return toList().toString();
+ }
+}
diff --git a/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/GinqErrorTest.groovy b/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/GinqErrorTest.groovy
new file mode 100644
index 0000000..0fedabd
--- /dev/null
+++ b/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/GinqErrorTest.groovy
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq
+
+import groovy.transform.CompileStatic
+import org.junit.Test
+
+import static groovy.test.GroovyAssert.shouldFail
+
+@CompileStatic
+class GinqErrorTest {
+ @Test
+ void "testGinq - from select - 1"() {
+ def err = shouldFail '''\
+ def numbers = [0, 1, 2]
+ GINQ {
+ from numbers
+ select n
+ }
+ '''
+
+ assert err.toString().contains('`in` is expected for `from`, e.g. `from n in nums` @ line 3, column 17.')
+ }
+
+ @Test
+ void "testGinq - from select - 2"() {
+ def err = shouldFail '''\
+ def numbers = [0, 1, 2]
+ GINQ {
+ from n as numbers
+ select n
+ }
+ '''
+
+ assert err.toString().contains('`in` is expected for `from`, e.g. `from n in nums` @ line 3, column 17.')
+ }
+
+ @Test
+ void "testGinq - from select - 3"() {
+ def err = shouldFail '''\
+ def numbers = [0, 1, 2]
+ GINQ {
+ from n, numbers
+ select n
+ }
+ '''
+
+ assert err.toString().contains('Only 1 argument expected for `from`, e.g. `from n in nums` @ line 3, column 17.')
+ }
+
+ @Test
+ void "testGinq - from innerJoin select - 1"() {
+ def err = shouldFail '''\
+ def nums1 = [1, 2, 3]
+ def nums2 = [1, 2, 3]
+ assert [[1, 1], [2, 2], [3, 3]] == GINQ {
+ from n1 in nums1
+ innerJoin n2 in nums2
+ select n1, n2
+ }.toList()
+ '''
+
+ assert err.toString().contains('`on` clause is expected for `innerJoin` @ line 5, column 17.')
+ }
+
+}
diff --git a/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/GinqTest.groovy b/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/GinqTest.groovy
new file mode 100644
index 0000000..e10301d
--- /dev/null
+++ b/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/GinqTest.groovy
@@ -0,0 +1,380 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq
+
+
+import groovy.transform.CompileStatic
+import org.junit.Test
+
+import static groovy.test.GroovyAssert.assertScript
+
+
+@CompileStatic
+class GinqTest {
+ @Test
+ void "testGinq - from select - 1"() {
+ assertScript '''
+ def numbers = [0, 1, 2]
+ assert [0, 1, 2] == GINQ {
+ from n in numbers
+ select n
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from select - 2"() {
+ assertScript '''
+ def numbers = [0, 1, 2]
+ assert [0, 2, 4] == GINQ {
+ from n in numbers
+ select n * 2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from select - 3"() {
+ assertScript '''
+ class Person {
+ String name
+ int age
+
+ Person(String name, int age) {
+ this.name = name
+ this.age = age
+ }
+ }
+
+ def persons = [new Person('Daniel', 35), new Person('Linda', 21), new Person('Peter', 30)]
+ assert [35, 21, 30] == GINQ {
+ from p in persons
+ select p.age
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from select - 4"() {
+ assertScript '''
+ class Person {
+ String name
+ int age
+
+ Person(String name, int age) {
+ this.name = name
+ this.age = age
+ }
+ }
+
+ def persons = [new Person('Daniel', 35), new Person('Linda', 21), new Person('Peter', 30)]
+ assert [['Daniel', 35], ['Linda', 21], ['Peter', 30]] == GINQ {
+ from p in persons
+ select p.name, p.age
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from select - 5"() {
+ assertScript '''
+ class Person {
+ String name
+ int age
+
+ Person(String name, int age) {
+ this.name = name
+ this.age = age
+ }
+ }
+
+ def persons = [new Person('Daniel', 35), new Person('Linda', 21), new Person('Peter', 30)]
+ assert [[name:'Daniel', age:35], [name:'Linda', age:21], [name:'Peter', age:30]] == GINQ {
+ from p in persons
+ select (name: p.name, age: p.age)
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from select - 6"() {
+ assertScript '''
+ def numbers = [0, 1, 2]
+ assert [0, 1, 2] == GINQ {
+ from n in numbers select n
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from where select - 1"() {
+ assertScript '''
+ def numbers = [0, 1, 2, 3, 4, 5]
+ assert [2, 4, 6] == GINQ {
+ from n in numbers
+ where n > 0 && n <= 3
+ select n * 2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from where select - 2"() {
+ assertScript '''
+ def numbers = [0, 1, 2, 3, 4, 5]
+ assert [2, 4, 6] == GINQ {
+ from n in numbers where n > 0 && n <= 3 select n * 2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerJoin select - 1"() {
+ assertScript '''
+ def nums1 = [1, 2, 3]
+ def nums2 = [1, 2, 3]
+ assert [[1, 1], [2, 2], [3, 3]] == GINQ {
+ from n1 in nums1
+ innerJoin n2 in nums2
+ on n1 == n2
+ select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerJoin select - 2"() {
+ assertScript '''
+ def nums1 = [1, 2, 3]
+ def nums2 = [1, 2, 3]
+ assert [[2, 1], [3, 2], [4, 3]] == GINQ {
+ from n1 in nums1
+ innerJoin n2 in nums2
+ on n1 == n2
+ select n1 + 1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerJoin select - 3"() {
+ assertScript '''
+ def nums1 = [1, 2, 3]
+ def nums2 = [1, 2, 3]
+ assert [[1, 2], [2, 3], [3, 4]] == GINQ {
+ from n1 in nums1
+ innerJoin n2 in nums2
+ on n1 == n2
+ select n1, n2 + 1
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerJoin select - 4"() {
+ assertScript '''
+ def nums1 = [1, 2, 3]
+ def nums2 = [1, 2, 3]
+ assert [[1, 2], [2, 3]] == GINQ {
+ from n1 in nums1
+ innerJoin n2 in nums2
+ on n1 + 1 == n2
+ select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerJoin select - 5"() {
+ assertScript '''
+ def nums1 = [1, 2, 3]
+ def nums2 = [1, 2, 3]
+ assert [[1, 2], [2, 3]] == GINQ {
+ from n1 in nums1 innerJoin n2 in nums2 on n1 + 1 == n2 select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerJoin select - 6"() {
+ assertScript '''
+ class Person {
+ String name
+ int age
+
+ Person(String name, int age) {
+ this.name = name
+ this.age = age
+ }
+ }
+
+ def persons1 = [new Person('Daniel', 35), new Person('Linda', 21), new Person('Peter', 30)]
+ def persons2 = [new Person('Jack', 35), new Person('Rose', 21), new Person('Smith', 30)]
+ assert [['Daniel', 'Jack'], ['Linda', 'Rose'], ['Peter', 'Smith']] == GINQ {
+ from p1 in persons1
+ innerJoin p2 in persons2
+ on p1.age == p2.age
+ select p1.name, p2.name
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerJoin select - 7"() {
+ assertScript '''
+ class Person {
+ String name
+ int age
+
+ Person(String name, int age) {
+ this.name = name
+ this.age = age
+ }
+ }
+
+ def persons1 = [new Person('Daniel', 35), new Person('Linda', 21), new Person('Peter', 30)]
+ def persons2 = [new Person('Jack', 35), new Person('Rose', 21), new Person('Smith', 30)]
+ assert [['DANIEL', 'JACK'], ['LINDA', 'ROSE'], ['PETER', 'SMITH']] == GINQ {
+ from p1 in persons1
+ innerJoin p2 in persons2
+ on p1.age == p2.age
+ select p1.name.toUpperCase(), p2.name.toUpperCase()
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerJoin select - 8"() {
+ assertScript '''
+ class Person {
+ String name
+ int age
+
+ Person(String name, int age) {
+ this.name = name
+ this.age = age
+ }
+ }
+
+ def same(str) { str }
+
+ def persons1 = [new Person('Daniel', 35), new Person('Linda', 21), new Person('Peter', 30)]
+ def persons2 = [new Person('Jack', 35), new Person('Rose', 21), new Person('Smith', 30)]
+ assert [['DANIEL', 'JACK'], ['LINDA', 'ROSE'], ['PETER', 'SMITH']] == GINQ {
+ from p1 in persons1
+ innerJoin p2 in persons2
+ on p1.age == p2.age
+ select same(p1.name.toUpperCase()), same(p2.name.toUpperCase())
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerJoin where select - 1"() {
+ assertScript '''
+ def nums1 = [1, 2, 3]
+ def nums2 = [1, 2, 3]
+ assert [[2, 2], [3, 3]] == GINQ {
+ from n1 in nums1
+ innerJoin n2 in nums2
+ on n1 == n2
+ where n1 > 1 && n2 <= 3
+ select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerJoin where select - 2"() {
+ assertScript '''
+ def nums1 = [1, 2, 3]
+ def nums2 = [1, 2, 3]
+ assert [[2, 2], [3, 3]] == GINQ {
+ from n1 in nums1
+ innerJoin n2 in nums2
+ on n1 == n2
+ where Math.pow(n1, 1) > 1 && Math.pow(n2, 1) <= 3
+ select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerJoin where select - 3"() {
+ assertScript '''
+ def nums1 = [1, 2, 3]
+ def nums2 = [1, 2, 3]
+ assert [[2, 2], [3, 3]] == GINQ {
+ from n1 in nums1 innerJoin n2 in nums2 on n1 == n2 where Math.pow(n1, 1) > 1 && Math.pow(n2, 1) <= 3 select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerJoin where select - 4"() {
+ assertScript '''
+ class Person {
+ String name
+ int age
+
+ Person(String name, int age) {
+ this.name = name
+ this.age = age
+ }
+ }
+
+ def persons1 = [new Person('Daniel', 35), new Person('Linda', 21), new Person('David', 30)]
+ def persons2 = [new Person('Jack', 35), new Person('Rose', 21), new Person('Smith', 30)]
+ assert [['Daniel', 'Jack']] == GINQ {
+ from p1 in persons1
+ innerJoin p2 in persons2
+ on p1.age == p2.age
+ where p1.name.startsWith('D') && p2.name.endsWith('k')
+ select p1.name, p2.name
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerJoin where select - 5"() {
+ assertScript '''
+ class Person {
+ String name
+ int age
+
+ Person(String name, int age) {
+ this.name = name
+ this.age = age
+ }
+ }
+
+ def same(obj) {obj}
+
+ def persons1 = [new Person('Daniel', 35), new Person('Linda', 21), new Person('David', 30)]
+ def persons2 = [new Person('Jack', 35), new Person('Rose', 21), new Person('Smith', 30)]
+ assert [['Daniel', 'Jack']] == GINQ {
+ from p1 in persons1
+ innerJoin p2 in persons2
+ on p1.age == p2.age
+ where same(p1.name.startsWith('D')) && same(p2.name.endsWith('k'))
+ select p1.name, p2.name
+ }.toList()
+ '''
+ }
+}
diff --git a/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/provider/QueryableCollectionTest.groovy b/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/provider/QueryableCollectionTest.groovy
new file mode 100644
index 0000000..344ffa4
--- /dev/null
+++ b/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/provider/QueryableCollectionTest.groovy
@@ -0,0 +1,407 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.provider
+
+
+import groovy.transform.CompileDynamic
+import groovy.transform.CompileStatic
+import groovy.transform.EqualsAndHashCode
+import groovy.transform.ToString
+import org.apache.groovy.linq.Queryable
+import org.junit.Test
+
+import java.util.stream.Collectors
+import java.util.stream.Stream
+
+import static org.apache.groovy.linq.provider.QueryableCollection.from
+
+@CompileStatic
+class QueryableCollectionTest {
+ @Test
+ void testFrom() {
+ assert [1, 2, 3] == from(Stream.of(1, 2, 3)).toList()
+ assert [1, 2, 3] == from(Arrays.asList(1, 2, 3)).toList()
+ }
+
+ @Test
+ void testInnerJoin0() {
+ def nums1 = [1, 2, 3]
+ def nums2 = [1, 2, 3]
+ def result = from(nums1).innerJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[1, 1], [2, 2], [3, 3]] == result
+ }
+
+ @Test
+ void testInnerJoin1() {
+ def nums1 = [1, 2, 3]
+ def nums2 = [2, 3, 4]
+ def result = from(nums1).innerJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[2, 2], [3, 3]] == result
+ }
+
+ @Test
+ void testLeftJoin0() {
+ def nums1 = [1, 2, 3]
+ def nums2 = [1, 2, 3]
+ def result = from(nums1).leftJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[1, 1], [2, 2], [3, 3]] == result
+ }
+
+ @Test
+ void testRightJoin0() {
+ def nums2 = [1, 2, 3]
+ def nums1 = [1, 2, 3]
+ def result = from(nums1).rightJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[1, 1], [2, 2], [3, 3]] == result
+ }
+
+ @Test
+ void testLeftJoin1() {
+ def nums1 = [1, 2, 3]
+ def nums2 = [2, 3, 4]
+ def result = from(nums1).leftJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[1, null], [2, 2], [3, 3]] == result
+ }
+
+ @Test
+ void testRightJoin1() {
+ def nums2 = [1, 2, 3]
+ def nums1 = [2, 3, 4]
+ def result = from(nums1).rightJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[null, 1], [2, 2], [3, 3]] == result
+ }
+
+ @Test
+ void testLeftJoin2() {
+ def nums1 = [1, 2, 3, null]
+ def nums2 = [2, 3, 4]
+ def result = from(nums1).leftJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[1, null], [2, 2], [3, 3], [null, null]] == result
+ }
+
+ @Test
+ void testRightJoin2() {
+ def nums2 = [1, 2, 3, null]
+ def nums1 = [2, 3, 4]
+ def result = from(nums1).rightJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[null, 1], [2, 2], [3, 3], [null, null]] == result
+ }
+
+ @Test
+ void testLeftJoin3() {
+ def nums1 = [1, 2, 3, null]
+ def nums2 = [2, 3, 4, null]
+ def result = from(nums1).leftJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[1, null], [2, 2], [3, 3], [null, null]] == result
+ }
+
+ @Test
+ void testRightJoin3() {
+ def nums2 = [1, 2, 3, null]
+ def nums1 = [2, 3, 4, null]
+ def result = from(nums1).rightJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[null, 1], [2, 2], [3, 3], [null, null]] == result
+ }
+
+ @Test
+ void testLeftJoin4() {
+ def nums1 = [1, 2, 3]
+ def nums2 = [2, 3, 4, null]
+ def result = from(nums1).leftJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[1, null], [2, 2], [3, 3]] == result
+ }
+
+ @Test
+ void testRightJoin4() {
+ def nums2 = [1, 2, 3]
+ def nums1 = [2, 3, 4, null]
+ def result = from(nums1).rightJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[null, 1], [2, 2], [3, 3]] == result
+ }
+
+ @Test
+ void testLeftJoin5() {
+ def nums1 = [1, 2, 3, null, null]
+ def nums2 = [2, 3, 4]
+ def result = from(nums1).leftJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[1, null], [2, 2], [3, 3], [null, null], [null, null]] == result
+ }
+
+ @Test
+ void testRightJoin5() {
+ def nums2 = [1, 2, 3, null, null]
+ def nums1 = [2, 3, 4]
+ def result = from(nums1).rightJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[null, 1], [2, 2], [3, 3], [null, null], [null, null]] == result
+ }
+
+ @Test
+ void testLeftJoin6() {
+ def nums1 = [1, 2, 3, null, null]
+ def nums2 = [2, 3, 4, null]
+ def result = from(nums1).leftJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[1, null], [2, 2], [3, 3], [null, null], [null, null]] == result
+ }
+
+ @Test
+ void testRightJoin6() {
+ def nums2 = [1, 2, 3, null, null]
+ def nums1 = [2, 3, 4, null]
+ def result = from(nums1).rightJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[null, 1], [2, 2], [3, 3], [null, null], [null, null]] == result
+ }
+
+ @Test
+ void testLeftJoin7() {
+ def nums1 = [1, 2, 3, null, null]
+ def nums2 = [2, 3, 4, null, null]
+ def result = from(nums1).leftJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[1, null], [2, 2], [3, 3], [null, null], [null, null]] == result
+ }
+
+ @Test
+ void testRightJoin7() {
+ def nums2 = [1, 2, 3, null, null]
+ def nums1 = [2, 3, 4, null, null]
+ def result = from(nums1).rightJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[null, 1], [2, 2], [3, 3], [null, null], [null, null]] == result
+ }
+
+ @Test
+ void testLeftJoin8() {
+ def nums1 = [1, 2, 3, null]
+ def nums2 = [2, 3, 4, null, null]
+ def result = from(nums1).leftJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[1, null], [2, 2], [3, 3], [null, null]] == result
+ }
+
+ @Test
+ void testRightJoin8() {
+ def nums2 = [1, 2, 3, null]
+ def nums1 = [2, 3, 4, null, null]
+ def result = from(nums1).rightJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[null, 1], [2, 2], [3, 3], [null, null]] == result
+ }
+
+ @Test
+ void testLeftJoin9() {
+ def nums1 = [1, 2, 3]
+ def nums2 = [2, 3, 4, null, null]
+ def result = from(nums1).leftJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[1, null], [2, 2], [3, 3]] == result
+ }
+
+ @Test
+ void testRightJoin9() {
+ def nums2 = [1, 2, 3]
+ def nums1 = [2, 3, 4, null, null]
+ def result = from(nums1).rightJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[null, 1], [2, 2], [3, 3]] == result
+ }
+
+ @Test
+ void testFullJoin() {
+ def nums1 = [1, 2, 3]
+ def nums2 = [2, 3, 4]
+ def result = from(nums1).fullJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[1, null], [2, 2], [3, 3], [null, 4]] == result
+ }
+
+ @Test
+ void testCrossJoin() {
+ def nums1 = [1, 2, 3]
+ def nums2 = [3, 4, 5]
+ def result = from(nums1).crossJoin(from(nums2)).toList()
+ assert [[1, 3], [1, 4], [1, 5], [2, 3], [2, 4], [2, 5], [3, 3], [3, 4], [3, 5]] == result
+ }
+
+ @Test
+ void testWhere() {
+ def nums = [1, 2, 3, 4, 5]
+ def result = from(nums).where(e -> e > 3).toList()
+ assert [4, 5] == result
+ }
+
+ @Test
+ void testGroupBySelect0() {
+ def nums = [1, 2, 2, 3, 3, 4, 4, 5]
+ def result = from(nums).groupBy(e -> e).select(e -> Tuple.tuple(e.v1, e.v2.toList())).toList()
+ assert [[1, [1]], [2, [2, 2]], [3, [3, 3]], [4, [4, 4]], [5, [5]]] == result
+ }
+
+ @Test
+ void testGroupBySelect1() {
+ def nums = [1, 2, 2, 3, 3, 4, 4, 5]
+ def result = from(nums).groupBy(e -> e).select(e -> Tuple.tuple(e.v1, e.v2.count())).toList()
+ assert [[1, 1], [2, 2], [3, 2], [4, 2], [5, 1]] == result
+ }
+
+ @Test
+ void testGroupBySelect2() {
+ def nums = [1, 2, 2, 3, 3, 4, 4, 5]
+ def result =
+ from(nums).groupBy(e -> e)
+ .select(e ->
+ Tuple.tuple(
+ e.v1,
+ e.v2.count(),
+ e.v2.sum(n -> new BigDecimal(n))
+ )
+ ).toList()
+ assert [[1, 1, 1], [2, 2, 4], [3, 2, 6], [4, 2, 8], [5, 1, 5]] == result
+ }
+
+ @Test
+ @CompileDynamic
+ void testGroupBySelect3() {
+ def nums = [1, 2, 2, 3, 3, 4, 4, 5]
+ def result =
+ from(nums).groupBy(e -> e, (k, q) -> k > 2)
+ .select(e ->
+ Tuple.tuple(
+ e.v1,
+ e.v2.count(),
+ e.v2.sum(n -> new BigDecimal(n))
+ )
+ ).toList()
+ assert [[3, 2, 6], [4, 2, 8], [5, 1, 5]] == result
+ }
+
+ @Test
+ void testOrderBy() {
+ Person daniel = new Person('Daniel', 35)
+ Person peter = new Person('Peter', 10)
+ Person alice = new Person('Alice', 22)
+ Person john = new Person('John', 10)
+
+ def persons = [daniel, peter, alice, john]
+ def result = from(persons).orderBy(
+ new Queryable.Order<Person, Comparable>((Person e) -> e.age, true),
+ new Queryable.Order<Person, Comparable>((Person e) -> e.name, true)
+ ).toList()
+ assert [john, peter, alice, daniel] == result
+
+ result = from(persons).orderBy(
+ new Queryable.Order<Person, Comparable>((Person e) -> e.age, false),
+ new Queryable.Order<Person, Comparable>((Person e) -> e.name, true)
+ ).toList()
+ assert [daniel, alice, john, peter] == result
+
+ result = from(persons).orderBy(
+ new Queryable.Order<Person, Comparable>((Person e) -> e.age, true),
+ new Queryable.Order<Person, Comparable>((Person e) -> e.name, false)
+ ).toList()
+ assert [peter, john, alice, daniel] == result
+
+ result = from(persons).orderBy(
+ new Queryable.Order<Person, Comparable>((Person e) -> e.age, false),
+ new Queryable.Order<Person, Comparable>((Person e) -> e.name, false)
+ ).toList()
+ assert [daniel, alice, peter, john] == result
+ }
+
+ @Test
+ void testLimit() {
+ def nums = [1, 2, 3, 4, 5]
+ def result = from(nums).limit(1, 2).toList()
+ assert [2, 3] == result
+
+ result = from(nums).limit(2).toList()
+ assert [1, 2] == result
+ }
+
+ @Test
+ void testSelect() {
+ def nums = [1, 2, 3, 4, 5]
+ def result = from(nums).select(e -> e + 1).toList()
+ assert [2, 3, 4, 5, 6] == result
+ }
+
+ @Test
+ void testDistinct() {
+ def nums = [1, 2, 2, 3, 3, 2, 3, 4, 5, 5]
+ def result = from(nums).distinct().toList()
+ assert [1, 2, 3, 4, 5] == result
+ }
+
+ @Test
+ void testUnion() {
+ def nums1 = [1, 2, 3]
+ def nums2 = [2, 3, 4]
+ def result = from(nums1).union(from(nums2)).toList()
+ assert [1, 2, 3, 4] == result
+ }
+
+ @Test
+ void testUnionAll() {
+ def nums1 = [1, 2, 3]
+ def nums2 = [2, 3, 4]
+ def result = from(nums1).unionAll(from(nums2)).toList()
+ assert [1, 2, 3, 2, 3, 4] == result
+ }
+
+ @Test
+ void testIntersect() {
+ def nums1 = [1, 2, 2, 3]
+ def nums2 = [2, 3, 3, 4]
+ def result = from(nums1).intersect(from(nums2)).toList()
+ assert [2, 3] == result
+ }
+
+ @Test
+ void testMinus() {
+ def nums1 = [1, 1, 2, 3]
+ def nums2 = [2, 3, 4]
+ def result = from(nums1).minus(from(nums2)).toList()
+ assert [1] == result
+ }
+
+ @Test
+ void testFromWhereLimitSelect() {
+ def nums1 = [1, 2, 3, 4, 5]
+ def nums2 = [0, 1, 2, 3, 4, 5, 6]
+ def result =
+ from(nums1)
+ .innerJoin(from(nums2), (a, b) -> a == b)
+ .where(t -> t.v1 > 1)
+ .limit(1, 2)
+ .select(t -> t.v1 + 1)
+ .toList()
+ assert [4, 5] == result
+ }
+
+ @Test
+ void testStream() {
+ def nums = [1, 2, 3]
+ def result = from(nums).stream().collect(Collectors.toList())
+ assert nums == result
+ }
+
+ @ToString
+ @EqualsAndHashCode
+ static class Person {
+ String name
+ int age
+
+ Person(String name, int age) {
+ this.name = name
+ this.age = age
+ }
+ }
+}