You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by su...@apache.org on 2020/10/11 09:11:14 UTC
[groovy] 01/03: GROOVY-8258: [GEP] Create a LINQ-like DSL
This is an automated email from the ASF dual-hosted git repository.
sunlan pushed a commit to branch GROOVY-8258
in repository https://gitbox.apache.org/repos/asf/groovy.git
commit 12b2b884fa8192812559f44a53242fb906dafed1
Author: Daniel Sun <su...@apache.org>
AuthorDate: Sat Oct 10 19:21:50 2020 +0800
GROOVY-8258: [GEP] Create a LINQ-like DSL
---
settings.gradle | 1 +
.../codehaus/groovy/ast/tools/GeneralUtils.java | 9 +
subprojects/groovy-linq/build.gradle | 35 +
.../apache/groovy/linq/GinqGroovyMethods.groovy | 47 +
.../org/apache/groovy/linq/dsl/GinqAstBuilder.java | 211 +++
.../apache/groovy/linq/dsl/GinqSyntaxError.java | 43 +
.../org/apache/groovy/linq/dsl/GinqVisitor.java | 47 +
.../groovy/linq/dsl/SyntaxErrorReportable.java | 43 +
.../dsl/expression/AbstractGinqExpression.java | 38 +
.../linq/dsl/expression/DataSourceExpression.java | 70 +
.../linq/dsl/expression/FilterExpression.java | 42 +
.../groovy/linq/dsl/expression/FromExpression.java | 47 +
.../groovy/linq/dsl/expression/GinqExpression.java | 72 +
.../linq/dsl/expression/GroupExpression.java | 44 +
.../groovy/linq/dsl/expression/JoinExpression.java | 67 +
.../groovy/linq/dsl/expression/OnExpression.java | 38 +
.../linq/dsl/expression/OrderExpression.java | 44 +
.../linq/dsl/expression/SelectExpression.java | 51 +
.../linq/dsl/expression/WhereExpression.java | 38 +
.../linq/provider/collection/GinqAstWalker.groovy | 490 +++++++
.../linq/provider/collection/NamedTuple.groovy | 58 +
.../groovy/linq/provider/collection/Queryable.java | 135 ++
.../provider/collection/QueryableCollection.java | 263 ++++
.../org/apache/groovy/linq/GinqErrorTest.groovy | 82 ++
.../groovy/org/apache/groovy/linq/GinqTest.groovy | 1445 ++++++++++++++++++++
.../linq/provider/collection/NamedTupleTest.groovy | 31 +
.../collection/QueryableCollectionTest.groovy | 406 ++++++
27 files changed, 3897 insertions(+)
diff --git a/settings.gradle b/settings.gradle
index d4a4425..8855341 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -49,6 +49,7 @@ def subprojects = [
'groovy-jmx',
'groovy-json',
'groovy-jsr223',
+ 'groovy-linq',
'groovy-macro',
'groovy-macro-library',
'groovy-nio',
diff --git a/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java b/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java
index 6abbe61..15e4db6 100644
--- a/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java
+++ b/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java
@@ -43,6 +43,7 @@ import org.codehaus.groovy.ast.expr.ConstructorCallExpression;
import org.codehaus.groovy.ast.expr.DeclarationExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.FieldExpression;
+import org.codehaus.groovy.ast.expr.LambdaExpression;
import org.codehaus.groovy.ast.expr.ListExpression;
import org.codehaus.groovy.ast.expr.MapEntryExpression;
import org.codehaus.groovy.ast.expr.MapExpression;
@@ -251,6 +252,14 @@ public class GeneralUtils {
return closureX(Parameter.EMPTY_ARRAY, code);
}
+ public static LambdaExpression lambdaX(final Parameter[] params, final Statement code) {
+ return new LambdaExpression(params, code);
+ }
+
+ public static LambdaExpression lambdaX(final Statement code) {
+ return lambdaX(Parameter.EMPTY_ARRAY, code);
+ }
+
/**
* Builds a binary expression that compares two values.
*
diff --git a/subprojects/groovy-linq/build.gradle b/subprojects/groovy-linq/build.gradle
new file mode 100644
index 0000000..5227c38
--- /dev/null
+++ b/subprojects/groovy-linq/build.gradle
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+plugins {
+ id 'org.apache.groovy-library'
+}
+
+dependencies {
+ implementation rootProject
+ implementation project(':groovy-macro')
+ testImplementation project(':groovy-test')
+ testImplementation project(':groovy-json')
+}
+
+groovyLibrary {
+ withoutBinaryCompatibilityChecks()
+ moduleDescriptor {
+ extensionClasses = 'org.apache.groovy.linq.GinqGroovyMethods'
+ }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/GinqGroovyMethods.groovy b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/GinqGroovyMethods.groovy
new file mode 100644
index 0000000..59d46a3
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/GinqGroovyMethods.groovy
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq
+
+import groovy.transform.CompileStatic
+import org.apache.groovy.linq.dsl.GinqAstBuilder
+import org.apache.groovy.linq.dsl.expression.GinqExpression
+import org.apache.groovy.linq.provider.collection.GinqAstWalker
+import org.codehaus.groovy.ast.expr.ClosureExpression
+import org.codehaus.groovy.ast.expr.Expression
+import org.codehaus.groovy.ast.expr.MethodCallExpression
+import org.codehaus.groovy.ast.stmt.Statement
+import org.codehaus.groovy.macro.runtime.Macro
+import org.codehaus.groovy.macro.runtime.MacroContext
+
+@CompileStatic
+class GinqGroovyMethods {
+ @Macro
+ static Expression GINQ(MacroContext ctx, final ClosureExpression closureExpression) {
+ Statement code = closureExpression.getCode()
+
+ GinqAstBuilder ginqAstBuilder = new GinqAstBuilder(ctx.getSourceUnit())
+ code.visit(ginqAstBuilder)
+ GinqExpression ginqExpression = ginqAstBuilder.getGinqExpression()
+
+ GinqAstWalker ginqBuilder = new GinqAstWalker(ctx.getSourceUnit())
+ MethodCallExpression selectMethodCallExpression = ginqBuilder.visitGinqExpression(ginqExpression)
+
+ return selectMethodCallExpression
+ }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqAstBuilder.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqAstBuilder.java
new file mode 100644
index 0000000..c446bc6
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqAstBuilder.java
@@ -0,0 +1,211 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.dsl;
+
+import org.apache.groovy.linq.dsl.expression.AbstractGinqExpression;
+import org.apache.groovy.linq.dsl.expression.DataSourceExpression;
+import org.apache.groovy.linq.dsl.expression.FilterExpression;
+import org.apache.groovy.linq.dsl.expression.FromExpression;
+import org.apache.groovy.linq.dsl.expression.GinqExpression;
+import org.apache.groovy.linq.dsl.expression.GroupExpression;
+import org.apache.groovy.linq.dsl.expression.JoinExpression;
+import org.apache.groovy.linq.dsl.expression.OnExpression;
+import org.apache.groovy.linq.dsl.expression.OrderExpression;
+import org.apache.groovy.linq.dsl.expression.SelectExpression;
+import org.apache.groovy.linq.dsl.expression.WhereExpression;
+import org.codehaus.groovy.GroovyBugError;
+import org.codehaus.groovy.ast.CodeVisitorSupport;
+import org.codehaus.groovy.ast.expr.ArgumentListExpression;
+import org.codehaus.groovy.ast.expr.BinaryExpression;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.expr.MethodCallExpression;
+import org.codehaus.groovy.control.SourceUnit;
+import org.codehaus.groovy.syntax.Types;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+
+/**
+ * Build the AST for GINQ
+ *
+ * @since 4.0.0
+ */
+public class GinqAstBuilder extends CodeVisitorSupport implements SyntaxErrorReportable {
+ private Deque<GinqExpression> ginqExpressionStack = new ArrayDeque<>();
+ private GinqExpression latestGinqExpression;
+ private final SourceUnit sourceUnit;
+
+ public GinqAstBuilder(SourceUnit sourceUnit) {
+ this.sourceUnit = sourceUnit;
+ }
+
+ public GinqExpression getGinqExpression() {
+ return latestGinqExpression;
+ }
+
+ private void setLatestGinqExpressionClause(AbstractGinqExpression ginqExpressionClause) {
+ GinqExpression ginqExpression = ginqExpressionStack.peek();
+ ginqExpression.putNodeMetaData(__LATEST_GINQ_EXPRESSION_CLAUSE, ginqExpressionClause);
+ }
+
+ private AbstractGinqExpression getLatestGinqExpressionClause() {
+ GinqExpression ginqExpression = ginqExpressionStack.peek();
+ return ginqExpression.getNodeMetaData(__LATEST_GINQ_EXPRESSION_CLAUSE);
+ }
+
+ @Override
+ public void visitMethodCallExpression(MethodCallExpression call) {
+ super.visitMethodCallExpression(call);
+ final String methodName = call.getMethodAsString();
+
+ if ("from".equals(methodName)) {
+ ginqExpressionStack.push(new GinqExpression()); // store the result
+ }
+
+ GinqExpression currentGinqExpression = ginqExpressionStack.peek();
+ AbstractGinqExpression latestGinqExpressionClause = getLatestGinqExpressionClause();
+
+ if ("from".equals(methodName) || JoinExpression.isJoinExpression(methodName)) {
+ ArgumentListExpression arguments = (ArgumentListExpression) call.getArguments();
+ if (arguments.getExpressions().size() != 1) {
+ this.collectSyntaxError(
+ new GinqSyntaxError(
+ "Only 1 argument expected for `" + methodName + "`, e.g. `" + methodName + " n in nums`",
+ call.getLineNumber(), call.getColumnNumber()
+ )
+ );
+ }
+ final Expression expression = arguments.getExpression(0);
+ if (!(expression instanceof BinaryExpression
+ && ((BinaryExpression) expression).getOperation().getType() == Types.KEYWORD_IN)) {
+ this.collectSyntaxError(
+ new GinqSyntaxError(
+ "`in` is expected for `" + methodName + "`, e.g. `" + methodName + " n in nums`",
+ call.getLineNumber(), call.getColumnNumber()
+ )
+ );
+ }
+ BinaryExpression binaryExpression = (BinaryExpression) expression;
+ Expression aliasExpr = binaryExpression.getLeftExpression();
+ Expression dataSourceExpr;
+ if (null == latestGinqExpression) {
+ dataSourceExpr = binaryExpression.getRightExpression();
+ } else {
+ // use the nested linq expresion and clear it
+ dataSourceExpr = latestGinqExpression;
+ latestGinqExpression = null;
+ }
+
+ DataSourceExpression dataSourceExpression;
+ if ("from".equals(methodName)) {
+ dataSourceExpression = new FromExpression(aliasExpr, dataSourceExpr);
+ currentGinqExpression.setFromExpression((FromExpression) dataSourceExpression);
+ } else {
+ dataSourceExpression = new JoinExpression(methodName, aliasExpr, dataSourceExpr);
+ currentGinqExpression.addJoinExpression((JoinExpression) dataSourceExpression);
+ }
+ dataSourceExpression.setSourcePosition(call);
+ setLatestGinqExpressionClause(dataSourceExpression);
+
+ return;
+ }
+
+ if ("where".equals(methodName) || "on".equals(methodName)) {
+ Expression filterExpr = ((ArgumentListExpression) call.getArguments()).getExpression(0);
+
+ if (filterExpr instanceof BinaryExpression && ((BinaryExpression) filterExpr).getOperation().getType() == Types.KEYWORD_IN) {
+ if (null != latestGinqExpression) {
+ // use the nested ginq and clear it
+ ((BinaryExpression) filterExpr).setRightExpression(latestGinqExpression);
+ latestGinqExpression = null;
+ }
+ }
+
+ FilterExpression filterExpression = null;
+ if ("where".equals(methodName)) {
+ filterExpression = new WhereExpression(filterExpr);
+ } else {
+ filterExpression = new OnExpression(filterExpr);
+ }
+
+ if (null == filterExpression) {
+ throw new GroovyBugError("Unknown method: " + methodName);
+ }
+
+ filterExpression.setSourcePosition(call);
+
+ if (latestGinqExpressionClause instanceof DataSourceExpression) {
+ if (filterExpression instanceof WhereExpression) {
+ ((DataSourceExpression) latestGinqExpressionClause).setWhereExpression((WhereExpression) filterExpression);
+ } else {
+ ((JoinExpression) latestGinqExpressionClause).setOnExpression((OnExpression) filterExpression);
+ }
+ } else {
+ throw new GroovyBugError("The preceding expression is not a DataSourceExpression: " + latestGinqExpressionClause);
+ }
+
+ return;
+ }
+
+ if ("groupby".equals(methodName)) {
+ GroupExpression groupExpression = new GroupExpression(call.getArguments());
+ groupExpression.setSourcePosition(call);
+
+ if (latestGinqExpressionClause instanceof DataSourceExpression) {
+ ((DataSourceExpression) latestGinqExpressionClause).setGroupExpression(groupExpression);
+ } else {
+ throw new GroovyBugError("The preceding expression is not a DataSourceExpression: " + latestGinqExpressionClause);
+ }
+
+ return;
+ }
+
+ if ("orderby".equals(methodName)) {
+ OrderExpression orderExpression = new OrderExpression(call.getArguments());
+ orderExpression.setSourcePosition(call);
+
+ if (latestGinqExpressionClause instanceof DataSourceExpression) {
+ ((DataSourceExpression) latestGinqExpressionClause).setOrderExpression(orderExpression);
+ } else {
+ throw new GroovyBugError("The preceding expression is not a DataSourceExpression: " + latestGinqExpressionClause);
+ }
+
+ return;
+ }
+
+ if ("select".equals(methodName)) {
+ SelectExpression selectExpression = new SelectExpression(call.getArguments());
+ selectExpression.setSourcePosition(call);
+
+ currentGinqExpression.setSelectExpression(selectExpression);
+ setLatestGinqExpressionClause(selectExpression);
+
+ latestGinqExpression = ginqExpressionStack.pop();
+
+ return;
+ }
+ }
+
+ @Override
+ public SourceUnit getSourceUnit() {
+ return sourceUnit;
+ }
+
+ private static final String __LATEST_GINQ_EXPRESSION_CLAUSE = "__latestGinqExpressionClause";
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqSyntaxError.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqSyntaxError.java
new file mode 100644
index 0000000..652d835
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqSyntaxError.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.dsl;
+
+/**
+ * Represents GINQ syntax error
+ *
+ * @since 4.0.0
+ */
+public class GinqSyntaxError extends AssertionError {
+ private final int line;
+ private final int column;
+
+ public GinqSyntaxError(String message, int line, int column) {
+ super(message, null);
+ this.line = line;
+ this.column = column;
+ }
+
+ public int getLine() {
+ return line;
+ }
+
+ public int getColumn() {
+ return column;
+ }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqVisitor.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqVisitor.java
new file mode 100644
index 0000000..8515b71
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqVisitor.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.dsl;
+
+import org.apache.groovy.linq.dsl.expression.AbstractGinqExpression;
+import org.apache.groovy.linq.dsl.expression.FromExpression;
+import org.apache.groovy.linq.dsl.expression.GinqExpression;
+import org.apache.groovy.linq.dsl.expression.GroupExpression;
+import org.apache.groovy.linq.dsl.expression.JoinExpression;
+import org.apache.groovy.linq.dsl.expression.OnExpression;
+import org.apache.groovy.linq.dsl.expression.OrderExpression;
+import org.apache.groovy.linq.dsl.expression.SelectExpression;
+import org.apache.groovy.linq.dsl.expression.WhereExpression;
+
+/**
+ * Represents the visitor for AST of GINQ
+ *
+ * @param <R> the type of visit result
+ * @since 4.0.0
+ */
+public interface GinqVisitor<R> {
+ R visitGinqExpression(GinqExpression ginqExpression);
+ R visitFromExpression(FromExpression fromExpression);
+ R visitJoinExpression(JoinExpression joinExpression);
+ R visitOnExpression(OnExpression onExpression);
+ R visitWhereExpression(WhereExpression whereExpression);
+ R visitGroupExpression(GroupExpression groupExpression);
+ R visitOrderExpression(OrderExpression orderExpression);
+ R visitSelectExpression(SelectExpression selectExpression);
+ R visit(AbstractGinqExpression expression);
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/SyntaxErrorReportable.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/SyntaxErrorReportable.java
new file mode 100644
index 0000000..5628451
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/SyntaxErrorReportable.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.dsl;
+
+import org.codehaus.groovy.control.SourceUnit;
+import org.codehaus.groovy.control.messages.SyntaxErrorMessage;
+import org.codehaus.groovy.syntax.SyntaxException;
+
+/**
+ * Supports reporting the syntax error of GINQ
+ *
+ * @since 4.0.0
+ */
+public interface SyntaxErrorReportable {
+ SourceUnit getSourceUnit();
+
+ default void collectSyntaxError(GinqSyntaxError ginqSyntaxError) {
+ SourceUnit sourceUnit = getSourceUnit();
+
+ SyntaxException e = new SyntaxException(
+ ginqSyntaxError.getMessage(),
+ ginqSyntaxError,
+ ginqSyntaxError.getLine(),
+ ginqSyntaxError.getColumn());
+ sourceUnit.getErrorCollector().addFatalError(new SyntaxErrorMessage(e, sourceUnit));
+ }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/AbstractGinqExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/AbstractGinqExpression.java
new file mode 100644
index 0000000..5633bab
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/AbstractGinqExpression.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.dsl.expression;
+
+import org.apache.groovy.linq.dsl.GinqVisitor;
+import org.codehaus.groovy.ast.NodeMetaDataHandler;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.expr.ExpressionTransformer;
+
+/**
+ * Represents GINQ expression which could hold meta data
+ *
+ * @since 4.0.0
+ */
+public abstract class AbstractGinqExpression extends Expression implements NodeMetaDataHandler {
+ @Override
+ public Expression transformExpression(ExpressionTransformer transformer) {
+ throw new UnsupportedOperationException("transform GINQ expression is not supported yet");
+ }
+
+ public abstract <R> R accept(GinqVisitor<R> visitor);
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/DataSourceExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/DataSourceExpression.java
new file mode 100644
index 0000000..88f27f7
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/DataSourceExpression.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.dsl.expression;
+
+import org.codehaus.groovy.ast.expr.Expression;
+
+/**
+ * Represents data source expression
+ *
+ * @since 4.0.0
+ */
+public abstract class DataSourceExpression extends AbstractGinqExpression {
+ protected Expression aliasExpr;
+ protected Expression dataSourceExpr;
+ protected WhereExpression whereExpression;
+ protected GroupExpression groupExpression;
+ protected OrderExpression orderExpression;
+
+ public DataSourceExpression(Expression aliasExpr, Expression dataSourceExpr) {
+ this.aliasExpr = aliasExpr;
+ this.dataSourceExpr = dataSourceExpr;
+ }
+
+ public Expression getAliasExpr() {
+ return aliasExpr;
+ }
+ public Expression getDataSourceExpr() {
+ return dataSourceExpr;
+ }
+
+ public void setWhereExpression(WhereExpression whereExpression) {
+ this.whereExpression = whereExpression;
+ }
+
+ public WhereExpression getWhereExpression() {
+ return whereExpression;
+ }
+
+ public GroupExpression getGroupExpression() {
+ return groupExpression;
+ }
+
+ public void setGroupExpression(GroupExpression groupExpression) {
+ this.groupExpression = groupExpression;
+ }
+
+ public OrderExpression getOrderExpression() {
+ return orderExpression;
+ }
+
+ public void setOrderExpression(OrderExpression orderExpression) {
+ this.orderExpression = orderExpression;
+ }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/FilterExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/FilterExpression.java
new file mode 100644
index 0000000..0fb4dde
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/FilterExpression.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.dsl.expression;
+
+import org.codehaus.groovy.ast.expr.Expression;
+
+/**
+ * Represents filter expression
+ *
+ * @since 4.0.0
+ */
+public abstract class FilterExpression extends AbstractGinqExpression {
+ protected Expression filterExpr;
+
+ public FilterExpression(Expression filterExpr) {
+ this.filterExpr = filterExpr;
+ }
+
+ public Expression getFilterExpr() {
+ return filterExpr;
+ }
+
+ public void setFilterExpr(Expression filterExpr) {
+ this.filterExpr = filterExpr;
+ }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/FromExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/FromExpression.java
new file mode 100644
index 0000000..42dde72
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/FromExpression.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.dsl.expression;
+
+import org.apache.groovy.linq.dsl.GinqVisitor;
+import org.codehaus.groovy.ast.expr.Expression;
+
+/**
+ * Represents the from expression
+ *
+ * @since 4.0.0
+ */
+public class FromExpression extends DataSourceExpression {
+ public FromExpression(Expression aliasExpr, Expression dataSourceExpr) {
+ super(aliasExpr, dataSourceExpr);
+ }
+
+ @Override
+ public <R> R accept(GinqVisitor<R> visitor) {
+ return visitor.visitFromExpression(this);
+ }
+
+ @Override
+ public String toString() {
+ return "FromExpression{" +
+ "aliasExpr=" + aliasExpr +
+ ", dataSourceExpr=" + dataSourceExpr +
+ ", whereExpression=" + whereExpression +
+ '}';
+ }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/GinqExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/GinqExpression.java
new file mode 100644
index 0000000..3ab9126
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/GinqExpression.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.dsl.expression;
+
+import org.apache.groovy.linq.dsl.GinqVisitor;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represent the root expression of GINQ
+ *
+ * @since 4.0.0
+ */
+public class GinqExpression extends AbstractGinqExpression {
+ private FromExpression fromExpression;
+ private final List<JoinExpression> joinExpressionList = new ArrayList<>();
+ private SelectExpression selectExpression;
+
+ @Override
+ public <R> R accept(GinqVisitor<R> visitor) {
+ return visitor.visitGinqExpression(this);
+ }
+
+ public FromExpression getFromExpression() {
+ return fromExpression;
+ }
+
+ public void setFromExpression(FromExpression fromExpression) {
+ this.fromExpression = fromExpression;
+ }
+
+ public List<JoinExpression> getJoinExpressionList() {
+ return joinExpressionList;
+ }
+
+ public void addJoinExpression(JoinExpression joinExpression) {
+ joinExpressionList.add(joinExpression);
+ }
+
+ public SelectExpression getSelectExpression() {
+ return selectExpression;
+ }
+
+ public void setSelectExpression(SelectExpression selectExpression) {
+ this.selectExpression = selectExpression;
+ }
+
+ @Override
+ public String toString() {
+ return "GinqExpression{" +
+ "fromExpression=" + fromExpression +
+ ", selectExpression=" + selectExpression +
+ '}';
+ }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/GroupExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/GroupExpression.java
new file mode 100644
index 0000000..d5d56c8
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/GroupExpression.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.dsl.expression;
+
+import org.apache.groovy.linq.dsl.GinqVisitor;
+import org.codehaus.groovy.ast.expr.Expression;
+
+/**
+ * Represents group by expression
+ *
+ * @since 4.0.0
+ */
+public class GroupExpression extends AbstractGinqExpression {
+ private final Expression classifierExpr;
+
+ public GroupExpression(Expression classifierExpr) {
+ this.classifierExpr = classifierExpr;
+ }
+
+ @Override
+ public <R> R accept(GinqVisitor<R> visitor) {
+ return visitor.visitGroupExpression(this);
+ }
+
+ public Expression getClassifierExpr() {
+ return classifierExpr;
+ }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/JoinExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/JoinExpression.java
new file mode 100644
index 0000000..4642afd
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/JoinExpression.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.dsl.expression;
+
+import org.apache.groovy.linq.dsl.GinqVisitor;
+import org.codehaus.groovy.ast.expr.Expression;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Represents join expression
+ *
+ * @since 4.0.0
+ */
+public class JoinExpression extends DataSourceExpression {
+ private static final String CROSS_JOIN = "crossjoin";
+ private static final List<String> JOIN_NAME_LIST = Arrays.asList("innerjoin", "leftjoin", "rightjoin", "fulljoin", CROSS_JOIN);
+ private final String joinName;
+ private OnExpression onExpression;
+
+ public JoinExpression(String joinName, Expression aliasExpr, Expression dataSourceExpr) {
+ super(aliasExpr, dataSourceExpr);
+ this.joinName = joinName;
+ }
+
+ public static boolean isJoinExpression(String methodName) {
+ return JOIN_NAME_LIST.contains(methodName);
+ }
+
+ public boolean isCrossJoin() {
+ return CROSS_JOIN.equals(joinName);
+ }
+
+ @Override
+ public <R> R accept(GinqVisitor<R> visitor) {
+ return visitor.visitJoinExpression(this);
+ }
+
+ public String getJoinName() {
+ return joinName;
+ }
+
+ public OnExpression getOnExpression() {
+ return onExpression;
+ }
+
+ public void setOnExpression(OnExpression onExpression) {
+ this.onExpression = onExpression;
+ }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/OnExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/OnExpression.java
new file mode 100644
index 0000000..98394d7
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/OnExpression.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.dsl.expression;
+
+import org.apache.groovy.linq.dsl.GinqVisitor;
+import org.codehaus.groovy.ast.expr.Expression;
+
+/**
+ * Represents on expression
+ *
+ * @since 4.0.0
+ */
+public class OnExpression extends FilterExpression {
+ public OnExpression(Expression filterExpr) {
+ super(filterExpr);
+ }
+
+ @Override
+ public <R> R accept(GinqVisitor<R> visitor) {
+ return visitor.visitOnExpression(this);
+ }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/OrderExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/OrderExpression.java
new file mode 100644
index 0000000..8acbaad
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/OrderExpression.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.dsl.expression;
+
+import org.apache.groovy.linq.dsl.GinqVisitor;
+import org.codehaus.groovy.ast.expr.Expression;
+
+/**
+ * Represents order by expression
+ *
+ * @since 4.0.0
+ */
+public class OrderExpression extends AbstractGinqExpression {
+ private final Expression ordersExpr;
+
+ public OrderExpression(Expression ordersExpr) {
+ this.ordersExpr = ordersExpr;
+ }
+
+ @Override
+ public <R> R accept(GinqVisitor<R> visitor) {
+ return visitor.visitOrderExpression(this);
+ }
+
+ public Expression getOrdersExpr() {
+ return ordersExpr;
+ }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/SelectExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/SelectExpression.java
new file mode 100644
index 0000000..82ec0c1
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/SelectExpression.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.dsl.expression;
+
+import org.apache.groovy.linq.dsl.GinqVisitor;
+import org.codehaus.groovy.ast.expr.Expression;
+
+/**
+ * Represents the select expression
+ *
+ * @since 4.0.0
+ */
+public class SelectExpression extends AbstractGinqExpression {
+ private final Expression projectionExpr;
+
+ public SelectExpression(Expression projectionExpr) {
+ this.projectionExpr = projectionExpr;
+ }
+
+ @Override
+ public <R> R accept(GinqVisitor<R> visitor) {
+ return visitor.visitSelectExpression(this);
+ }
+
+ public Expression getProjectionExpr() {
+ return projectionExpr;
+ }
+
+ @Override
+ public String toString() {
+ return "SelectExpression{" +
+ "projectionExpr=" + projectionExpr +
+ '}';
+ }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/WhereExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/WhereExpression.java
new file mode 100644
index 0000000..66bde4f
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/WhereExpression.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.dsl.expression;
+
+import org.apache.groovy.linq.dsl.GinqVisitor;
+import org.codehaus.groovy.ast.expr.Expression;
+
+/**
+ * Represent the where expression
+ *
+ * @since 4.0.0
+ */
+public class WhereExpression extends FilterExpression {
+ public WhereExpression(Expression filterExpr) {
+ super(filterExpr);
+ }
+
+ @Override
+ public <R> R accept(GinqVisitor<R> visitor) {
+ return visitor.visitWhereExpression(this);
+ }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/provider/collection/GinqAstWalker.groovy b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/provider/collection/GinqAstWalker.groovy
new file mode 100644
index 0000000..98315a8
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/provider/collection/GinqAstWalker.groovy
@@ -0,0 +1,490 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.provider.collection
+
+import groovy.transform.CompileDynamic
+import groovy.transform.CompileStatic
+import org.apache.groovy.linq.dsl.GinqSyntaxError
+import org.apache.groovy.linq.dsl.GinqVisitor
+import org.apache.groovy.linq.dsl.SyntaxErrorReportable
+import org.apache.groovy.linq.dsl.expression.AbstractGinqExpression
+import org.apache.groovy.linq.dsl.expression.DataSourceExpression
+import org.apache.groovy.linq.dsl.expression.FromExpression
+import org.apache.groovy.linq.dsl.expression.GinqExpression
+import org.apache.groovy.linq.dsl.expression.GroupExpression
+import org.apache.groovy.linq.dsl.expression.JoinExpression
+import org.apache.groovy.linq.dsl.expression.OnExpression
+import org.apache.groovy.linq.dsl.expression.OrderExpression
+import org.apache.groovy.linq.dsl.expression.SelectExpression
+import org.apache.groovy.linq.dsl.expression.WhereExpression
+import org.codehaus.groovy.GroovyBugError
+import org.codehaus.groovy.ast.ClassHelper
+import org.codehaus.groovy.ast.expr.ArgumentListExpression
+import org.codehaus.groovy.ast.expr.BinaryExpression
+import org.codehaus.groovy.ast.expr.CastExpression
+import org.codehaus.groovy.ast.expr.ClassExpression
+import org.codehaus.groovy.ast.expr.ConstantExpression
+import org.codehaus.groovy.ast.expr.ConstructorCallExpression
+import org.codehaus.groovy.ast.expr.EmptyExpression
+import org.codehaus.groovy.ast.expr.Expression
+import org.codehaus.groovy.ast.expr.ExpressionTransformer
+import org.codehaus.groovy.ast.expr.GStringExpression
+import org.codehaus.groovy.ast.expr.LambdaExpression
+import org.codehaus.groovy.ast.expr.ListExpression
+import org.codehaus.groovy.ast.expr.MethodCallExpression
+import org.codehaus.groovy.ast.expr.PropertyExpression
+import org.codehaus.groovy.ast.expr.TupleExpression
+import org.codehaus.groovy.ast.expr.VariableExpression
+import org.codehaus.groovy.control.SourceUnit
+import org.codehaus.groovy.syntax.Types
+
+import java.util.stream.Collectors
+
+import static org.codehaus.groovy.ast.tools.GeneralUtils.args
+import static org.codehaus.groovy.ast.tools.GeneralUtils.callX
+import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorX
+import static org.codehaus.groovy.ast.tools.GeneralUtils.lambdaX
+import static org.codehaus.groovy.ast.tools.GeneralUtils.param
+import static org.codehaus.groovy.ast.tools.GeneralUtils.params
+import static org.codehaus.groovy.ast.tools.GeneralUtils.propX
+import static org.codehaus.groovy.ast.tools.GeneralUtils.stmt
+
+/**
+ * Visit AST of GINQ and generate target method calls for GINQ
+ *
+ * @since 4.0.0
+ */
+@CompileStatic
+class GinqAstWalker implements GinqVisitor<Object>, SyntaxErrorReportable {
+ private final SourceUnit sourceUnit
+ private GinqExpression currentGinqExpression
+
+ GinqAstWalker(SourceUnit sourceUnit) {
+ this.sourceUnit = sourceUnit
+ }
+
+ @Override
+ MethodCallExpression visitGinqExpression(GinqExpression ginqExpression) {
+ this.currentGinqExpression = ginqExpression
+ FromExpression fromExpression = ginqExpression.getFromExpression()
+ MethodCallExpression fromMethodCallExpression = this.visitFromExpression(fromExpression)
+
+ MethodCallExpression selectMethodReceiver = fromMethodCallExpression
+
+ JoinExpression lastJoinExpression = null
+ MethodCallExpression lastJoinMethodCallExpression = null
+ for (JoinExpression joinExpression : ginqExpression.getJoinExpressionList()) {
+ joinExpression.putNodeMetaData(__METHOD_CALL_RECEIVER, lastJoinMethodCallExpression ?: fromMethodCallExpression)
+ joinExpression.putNodeMetaData(__DATA_SOURCE_EXPRESSION, lastJoinExpression ?: fromExpression)
+
+ lastJoinExpression = joinExpression
+ lastJoinMethodCallExpression = this.visitJoinExpression(lastJoinExpression)
+ }
+
+ if (lastJoinMethodCallExpression) {
+ selectMethodReceiver = lastJoinMethodCallExpression
+ }
+
+ SelectExpression selectExpression = ginqExpression.getSelectExpression()
+ selectExpression.putNodeMetaData(__METHOD_CALL_RECEIVER, selectMethodReceiver)
+ selectExpression.putNodeMetaData(__DATA_SOURCE_EXPRESSION, lastJoinExpression ?: fromExpression)
+
+ MethodCallExpression selectMethodCallExpression = this.visitSelectExpression(selectExpression)
+
+ return selectMethodCallExpression
+ }
+
+ @Override
+ MethodCallExpression visitFromExpression(FromExpression fromExpression) {
+ MethodCallExpression resultMethodCallExpression
+ MethodCallExpression fromMethodCallExpression = constructFromMethodCallExpression(fromExpression.dataSourceExpr)
+ resultMethodCallExpression = fromMethodCallExpression
+
+ WhereExpression whereExpression = fromExpression.whereExpression
+
+ return decorateDataSourceMethodCallExpression(resultMethodCallExpression, fromExpression, whereExpression)
+ }
+
+ private MethodCallExpression decorateDataSourceMethodCallExpression(MethodCallExpression dataSourceMethodCallExpression,
+ DataSourceExpression dataSourceExpression, WhereExpression whereExpression) {
+ if (whereExpression) {
+ whereExpression.putNodeMetaData(__DATA_SOURCE_EXPRESSION, dataSourceExpression)
+ whereExpression.putNodeMetaData(__METHOD_CALL_RECEIVER, dataSourceMethodCallExpression)
+
+ MethodCallExpression whereMethodCallExpression = visitWhereExpression(whereExpression)
+ dataSourceMethodCallExpression = whereMethodCallExpression
+ }
+
+ GroupExpression groupExpression = dataSourceExpression.groupExpression
+ if (groupExpression) {
+ groupExpression.putNodeMetaData(__DATA_SOURCE_EXPRESSION, dataSourceExpression)
+ groupExpression.putNodeMetaData(__METHOD_CALL_RECEIVER, dataSourceMethodCallExpression)
+
+ MethodCallExpression groupMethodCallExpression = visitGroupExpression(groupExpression)
+ dataSourceMethodCallExpression = groupMethodCallExpression
+ }
+
+ OrderExpression orderExpression = dataSourceExpression.orderExpression
+ if (orderExpression) {
+ orderExpression.putNodeMetaData(__DATA_SOURCE_EXPRESSION, dataSourceExpression)
+ orderExpression.putNodeMetaData(__METHOD_CALL_RECEIVER, dataSourceMethodCallExpression)
+
+ MethodCallExpression orderMethodCallExpression = visitOrderExpression(orderExpression)
+ dataSourceMethodCallExpression = orderMethodCallExpression
+ }
+
+ return dataSourceMethodCallExpression
+ }
+
+ @Override
+ MethodCallExpression visitJoinExpression(JoinExpression joinExpression) {
+ Expression receiver = joinExpression.getNodeMetaData(__METHOD_CALL_RECEIVER)
+ OnExpression onExpression = joinExpression.onExpression
+
+ if (!onExpression && !joinExpression.crossJoin) {
+ this.collectSyntaxError(
+ new GinqSyntaxError(
+ "`on` clause is expected for `" + joinExpression.joinName + "`",
+ joinExpression.getLineNumber(), joinExpression.getColumnNumber()
+ )
+ )
+ }
+
+ WhereExpression whereExpression = joinExpression.whereExpression
+
+ MethodCallExpression joinMethodCallExpression = constructJoinMethodCallExpression(receiver, joinExpression, onExpression, whereExpression)
+
+ return joinMethodCallExpression
+ }
+
+ @Override
+ MethodCallExpression visitOnExpression(OnExpression onExpression) {
+ return null // do nothing
+ }
+
+ @CompileDynamic
+ private MethodCallExpression constructFromMethodCallExpression(Expression dataSourceExpr) {
+ MethodCallExpression fromMethodCallExpression = macro {
+ $v{ makeQueryableCollectionClassExpression() }.from($v {
+ if (dataSourceExpr instanceof AbstractGinqExpression) {
+ return this.visit((AbstractGinqExpression) dataSourceExpr)
+ } else {
+ return dataSourceExpr
+ }
+ })
+ }
+
+ return fromMethodCallExpression
+ }
+
+ private MethodCallExpression constructJoinMethodCallExpression(
+ Expression receiver, JoinExpression joinExpression,
+ OnExpression onExpression, WhereExpression whereExpression) {
+
+ DataSourceExpression otherDataSourceExpression = joinExpression.getNodeMetaData(__DATA_SOURCE_EXPRESSION)
+ Expression otherAliasExpr = otherDataSourceExpression.aliasExpr
+
+ String otherParamName = otherAliasExpr.text
+ Expression filterExpr = EmptyExpression.INSTANCE
+ if (onExpression) {
+ filterExpr = onExpression.getFilterExpr()
+ Tuple2<String, Expression> paramNameAndLambdaCode = correctVariablesOfLambdaExpression(otherDataSourceExpression, filterExpr)
+ otherParamName = paramNameAndLambdaCode.v1
+ filterExpr = paramNameAndLambdaCode.v2
+ }
+
+ MethodCallExpression resultMethodCallExpression
+ MethodCallExpression joinMethodCallExpression = callX(receiver, joinExpression.joinName.replace('join', 'Join'),
+ args(
+ constructFromMethodCallExpression(joinExpression.dataSourceExpr),
+ null == onExpression ? EmptyExpression.INSTANCE : lambdaX(
+ params(
+ param(ClassHelper.DYNAMIC_TYPE, otherParamName),
+ param(ClassHelper.DYNAMIC_TYPE, joinExpression.aliasExpr.text)
+ ),
+ stmt(filterExpr)
+ )
+ )
+ )
+ resultMethodCallExpression = joinMethodCallExpression
+
+ if (joinExpression.crossJoin) {
+ // cross join does not need `on` clause
+ Expression lastArgumentExpression = ((ArgumentListExpression) joinMethodCallExpression.arguments).getExpressions().removeLast()
+ if (EmptyExpression.INSTANCE !== lastArgumentExpression) {
+ throw new GroovyBugError("Wrong argument removed")
+ }
+ }
+
+ return decorateDataSourceMethodCallExpression(resultMethodCallExpression, joinExpression, whereExpression)
+ }
+
+ @Override
+ MethodCallExpression visitWhereExpression(WhereExpression whereExpression) {
+ DataSourceExpression dataSourceExpression = whereExpression.getNodeMetaData(__DATA_SOURCE_EXPRESSION)
+ Expression fromMethodCallExpression = whereExpression.getNodeMetaData(__METHOD_CALL_RECEIVER)
+ Expression filterExpr = whereExpression.getFilterExpr()
+
+ if (filterExpr instanceof BinaryExpression && ((BinaryExpression) filterExpr).operation.type == Types.KEYWORD_IN) {
+ filterExpr = filterExpr.transformExpression(new ExpressionTransformer() {
+ @Override
+ Expression transform(Expression expression) {
+ if (expression instanceof AbstractGinqExpression) {
+ return callX((Expression) GinqAstWalker.this.visit((AbstractGinqExpression) expression), "toList")
+ }
+
+ return expression.transformExpression(this)
+ }
+ })
+ }
+
+ return callXWithLambda(fromMethodCallExpression, "where", dataSourceExpression, filterExpr)
+ }
+
+ @Override
+ MethodCallExpression visitGroupExpression(GroupExpression groupExpression) {
+ DataSourceExpression dataSourceExpression = groupExpression.getNodeMetaData(__DATA_SOURCE_EXPRESSION)
+ Expression groupMethodCallReceiver = groupExpression.getNodeMetaData(__METHOD_CALL_RECEIVER)
+ Expression classifierExpr = groupExpression.classifierExpr
+
+ List<Expression> argumentExpressionList = ((ArgumentListExpression) classifierExpr).getExpressions()
+ ConstructorCallExpression namedListCtorCallExpression = constructNamedListCtorCallExpression(argumentExpressionList)
+
+ MethodCallExpression groupMethodCallExpression = callXWithLambda(groupMethodCallReceiver, "groupBy2", dataSourceExpression, namedListCtorCallExpression)
+
+ this.currentGinqExpression.putNodeMetaData(__GROUP_BY, true)
+ return groupMethodCallExpression
+ }
+
+ @Override
+ MethodCallExpression visitOrderExpression(OrderExpression orderExpression) {
+ DataSourceExpression dataSourceExpression = orderExpression.getNodeMetaData(__DATA_SOURCE_EXPRESSION)
+ Expression orderMethodCallReceiver = orderExpression.getNodeMetaData(__METHOD_CALL_RECEIVER)
+ Expression ordersExpr = orderExpression.ordersExpr
+
+ List<Expression> argumentExpressionList = ((ArgumentListExpression) ordersExpr).getExpressions()
+ List<Expression> orderCtorCallExpressions = argumentExpressionList.stream().map(e -> {
+ Expression target = e
+ boolean asc = true
+ if (e instanceof BinaryExpression && e.operation.type == Types.KEYWORD_IN) {
+ target = e.leftExpression
+ asc = 'asc' == e.rightExpression.text
+ }
+
+ LambdaExpression lambdaExpression = constructLambdaExpression(dataSourceExpression, target)
+
+ return ctorX(ClassHelper.make(Queryable.Order.class), args(lambdaExpression, new ConstantExpression(asc)))
+ }).collect(Collectors.toList())
+
+ return callX(orderMethodCallReceiver, "orderBy", args(orderCtorCallExpressions))
+ }
+
+ @Override
+ MethodCallExpression visitSelectExpression(SelectExpression selectExpression) {
+ Expression selectMethodReceiver = selectExpression.getNodeMetaData(__METHOD_CALL_RECEIVER)
+ DataSourceExpression dataSourceExpression = selectExpression.getNodeMetaData(__DATA_SOURCE_EXPRESSION)
+ Expression projectionExpr = selectExpression.getProjectionExpr()
+
+ List<Expression> expressionList = ((TupleExpression) projectionExpr).getExpressions()
+ Expression lambdaCode
+ if (expressionList.size() > 1) {
+ ConstructorCallExpression namedListCtorCallExpression = constructNamedListCtorCallExpression(expressionList)
+ lambdaCode = namedListCtorCallExpression
+ } else {
+ lambdaCode = expressionList.get(0)
+ }
+
+ return callXWithLambda(selectMethodReceiver, "select", dataSourceExpression, lambdaCode)
+ }
+
+ private static ConstructorCallExpression constructNamedListCtorCallExpression(List<Expression> expressionList) {
+ int expressionListSize = expressionList.size()
+ List<Expression> elementExpressionList = new ArrayList<>(expressionListSize)
+ List<Expression> nameExpressionList = new ArrayList<>(expressionListSize)
+ for (Expression e : expressionList) {
+ Expression elementExpression = e
+ Expression nameExpression = new ConstantExpression(e.text)
+
+ if (e instanceof CastExpression) {
+ elementExpression = e.expression
+ nameExpression = new ConstantExpression(e.type.text)
+ } else if (e instanceof PropertyExpression) {
+ if (e.property instanceof ConstantExpression) {
+ elementExpression = e
+ nameExpression = new ConstantExpression(e.property.text)
+ } else if (e.property instanceof GStringExpression) {
+ elementExpression = e
+ nameExpression = e.property
+ }
+ }
+ elementExpressionList << elementExpression
+ nameExpressionList << nameExpression
+ }
+
+ ConstructorCallExpression namedListCtorCallExpression = ctorX(ClassHelper.make(NamedTuple.class), args(new ListExpression(elementExpressionList), new ListExpression(nameExpressionList)))
+ return namedListCtorCallExpression
+ }
+
+ private Expression correctVariablesOfGinqExpression(DataSourceExpression dataSourceExpression, Expression expr) {
+ boolean isGroup = isGroupByVisited()
+ boolean isJoin = dataSourceExpression instanceof JoinExpression
+
+ def correctVars = { Expression expression ->
+ if (expression instanceof VariableExpression) {
+ Expression transformedExpression = null
+
+ if (isGroup) { // groupby
+ // in #1, we will correct receiver of built-in aggregate functions
+ // the correct receiver is `__t.v2`, so we should not replace `__t` here
+ if (__T != expression.text) {
+ // replace `gk` in the groupby with `__t.v1.gk`, note: __t.v1 stores the group key
+ transformedExpression = propX(propX(new VariableExpression(__T), 'v1'), expression.text)
+ }
+ } else if (isJoin) {
+ /*
+ * `n1`(from node) join `n2` join `n3` will construct a join tree:
+ *
+ * __t (join node)
+ * |__ v2 (n3)
+ * |__ v1 (join node)
+ * |__ v2 (n2)
+ * |__ v1 (n1) (from node)
+ *
+ * Note: `__t` is a tuple with 2 elements
+ * so `n3`'s access path is `__t.v2`
+ * and `n2`'s access path is `__t.v1.v2`
+ * and `n1`'s access path is `__t.v1.v1`
+ *
+ * The following code shows how to construct the access path for variables
+ */
+ def prop = new VariableExpression(__T)
+ for (DataSourceExpression dse = dataSourceExpression;
+ null == transformedExpression && dse instanceof JoinExpression;
+ dse = dse.getNodeMetaData(__DATA_SOURCE_EXPRESSION)) {
+
+ DataSourceExpression otherDataSourceExpression = dse.getNodeMetaData(__DATA_SOURCE_EXPRESSION)
+ Expression firstAliasExpr = otherDataSourceExpression?.aliasExpr ?: EmptyExpression.INSTANCE
+ Expression secondAliasExpr = dse.aliasExpr
+
+ if (firstAliasExpr.text == expression.text && otherDataSourceExpression !instanceof JoinExpression) {
+ transformedExpression = propX(prop, 'v1')
+ } else if (secondAliasExpr.text == expression.text) {
+ transformedExpression = propX(prop, 'v2')
+ } else { // not found
+ prop = propX(prop, 'v1')
+ }
+ }
+ }
+ if (null != transformedExpression) {
+ return transformedExpression
+ }
+ } else if (expression instanceof MethodCallExpression) {
+ // #1
+ if (isGroup) { // groupby
+ if (expression.implicitThis) {
+ String methodName = expression.methodAsString
+ if ('count' == methodName && ((TupleExpression) expression.arguments).getExpressions().isEmpty()) {
+ expression.objectExpression = propX(new VariableExpression(__T), 'v2')
+ return expression
+ }
+ }
+ }
+ }
+
+ return expression
+ }
+
+ // The synthetic lambda parameter `__t` represents the element from the result datasource of joining, e.g. `n1` innerJoin `n2`
+ // The element from first datasource(`n1`) is referenced via `_t.v1`
+ // and the element from second datasource(`n2`) is referenced via `_t.v2`
+ expr = expr.transformExpression(new ExpressionTransformer() {
+ @Override
+ Expression transform(Expression expression) {
+ Expression transformedExpression = correctVars(expression)
+ if (transformedExpression !== expression) {
+ return transformedExpression
+ }
+
+ return expression.transformExpression(this)
+ }
+ })
+
+ return correctVars(expr)
+ }
+
+ @Override
+ Object visit(AbstractGinqExpression expression) {
+ return expression.accept(this)
+ }
+
+ private MethodCallExpression callXWithLambda(Expression receiver, String methodName, DataSourceExpression dataSourceExpression, Expression lambdaCode) {
+ LambdaExpression lambdaExpression = constructLambdaExpression(dataSourceExpression, lambdaCode)
+
+ callXWithLambda(receiver, methodName, lambdaExpression)
+ }
+
+ private LambdaExpression constructLambdaExpression(DataSourceExpression dataSourceExpression, Expression lambdaCode) {
+ Tuple2<String, Expression> paramNameAndLambdaCode = correctVariablesOfLambdaExpression(dataSourceExpression, lambdaCode)
+
+ lambdaX(
+ params(param(ClassHelper.DYNAMIC_TYPE, paramNameAndLambdaCode.v1)),
+ stmt(paramNameAndLambdaCode.v2)
+ )
+ }
+
+ private Tuple2<String, Expression> correctVariablesOfLambdaExpression(DataSourceExpression dataSourceExpression, Expression lambdaCode) {
+ boolean isGroup = isGroupByVisited()
+
+ String lambdaParamName
+ if (dataSourceExpression instanceof JoinExpression || isGroup) {
+ lambdaParamName = __T
+ lambdaCode = correctVariablesOfGinqExpression(dataSourceExpression, lambdaCode)
+ } else {
+ lambdaParamName = dataSourceExpression.aliasExpr.text
+ }
+
+ return Tuple.tuple(lambdaParamName, lambdaCode)
+ }
+
+ private boolean isGroupByVisited() {
+ return currentGinqExpression.getNodeMetaData(__GROUP_BY) ?: false
+ }
+
+ private static MethodCallExpression callXWithLambda(Expression receiver, String methodName, LambdaExpression lambdaExpression) {
+ callX(
+ receiver,
+ methodName,
+ lambdaExpression
+ )
+ }
+
+ private static makeQueryableCollectionClassExpression() {
+ new ClassExpression(ClassHelper.make(Queryable.class))
+ }
+
+ @Override
+ SourceUnit getSourceUnit() {
+ sourceUnit
+ }
+
+ private static final String __DATA_SOURCE_EXPRESSION = "__dataSourceExpression"
+ private static final String __METHOD_CALL_RECEIVER = "__methodCallReceiver"
+ private static final String __T = "__t"
+ private static final String __GROUP_BY = "__GROUP_BY"
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/provider/collection/NamedTuple.groovy b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/provider/collection/NamedTuple.groovy
new file mode 100644
index 0000000..f0d5910
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/provider/collection/NamedTuple.groovy
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.provider.collection
+
+
+import groovy.transform.CompileStatic
+
+/**
+ * Immutable named list to represent list result of GINQ
+ *
+ * @since 4.0.0
+ */
+@CompileStatic
+class NamedTuple<E> extends Tuple<E> {
+ private final List<String> nameList
+
+ NamedTuple(List<E> elementList, List<String> nameList) {
+ super(elementList as E[])
+ this.nameList = nameList
+ }
+
+ E getAt(String name) {
+ final int index = nameList.indexOf(name)
+
+ if (-1 == index) {
+ throw new IndexOutOfBoundsException("Failed to find element with name: $name")
+ }
+
+ return get(index)
+ }
+
+ E get(String name) {
+ return getAt(name)
+ }
+
+ @Override
+ String toString() {
+ '(' + nameList.withIndex()
+ .collect((String n, int i) -> { "${n}:${this[i]}" })
+ .join(', ') + ')'
+ }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/provider/collection/Queryable.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/provider/collection/Queryable.java
new file mode 100644
index 0000000..39fa431
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/provider/collection/Queryable.java
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.provider.collection;
+
+import groovy.lang.Tuple2;
+import groovy.transform.Internal;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.BiPredicate;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+
+/**
+ * Represents the queryable objects, e.g. Java collections
+ *
+ * @param <T> the type of Queryable element
+ * @since 4.0.0
+ */
+@Internal
+public interface Queryable<T> {
+ static <T> Queryable<T> from(Iterable<T> sourceIterable) {
+ return new QueryableCollection<>(sourceIterable);
+ }
+
+ static <T> Queryable<T> from(Stream<? extends T> sourceStream) {
+ return new QueryableCollection<>(sourceStream);
+ }
+
+ <U> Queryable<Tuple2<T, U>> innerJoin(Queryable<? extends U> queryable, BiPredicate<? super T, ? super U> joiner);
+
+ <U> Queryable<Tuple2<T, U>> leftJoin(Queryable<? extends U> queryable, BiPredicate<? super T, ? super U> joiner);
+
+ <U> Queryable<Tuple2<T, U>> rightJoin(Queryable<? extends U> queryable, BiPredicate<? super T, ? super U> joiner);
+
+ default <U> Queryable<Tuple2<T, U>> fullJoin(Queryable<? extends U> queryable, BiPredicate<? super T, ? super U> joiner) {
+ Queryable<Tuple2<T, U>> lj = this.leftJoin(queryable, joiner);
+ Queryable<Tuple2<T, U>> rj = this.rightJoin(queryable, joiner);
+ return lj.union(rj);
+ }
+
+ <U> Queryable<Tuple2<T, U>> crossJoin(Queryable<? extends U> queryable);
+
+ Queryable<T> where(Predicate<? super T> filter);
+
+ <K> Queryable<Tuple2<K, Queryable<T>>> groupBy(Function<? super T, ? extends K> classifier, BiPredicate<? super K, ? super Queryable<? extends T>> having);
+
+ default <K> Queryable<Tuple2<K, Queryable<T>>> groupBy(Function<? super T, ? extends K> classifier) {
+ return groupBy(classifier, (k, q) -> true);
+ }
+
+ <U extends Comparable<? super U>> Queryable<T> orderBy(Order<? super T, ? extends U>... orders);
+
+ Queryable<T> limit(int offset, int size);
+
+ default Queryable<T> limit(int size) {
+ return limit(0, size);
+ }
+
+ <U> Queryable<U> select(Function<? super T, ? extends U> mapper);
+
+ Queryable<T> distinct();
+
+ default Queryable<T> union(Queryable<? extends T> queryable) {
+ return this.unionAll(queryable).distinct();
+ }
+
+ Queryable<T> unionAll(Queryable<? extends T> queryable);
+
+ Queryable<T> intersect(Queryable<? extends T> queryable);
+
+ Queryable<T> minus(Queryable<? extends T> queryable);
+
+ List<T> toList();
+
+ default Stream<T> stream() {
+ return toList().stream();
+ }
+
+ // Built-in aggregate functions {
+ int count();
+ BigDecimal sum(Function<? super T, BigDecimal> mapper);
+ <R> R agg(Function<? super Queryable<? extends T>, ? extends R> mapper);
+ // } Built-in aggregate functions
+
+ class Order<T, U extends Comparable<? super U>> {
+ private final Function<? super T, ? extends U> keyExtractor;
+ private final boolean asc;
+
+ public Order(Function<? super T, ? extends U> keyExtractor, boolean asc) {
+ this.keyExtractor = keyExtractor;
+ this.asc = asc;
+ }
+
+ public Function<? super T, ? extends U> getKeyExtractor() {
+ return keyExtractor;
+ }
+
+ public boolean isAsc() {
+ return asc;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Order)) return false;
+ Order<?, ?> order = (Order<?, ?>) o;
+ return asc == order.asc &&
+ keyExtractor.equals(order.keyExtractor);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(keyExtractor, asc);
+ }
+ }
+}
diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/provider/collection/QueryableCollection.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/provider/collection/QueryableCollection.java
new file mode 100644
index 0000000..1e55141
--- /dev/null
+++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/provider/collection/QueryableCollection.java
@@ -0,0 +1,263 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.provider.collection;
+
+import groovy.lang.Tuple;
+import groovy.lang.Tuple2;
+import groovy.transform.Internal;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.BiPredicate;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+import static org.apache.groovy.linq.provider.collection.Queryable.from;
+
+/**
+ * Represents the queryable collections
+ *
+ * @param <T> the type of Queryable element
+ * @since 4.0.0
+ */
+@Internal
+class QueryableCollection<T> implements Queryable<T>, Iterable<T> {
+ private final Iterable<T> sourceIterable;
+
+ QueryableCollection(Iterable<T> sourceIterable) {
+ if (sourceIterable instanceof QueryableCollection) {
+ QueryableCollection<T> queryableCollection = (QueryableCollection<T>) sourceIterable;
+ this.sourceIterable = queryableCollection.sourceIterable;
+ } else {
+ this.sourceIterable = sourceIterable;
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ QueryableCollection(Stream<? extends T> sourceStream) {
+ this((Iterable<T>) toIterable(sourceStream));
+ }
+
+ @Override
+ public Iterator<T> iterator() {
+ return sourceIterable.iterator();
+ }
+
+ @Override
+ public <U> Queryable<Tuple2<T, U>> innerJoin(Queryable<? extends U> queryable, BiPredicate<? super T, ? super U> joiner) {
+ Stream<Tuple2<T, U>> stream =
+ this.stream()
+ .flatMap(p ->
+ queryable.stream()
+ .filter(c -> joiner.test(p, c))
+ .map(c -> Tuple.tuple(p, c)));
+
+ return from(stream);
+ }
+
+ @Override
+ public <U> Queryable<Tuple2<T, U>> leftJoin(Queryable<? extends U> queryable, BiPredicate<? super T, ? super U> joiner) {
+ return outerJoin(this, queryable, joiner);
+ }
+
+ @Override
+ public <U> Queryable<Tuple2<T, U>> rightJoin(Queryable<? extends U> queryable, BiPredicate<? super T, ? super U> joiner) {
+ return outerJoin(queryable, this, (a, b) -> joiner.test(b, a)).select(e -> Tuple.tuple(e.getV2(), e.getV1()));
+ }
+
+ @Override
+ public <U> Queryable<Tuple2<T, U>> crossJoin(Queryable<? extends U> queryable) {
+ Stream<Tuple2<T, U>> stream =
+ this.stream()
+ .flatMap(p ->
+ queryable.stream()
+ .map(c -> Tuple.tuple(p, c)));
+
+ return from(stream);
+ }
+
+ @Override
+ public Queryable<T> where(Predicate<? super T> filter) {
+ Stream<T> stream = this.stream().filter(filter::test);
+
+ return from(stream);
+ }
+
+ @Override
+ public <K> Queryable<Tuple2<K, Queryable<T>>> groupBy(Function<? super T, ? extends K> classifier, BiPredicate<? super K, ? super Queryable<? extends T>> having) {
+ Stream<Tuple2<K, Queryable<T>>> stream =
+ this.stream()
+ .collect(Collectors.groupingBy(classifier, Collectors.toList()))
+ .entrySet().stream()
+ .filter(m -> having.test(m.getKey(), from(m.getValue())))
+ .map(m -> Tuple.tuple(m.getKey(), from(m.getValue())));
+
+ return from(stream);
+ }
+
+ /**
+ * Same to {@link #groupBy(Function, BiPredicate)}, workaround for the conflicts with DGM
+ */
+ public <K> Queryable<Tuple2<K, Queryable<T>>> groupBy2(Function<? super T, ? extends K> classifier, BiPredicate<? super K, ? super Queryable<? extends T>> having) {
+ return this.groupBy(classifier, having);
+ }
+
+ /**
+ * Same to {@link #groupBy(Function)}, workaround for the conflicts with DGM
+ */
+ public <K> Queryable<Tuple2<K, Queryable<T>>> groupBy2(Function<? super T, ? extends K> classifier) {
+ return this.groupBy(classifier);
+ }
+
+ @Override
+ public <U extends Comparable<? super U>> Queryable<T> orderBy(Order<? super T, ? extends U>... orders) {
+ Comparator<T> comparator = null;
+ for (int i = 0, n = orders.length; i < n; i++) {
+ Order<? super T, ? extends U> order = orders[i];
+ Comparator<U> ascOrDesc = order.isAsc() ? Comparator.naturalOrder() : Comparator.reverseOrder();
+ comparator =
+ 0 == i
+ ? Comparator.comparing(order.getKeyExtractor(), ascOrDesc)
+ : comparator.thenComparing(order.getKeyExtractor(), ascOrDesc);
+ }
+
+ if (null == comparator) {
+ return this;
+ }
+
+ return from(this.stream().sorted(comparator));
+ }
+
+ @Override
+ public Queryable<T> limit(int offset, int size) {
+ Stream<T> stream = this.stream().skip(offset).limit(size);
+
+ return from(stream);
+ }
+
+ @Override
+ public <U> Queryable<U> select(Function<? super T, ? extends U> mapper) {
+ Stream<U> stream = this.stream().map(mapper);
+
+ return from(stream);
+ }
+
+ @Override
+ public Queryable<T> distinct() {
+ Stream<? extends T> stream = this.stream().distinct();
+
+ return from(stream);
+ }
+
+ @Override
+ public Queryable<T> unionAll(Queryable<? extends T> queryable) {
+ Stream<T> stream = Stream.concat(this.stream(), queryable.stream());
+
+ return from(stream);
+ }
+
+ @Override
+ public Queryable<T> intersect(Queryable<? extends T> queryable) {
+ Stream<T> stream = this.stream().filter(a -> queryable.stream().anyMatch(b -> b.equals(a))).distinct();
+
+ return from(stream);
+ }
+
+ @Override
+ public Queryable<T> minus(Queryable<? extends T> queryable) {
+ Stream<T> stream = this.stream().filter(a -> queryable.stream().noneMatch(b -> b.equals(a))).distinct();
+
+ return from(stream);
+ }
+
+ @Override
+ public List<T> toList() {
+ return stream().collect(Collectors.toList());
+ }
+
+ @Override
+ public Stream<T> stream() {
+ return toStream(sourceIterable); // we have to create new stream every time because Java stream can not be reused
+ }
+
+ @Override
+ public int count() {
+ return agg(q -> q.toList().size());
+ }
+
+ @Override
+ public BigDecimal sum(Function<? super T, BigDecimal> mapper) {
+ return agg(q -> this.stream().map(mapper).reduce(BigDecimal.ZERO, BigDecimal::add));
+ }
+
+ @Override
+ public <R> R agg(Function<? super Queryable<? extends T>, ? extends R> mapper) {
+ return mapper.apply(this);
+ }
+
+ private static <T, U> Queryable<Tuple2<T, U>> outerJoin(Queryable<? extends T> queryable1, Queryable<? extends U> queryable2, BiPredicate<? super T, ? super U> joiner) {
+ Stream<Tuple2<T, U>> stream =
+ queryable1.stream()
+ .flatMap(p ->
+ queryable2.stream()
+ .map(c -> joiner.test(p, c) ? c : null)
+ .reduce(new ArrayList<U>(), (r, e) -> {
+ int size = r.size();
+ if (0 == size) {
+ r.add(e);
+ return r;
+ }
+
+ int lastIndex = size - 1;
+ Object lastElement = r.get(lastIndex);
+
+ if (null != e) {
+ if (null == lastElement) {
+ r.set(lastIndex, e);
+ } else {
+ r.add(e);
+ }
+ }
+
+ return r;
+ }, (i, o) -> o).stream()
+ .map(c -> null == c ? Tuple.tuple(p, null) : Tuple.tuple(p, c)));
+
+ return from(stream);
+ }
+
+ private static <T> Stream<T> toStream(Iterable<T> sourceIterable) {
+ return StreamSupport.stream(sourceIterable.spliterator(), false);
+ }
+
+ private static <T> Iterable<T> toIterable(Stream<T> sourceStream) {
+ return sourceStream.collect(Collectors.toList());
+ }
+
+ @Override
+ public String toString() {
+ return toList().toString();
+ }
+}
diff --git a/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/GinqErrorTest.groovy b/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/GinqErrorTest.groovy
new file mode 100644
index 0000000..8074604
--- /dev/null
+++ b/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/GinqErrorTest.groovy
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq
+
+import groovy.transform.CompileStatic
+import org.junit.Test
+
+import static groovy.test.GroovyAssert.shouldFail
+
+@CompileStatic
+class GinqErrorTest {
+ @Test
+ void "testGinq - from select - 1"() {
+ def err = shouldFail '''\
+ def numbers = [0, 1, 2]
+ GINQ {
+ from numbers
+ select n
+ }
+ '''
+
+ assert err.toString().contains('`in` is expected for `from`, e.g. `from n in nums` @ line 3, column 17.')
+ }
+
+ @Test
+ void "testGinq - from select - 2"() {
+ def err = shouldFail '''\
+ def numbers = [0, 1, 2]
+ GINQ {
+ from n as numbers
+ select n
+ }
+ '''
+
+ assert err.toString().contains('`in` is expected for `from`, e.g. `from n in nums` @ line 3, column 17.')
+ }
+
+ @Test
+ void "testGinq - from select - 3"() {
+ def err = shouldFail '''\
+ def numbers = [0, 1, 2]
+ GINQ {
+ from n, numbers
+ select n
+ }
+ '''
+
+ assert err.toString().contains('Only 1 argument expected for `from`, e.g. `from n in nums` @ line 3, column 17.')
+ }
+
+ @Test
+ void "testGinq - from innerjoin select - 1"() {
+ def err = shouldFail '''\
+ def nums1 = [1, 2, 3]
+ def nums2 = [1, 2, 3]
+ assert [[1, 1], [2, 2], [3, 3]] == GINQ {
+ from n1 in nums1
+ innerjoin n2 in nums2
+ select n1, n2
+ }.toList()
+ '''
+
+ assert err.toString().contains('`on` clause is expected for `innerjoin` @ line 5, column 17.')
+ }
+
+}
diff --git a/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/GinqTest.groovy b/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/GinqTest.groovy
new file mode 100644
index 0000000..f1e654b
--- /dev/null
+++ b/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/GinqTest.groovy
@@ -0,0 +1,1445 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq
+
+import groovy.json.JsonSlurper
+import groovy.transform.CompileDynamic
+import groovy.transform.CompileStatic
+import org.junit.Test
+
+import static groovy.test.GroovyAssert.assertScript
+
+@CompileStatic
+class GinqTest {
+ @Test
+ void "testGinq - from select - 0"() {
+ assertScript '''
+ assert [0, 1, 2] == GINQ {
+ from n in [0, 1, 2]
+ select n
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from select - 1"() {
+ assertScript '''
+ def numbers = [0, 1, 2]
+ assert [0, 1, 2] == GINQ {
+ from n in numbers
+ select n
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from select - 2"() {
+ assertScript '''
+ def numbers = [0, 1, 2]
+ assert [0, 2, 4] == GINQ {
+ from n in numbers
+ select n * 2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from select - 3"() {
+ assertScript '''
+ class Person {
+ String name
+ int age
+
+ Person(String name, int age) {
+ this.name = name
+ this.age = age
+ }
+ }
+
+ def persons = [new Person('Daniel', 35), new Person('Linda', 21), new Person('Peter', 30)]
+ assert [35, 21, 30] == GINQ {
+ from p in persons
+ select p.age
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from select - 4"() {
+ assertScript '''
+ class Person {
+ String name
+ int age
+
+ Person(String name, int age) {
+ this.name = name
+ this.age = age
+ }
+ }
+
+ def persons = [new Person('Daniel', 35), new Person('Linda', 21), new Person('Peter', 30)]
+ assert [['Daniel', 35], ['Linda', 21], ['Peter', 30]] == GINQ {
+ from p in persons
+ select p.name, p.age
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from select - 5"() {
+ assertScript '''
+ class Person {
+ String name
+ int age
+
+ Person(String name, int age) {
+ this.name = name
+ this.age = age
+ }
+ }
+
+ def persons = [new Person('Daniel', 35), new Person('Linda', 21), new Person('Peter', 30)]
+ assert [[name:'Daniel', age:35], [name:'Linda', age:21], [name:'Peter', age:30]] == GINQ {
+ from p in persons
+ select (name: p.name, age: p.age)
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from select - 6"() {
+ assertScript '''
+ def numbers = [0, 1, 2]
+ assert [0, 1, 2] == GINQ {
+ from n in numbers select n
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from where select - 1"() {
+ assertScript '''
+ def numbers = [0, 1, 2, 3, 4, 5]
+ assert [2, 4, 6] == GINQ {
+ from n in numbers
+ where n > 0 && n <= 3
+ select n * 2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from where select - 2"() {
+ assertScript '''
+ def numbers = [0, 1, 2, 3, 4, 5]
+ assert [2, 4, 6] == GINQ {
+ from n in numbers where n > 0 && n <= 3 select n * 2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerjoin select - 1"() {
+ assertScript '''
+ def nums1 = [1, 2, 3]
+ def nums2 = [1, 2, 3]
+ assert [[1, 1], [2, 2], [3, 3]] == GINQ {
+ from n1 in nums1
+ innerjoin n2 in nums2 on n1 == n2
+ select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerjoin select - 2"() {
+ assertScript '''
+ def nums1 = [1, 2, 3]
+ def nums2 = [1, 2, 3]
+ assert [[2, 1], [3, 2], [4, 3]] == GINQ {
+ from n1 in nums1
+ innerjoin n2 in nums2 on n1 == n2
+ select n1 + 1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerjoin select - 3"() {
+ assertScript '''
+ def nums1 = [1, 2, 3]
+ def nums2 = [1, 2, 3]
+ assert [[1, 2], [2, 3], [3, 4]] == GINQ {
+ from n1 in nums1
+ innerjoin n2 in nums2 on n1 == n2
+ select n1, n2 + 1
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerjoin select - 4"() {
+ assertScript '''
+ def nums1 = [1, 2, 3]
+ def nums2 = [1, 2, 3]
+ assert [[1, 2], [2, 3]] == GINQ {
+ from n1 in nums1
+ innerjoin n2 in nums2 on n1 + 1 == n2
+ select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerjoin select - 5"() {
+ assertScript '''
+ def nums1 = [1, 2, 3]
+ def nums2 = [1, 2, 3]
+ assert [[1, 2], [2, 3]] == GINQ {
+ from n1 in nums1 innerjoin n2 in nums2 on n1 + 1 == n2 select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerjoin select - 6"() {
+ assertScript '''
+ class Person {
+ String name
+ int age
+
+ Person(String name, int age) {
+ this.name = name
+ this.age = age
+ }
+ }
+
+ def persons1 = [new Person('Daniel', 35), new Person('Linda', 21), new Person('Peter', 30)]
+ def persons2 = [new Person('Jack', 35), new Person('Rose', 21), new Person('Smith', 30)]
+ assert [['Daniel', 'Jack'], ['Linda', 'Rose'], ['Peter', 'Smith']] == GINQ {
+ from p1 in persons1
+ innerjoin p2 in persons2 on p1.age == p2.age
+ select p1.name, p2.name
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerjoin select - 7"() {
+ assertScript '''
+ class Person {
+ String name
+ int age
+
+ Person(String name, int age) {
+ this.name = name
+ this.age = age
+ }
+ }
+
+ def persons1 = [new Person('Daniel', 35), new Person('Linda', 21), new Person('Peter', 30)]
+ def persons2 = [new Person('Jack', 35), new Person('Rose', 21), new Person('Smith', 30)]
+ assert [['DANIEL', 'JACK'], ['LINDA', 'ROSE'], ['PETER', 'SMITH']] == GINQ {
+ from p1 in persons1
+ innerjoin p2 in persons2 on p1.age == p2.age
+ select p1.name.toUpperCase(), p2.name.toUpperCase()
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerjoin select - 8"() {
+ assertScript '''
+ class Person {
+ String name
+ int age
+
+ Person(String name, int age) {
+ this.name = name
+ this.age = age
+ }
+ }
+
+ def same(str) { str }
+
+ def persons1 = [new Person('Daniel', 35), new Person('Linda', 21), new Person('Peter', 30)]
+ def persons2 = [new Person('Jack', 35), new Person('Rose', 21), new Person('Smith', 30)]
+ assert [['DANIEL', 'JACK'], ['LINDA', 'ROSE'], ['PETER', 'SMITH']] == GINQ {
+ from p1 in persons1
+ innerjoin p2 in persons2 on p1.age == p2.age
+ select same(p1.name.toUpperCase()), same(p2.name.toUpperCase())
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerjoin select - 9"() {
+ assertScript '''
+ assert [1, 2, 3] == GINQ {
+ from n in [1, 2, 3]
+ innerjoin k in [2, 3, 4] on n + 1 == k
+ select n
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerjoin select - 10"() {
+ assertScript '''
+ assert [2, 3, 4] == GINQ {
+ from n in [1, 2, 3]
+ innerjoin k in [2, 3, 4] on n + 1 == k
+ select k
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerjoin select - 11"() {
+ assertScript '''
+ def nums1 = [1, 2, 3]
+ def nums2 = [2, 3, 4]
+ def nums3 = [3, 4, 5]
+ assert [[3, 3, 3]] == GINQ {
+ from n1 in nums1
+ innerjoin n2 in nums2 on n2 == n1
+ innerjoin n3 in nums3 on n3 == n2
+ select n1, n2, n3
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerjoin select - 12"() {
+ assertScript '''
+ def nums1 = [1, 2, 3]
+ def nums2 = [2, 3, 4]
+ def nums3 = [3, 4, 5]
+ assert [[3, 3, 3]] == GINQ {
+ from n1 in nums1
+ innerjoin n2 in nums2 on n2 == n1
+ innerjoin n3 in nums3 on n3 == n1
+ select n1, n2, n3
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerjoin select - 13"() {
+ assertScript '''
+ def nums1 = [1, 2, 3]
+ def nums2 = [2, 3, 4]
+ def nums3 = [3, 4, 5]
+ assert [[3, 3, 3]] == GINQ {
+ from v in (
+ from n1 in nums1
+ innerjoin n2 in nums2 on n1 == n2
+ select n1, n2
+ )
+ innerjoin n3 in nums3 on v.n2 == n3
+ select v.n1, v.n2, n3
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerjoin where select - 1"() {
+ assertScript '''
+ def nums1 = [1, 2, 3]
+ def nums2 = [1, 2, 3]
+ assert [[2, 2], [3, 3]] == GINQ {
+ from n1 in nums1
+ innerjoin n2 in nums2 on n1 == n2
+ where n1 > 1 && n2 <= 3
+ select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerjoin where select - 2"() {
+ assertScript '''
+ def nums1 = [1, 2, 3]
+ def nums2 = [1, 2, 3]
+ assert [[2, 2], [3, 3]] == GINQ {
+ from n1 in nums1
+ innerjoin n2 in nums2 on n1 == n2
+ where Math.pow(n1, 1) > 1 && Math.pow(n2, 1) <= 3
+ select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerjoin where select - 3"() {
+ assertScript '''
+ def nums1 = [1, 2, 3]
+ def nums2 = [1, 2, 3]
+ assert [[2, 2], [3, 3]] == GINQ {
+ from n1 in nums1 innerjoin n2 in nums2 on n1 == n2 where Math.pow(n1, 1) > 1 && Math.pow(n2, 1) <= 3 select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerjoin where select - 4"() {
+ assertScript '''
+ class Person {
+ String name
+ int age
+
+ Person(String name, int age) {
+ this.name = name
+ this.age = age
+ }
+ }
+
+ def persons1 = [new Person('Daniel', 35), new Person('Linda', 21), new Person('David', 30)]
+ def persons2 = [new Person('Jack', 35), new Person('Rose', 21), new Person('Smith', 30)]
+ assert [['Daniel', 'Jack']] == GINQ {
+ from p1 in persons1
+ innerjoin p2 in persons2 on p1.age == p2.age
+ where p1.name.startsWith('D') && p2.name.endsWith('k')
+ select p1.name, p2.name
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerjoin where select - 5"() {
+ assertScript '''
+ class Person {
+ String name
+ int age
+
+ Person(String name, int age) {
+ this.name = name
+ this.age = age
+ }
+ }
+
+ def same(obj) {obj}
+
+ def persons1 = [new Person('Daniel', 35), new Person('Linda', 21), new Person('David', 30)]
+ def persons2 = [new Person('Jack', 35), new Person('Rose', 21), new Person('Smith', 30)]
+ assert [['Daniel', 'Jack']] == GINQ {
+ from p1 in persons1
+ innerjoin p2 in persons2 on p1.age == p2.age
+ where same(p1.name.startsWith('D')) && same(p2.name.endsWith('k'))
+ select p1.name, p2.name
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - nested from - 0"() {
+ assertScript '''
+ assert [1, 2, 3] == GINQ {
+ from v in (
+ from n in [1, 2, 3]
+ select n
+ )
+ select v
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - nested from - 1"() {
+ assertScript '''
+ def numbers = [1, 2, 3]
+ assert [1, 2, 3] == GINQ {
+ from v in (
+ from n in numbers
+ select n
+ )
+ select v
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - nested from - 2"() {
+ assertScript '''
+ def numbers = [1, 2, 3]
+ assert [1, 2] == GINQ {
+ from v in (
+ from n in numbers
+ where n < 3
+ select n
+ )
+ select v
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - nested from - 3"() {
+ assertScript '''
+ def numbers = [1, 2, 3]
+ assert [2] == GINQ {
+ from v in (
+ from n in numbers
+ where n < 3
+ select n
+ )
+ where v > 1
+ select v
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - nested from - 4"() {
+ assertScript '''
+ def nums1 = [1, 2, 3, 4, 5]
+ def nums2 = [1, 2, 3, 4, 5]
+ assert [[3, 3], [5, 5]] == GINQ {
+ from v in (
+ from n1 in nums1
+ innerjoin n2 in nums2 on n1 == n2
+ where n1 > 1 && n2 <= 5
+ select n1, n2
+ )
+ where v.n1 >= 3 && v.n2 in [3, 5]
+ select v
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - nested from - 5"() {
+ assertScript '''
+ def nums1 = [1, 2, 3, 4, 5]
+ def nums2 = [1, 2, 3, 4, 5]
+ assert [[3, 3], [5, 5]] == GINQ {
+ from v in (
+ from n1 in nums1
+ innerjoin n2 in nums2 on n1 == n2
+ where n1 > 1 && n2 <= 5
+ select n1, n2
+ )
+ where v['n1'] >= 3 && v['n2'] in [3, 5]
+ select v
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - nested from - 6"() {
+ assertScript '''
+ def nums1 = [1, 2, 3, 4, 5]
+ def nums2 = [1, 2, 3, 4, 5]
+ assert [[3, 3], [5, 5]] == GINQ {
+ from v in (
+ from n1 in nums1
+ innerjoin n2 in nums2 on n1 == n2
+ where n1 > 1 && n2 <= 5
+ select n1, n2
+ )
+ where v[0] >= 3 && v[1] in [3, 5] // v[0] references column1 n1, and v[1] references column2 n2
+ select v
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - nested from - 7"() {
+ assertScript '''
+ def nums1 = [1, 2, 3, 4, 5]
+ def nums2 = [1, 2, 3, 4, 5]
+ assert [[3, 3], [5, 5]] == GINQ {
+ from v in (
+ from n1 in nums1
+ innerjoin n2 in nums2 on n1 == n2
+ where n1 > 1 && n2 <= 5
+ select n1 as vn1, n2 as vn2 // rename column names
+ )
+ where v.vn1 >= 3 && v.vn2 in [3, 5]
+ select v
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - nested from - 8"() {
+ assertScript '''
+ def nums1 = [1, 2, 3, 4, 5]
+ def nums2 = [1, 2, 3, 4, 5]
+ assert [[3, 3], [5, 5]] == GINQ {
+ from v in (
+ from n1 in nums1
+ innerjoin n2 in nums2 on n1 == n2
+ where n1 > 1 && n2 <= 5
+ select ((n1 as Integer) as vn1), ((n2 as Integer) as vn2)
+ )
+ where v.vn1 >= 3 && v.vn2 in [3, 5]
+ select v
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - nested from - 9"() {
+ assertScript '''
+ assert [2, 6] == GINQ {
+ from v in (
+ from n in (
+ from m in [1, 2, 3]
+ select m as v1, (m + 1) as v2
+ )
+ where n.v2 < 4
+ select n.v1 * n.v2
+ )
+ select v
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - nested from - 10"() {
+ assertScript '''
+ assert [2, 6] == GINQ {
+ from v in (
+ from n in (
+ from m in [1, 2, 3]
+ select m, (m + 1) as v2
+ )
+ where n.v2 < 4
+ select n.m * n.v2
+ )
+ select v
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - nested from - 11"() {
+ assertScript '''
+ assert [[1, 2], [2, 3]] == GINQ {
+ from v in (
+ from n in (
+ from m in [1, 2, 3]
+ select m, (m + 1) as v2
+ )
+ where n.v2 < 4
+ select n.m, n.v2 // its column names are: m, v2
+ )
+ select v.m, v.v2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - nested from - 12"() {
+ assertScript '''
+ assert [[1, 2], [2, 3]] == GINQ {
+ from v in (
+ from n in (
+ from m in [1, 2, 3]
+ select m, (m + 1) as v2
+ )
+ where n.v2 < 4
+ select n.m, n.v2
+ )
+ select v."${'m'}", v.v2 // dynamic column name
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - nested from - 13"() {
+ assertScript '''
+ assert [2, 6] == GINQ {
+ from v in (
+ from n in (
+ from m in [1, 2, 3]
+ select m as v1, (m + 1) as v2
+ )
+ innerjoin k in [2, 3, 4] on n.v2 == k
+ where n.v2 < 4
+ select n.v1 * k
+ )
+ select v
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - nested from - 14"() {
+ assertScript '''
+ assert [2, 3] == GINQ {
+ from n in [1, 2, 3]
+ innerjoin k in (
+ from m in [2, 3, 4]
+ select m
+ ) on n == k
+ select n
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - nested from - 15"() {
+ assertScript '''
+ assert [1, 2] == GINQ {
+ from n in [0, 1, 2]
+ where n in (
+ from m in [1, 2]
+ select m
+ )
+ select n
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - nested from - 16"() {
+ assertScript '''
+ assert [[2, 2]] == GINQ {
+ from t in [[0, 0], [1, 1], [2, 2]]
+ where t in (
+ from m in [1, 2]
+ innerjoin k in [2, 3] on k == m
+ select m, k
+ )
+ select t
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - nested from - 17"() {
+ assertScript '''
+ import static groovy.lang.Tuple.*
+
+ @groovy.transform.EqualsAndHashCode
+ class Person {
+ String firstName
+ String lastName
+ int age
+ String gender
+
+ Person(String firstName, String lastName, int age, String gender) {
+ this.firstName = firstName
+ this.lastName = lastName
+ this.age = age
+ this.gender = gender
+ }
+ }
+ @groovy.transform.EqualsAndHashCode
+ class LuckyInfo {
+ String lastName
+ String gender
+ boolean valid
+
+ LuckyInfo(String lastName, String gender, boolean valid) {
+ this.lastName = lastName
+ this.gender = gender
+ this.valid = valid
+ }
+ }
+
+ def persons = [new Person('Daniel', 'Sun', 35, 'Male'), new Person('Linda', 'Yang', 21, 'Female'),
+ new Person('Peter', 'Yang', 30, 'Male'), new Person('Rose', 'Yang', 30, 'Female')]
+ def luckyInfoList = [new LuckyInfo('Sun', 'Male', true), new LuckyInfo('Yang', 'Female', true),
+ new LuckyInfo('Yang', 'Male', false)]
+
+ assert ['Daniel', 'Linda', 'Rose'] == GINQ {
+ from p in persons
+ where tuple(p.lastName, p.gender) in (
+ from luckyInfo in luckyInfoList
+ where luckyInfo.valid == true
+ select luckyInfo.lastName, luckyInfo.gender
+ )
+ select p.firstName
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from leftjoin select - 1"() {
+ assertScript '''
+ def nums1 = [1, 2, 3]
+ def nums2 = [1, 2, 3]
+ assert [[1, 1], [2, 2], [3, 3]] == GINQ {
+ from n1 in nums1
+ leftjoin n2 in nums2 on n1 == n2
+ select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from leftjoin select - 2"() {
+ assertScript '''
+ def nums1 = [1, 2, 3]
+ def nums2 = [2, 3, 4]
+ assert [[1, null], [2, 2], [3, 3]] == GINQ {
+ from n1 in nums1
+ leftjoin n2 in nums2 on n1 == n2
+ select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from leftjoin select - 3"() {
+ assertScript '''
+ def nums1 = [1, 2, 3, null]
+ def nums2 = [2, 3, 4]
+ assert [[1, null], [2, 2], [3, 3], [null, null]] == GINQ {
+ from n1 in nums1
+ leftjoin n2 in nums2 on n1 == n2
+ select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from leftjoin select - 4"() {
+ assertScript '''
+ def nums1 = [1, 2, 3, null]
+ def nums2 = [2, 3, 4, null]
+ assert [[1, null], [2, 2], [3, 3], [null, null]] == GINQ {
+ from n1 in nums1
+ leftjoin n2 in nums2 on n1 == n2
+ select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from leftjoin select - 5"() {
+ assertScript '''
+ def nums1 = [1, 2, 3]
+ def nums2 = [2, 3, 4, null]
+ assert [[1, null], [2, 2], [3, 3]] == GINQ {
+ from n1 in nums1
+ leftjoin n2 in nums2 on n1 == n2
+ select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from leftjoin select - 6"() {
+ assertScript '''
+ def nums1 = [1, 2, 3, null, null]
+ def nums2 = [2, 3, 4]
+ assert [[1, null], [2, 2], [3, 3], [null, null], [null, null]] == GINQ {
+ from n1 in nums1
+ leftjoin n2 in nums2 on n1 == n2
+ select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from leftjoin select - 7"() {
+ assertScript '''
+ def nums1 = [1, 2, 3, null, null]
+ def nums2 = [2, 3, 4, null]
+ assert [[1, null], [2, 2], [3, 3], [null, null], [null, null]] == GINQ {
+ from n1 in nums1
+ leftjoin n2 in nums2 on n1 == n2
+ select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from leftjoin select - 8"() {
+ assertScript '''
+ def nums1 = [1, 2, 3, null, null]
+ def nums2 = [2, 3, 4, null, null]
+ assert [[1, null], [2, 2], [3, 3], [null, null], [null, null]] == GINQ {
+ from n1 in nums1
+ leftjoin n2 in nums2 on n1 == n2
+ select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from leftjoin select - 9"() {
+ assertScript '''
+ def nums1 = [1, 2, 3, null]
+ def nums2 = [2, 3, 4, null, null]
+ assert [[1, null], [2, 2], [3, 3], [null, null]] == GINQ {
+ from n1 in nums1
+ leftjoin n2 in nums2 on n1 == n2
+ select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from leftjoin select - 10"() {
+ assertScript '''
+ def nums1 = [1, 2, 3]
+ def nums2 = [2, 3, 4, null, null]
+ assert [[1, null], [2, 2], [3, 3]] == GINQ {
+ from n1 in nums1
+ leftjoin n2 in nums2 on n1 == n2
+ select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from leftjoin where select - 1"() {
+ assertScript '''
+ def nums1 = [1, 2, 3]
+ def nums2 = [2, 3, 4, null, null]
+ assert [[2, 2], [3, 3]] == GINQ {
+ from n1 in nums1
+ leftjoin n2 in nums2 on n1 == n2
+ where n2 != null
+ select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from rightjoin select - 1"() {
+ assertScript '''
+ def nums1 = [1, 2, 3]
+ def nums2 = [1, 2, 3]
+ assert [[1, 1], [2, 2], [3, 3]] == GINQ {
+ from n1 in nums1
+ rightjoin n2 in nums2 on n1 == n2
+ select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from rightjoin select - 2"() {
+ assertScript '''
+ def nums2 = [1, 2, 3]
+ def nums1 = [2, 3, 4]
+ assert [[null, 1], [2, 2], [3, 3]] == GINQ {
+ from n1 in nums1
+ rightjoin n2 in nums2 on n1 == n2
+ select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from rightjoin select - 3"() {
+ assertScript '''
+ def nums2 = [1, 2, 3, null]
+ def nums1 = [2, 3, 4]
+ assert [[null, 1], [2, 2], [3, 3], [null, null]] == GINQ {
+ from n1 in nums1
+ rightjoin n2 in nums2 on n1 == n2
+ select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from rightjoin select - 4"() {
+ assertScript '''
+ def nums2 = [1, 2, 3, null]
+ def nums1 = [2, 3, 4, null]
+ assert [[null, 1], [2, 2], [3, 3], [null, null]] == GINQ {
+ from n1 in nums1
+ rightjoin n2 in nums2 on n1 == n2
+ select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from rightjoin select - 5"() {
+ assertScript '''
+ def nums2 = [1, 2, 3]
+ def nums1 = [2, 3, 4, null]
+ assert [[null, 1], [2, 2], [3, 3]] == GINQ {
+ from n1 in nums1
+ rightjoin n2 in nums2 on n1 == n2
+ select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from rightjoin select - 6"() {
+ assertScript '''
+ def nums2 = [1, 2, 3, null, null]
+ def nums1 = [2, 3, 4]
+ assert [[null, 1], [2, 2], [3, 3], [null, null], [null, null]] == GINQ {
+ from n1 in nums1
+ rightjoin n2 in nums2 on n1 == n2
+ select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from rightjoin select - 7"() {
+ assertScript '''
+ def nums2 = [1, 2, 3, null, null]
+ def nums1 = [2, 3, 4, null]
+ assert [[null, 1], [2, 2], [3, 3], [null, null], [null, null]] == GINQ {
+ from n1 in nums1
+ rightjoin n2 in nums2 on n1 == n2
+ select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from rightjoin select - 8"() {
+ assertScript '''
+ def nums2 = [1, 2, 3, null, null]
+ def nums1 = [2, 3, 4, null, null]
+ assert [[null, 1], [2, 2], [3, 3], [null, null], [null, null]] == GINQ {
+ from n1 in nums1
+ rightjoin n2 in nums2 on n1 == n2
+ select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from rightjoin select - 9"() {
+ assertScript '''
+ def nums2 = [1, 2, 3, null]
+ def nums1 = [2, 3, 4, null, null]
+ assert [[null, 1], [2, 2], [3, 3], [null, null]] == GINQ {
+ from n1 in nums1
+ rightjoin n2 in nums2 on n1 == n2
+ select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from rightjoin select - 10"() {
+ assertScript '''
+ def nums2 = [1, 2, 3]
+ def nums1 = [2, 3, 4, null, null]
+ assert [[null, 1], [2, 2], [3, 3]] == GINQ {
+ from n1 in nums1
+ rightjoin n2 in nums2 on n1 == n2
+ select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from rightjoin where select - 1"() {
+ assertScript '''
+ def nums2 = [1, 2, 3]
+ def nums1 = [2, 3, 4, null, null]
+ assert [[2, 2], [3, 3]] == GINQ {
+ from n1 in nums1
+ rightjoin n2 in nums2 on n1 == n2
+ where n1 != null
+ select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from fulljoin select - 1"() {
+ assertScript '''
+ def nums1 = [1, 2, 3]
+ def nums2 = [2, 3, 4]
+ assert [[1, null], [2, 2], [3, 3], [null, 4]] == GINQ {
+ from n1 in nums1
+ fulljoin n2 in nums2 on n1 == n2
+ select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from fulljoin where select - 1"() {
+ assertScript '''
+ def nums1 = [1, 2, 3]
+ def nums2 = [2, 3, 4]
+ assert [[2, 2], [3, 3]] == GINQ {
+ from n1 in nums1
+ fulljoin n2 in nums2 on n1 == n2
+ where n1 != null && n2 != null
+ select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from crossjoin select - 1"() {
+ assertScript '''
+ def nums1 = [1, 2, 3]
+ def nums2 = [3, 4, 5]
+ assert [[1, 3], [1, 4], [1, 5], [2, 3], [2, 4], [2, 5], [3, 3], [3, 4], [3, 5]] == GINQ {
+ from n1 in nums1
+ crossjoin n2 in nums2
+ select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from crossjoin where select - 1"() {
+ assertScript '''
+ def nums1 = [1, 2, 3]
+ def nums2 = [3, 4, 5]
+ assert [[3, 3], [3, 5]] == GINQ {
+ from n1 in nums1
+ crossjoin n2 in nums2
+ where n1 > 2 && n2 != 4
+ select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from orderby select - 1"() {
+ assertScript '''
+ assert [1, 2, 5, 6] == GINQ {
+ from n in [1, 5, 2, 6]
+ orderby n
+ select n
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from orderby select - 2"() {
+ assertScript '''
+ @groovy.transform.EqualsAndHashCode
+ class Person {
+ String name
+ int age
+
+ Person(String name, int age) {
+ this.name = name
+ this.age = age
+ }
+ }
+ def persons = [new Person('Linda', 21), new Person('Daniel', 35), new Person('David', 21)]
+ assert [new Person('Daniel', 35), new Person('Linda', 21), new Person('David', 21)] == GINQ {
+ from p in persons
+ orderby p.age in desc
+ select p
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from orderby select - 3"() {
+ assertScript '''
+ @groovy.transform.EqualsAndHashCode
+ class Person {
+ String name
+ int age
+
+ Person(String name, int age) {
+ this.name = name
+ this.age = age
+ }
+ }
+ def persons = [new Person('Linda', 21), new Person('Daniel', 35), new Person('David', 21)]
+ assert [new Person('Daniel', 35), new Person('David', 21), new Person('Linda', 21)] == GINQ {
+ from p in persons
+ orderby p.age in desc, p.name
+ select p
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from orderby select - 4"() {
+ assertScript '''
+ @groovy.transform.EqualsAndHashCode
+ class Person {
+ String name
+ int age
+
+ Person(String name, int age) {
+ this.name = name
+ this.age = age
+ }
+ }
+ def persons = [new Person('Linda', 21), new Person('Daniel', 35), new Person('David', 21)]
+ assert [new Person('Daniel', 35), new Person('David', 21), new Person('Linda', 21)] == GINQ {
+ from p in persons
+ orderby p.age in desc, p.name in asc
+ select p
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from orderby select - 5"() {
+ assertScript '''
+ @groovy.transform.EqualsAndHashCode
+ class Person {
+ String name
+ int age
+
+ Person(String name, int age) {
+ this.name = name
+ this.age = age
+ }
+ }
+ def persons = [new Person('Linda', 21), new Person('Daniel', 35), new Person('David', 21)]
+ assert [new Person('Daniel', 35), new Person('Linda', 21), new Person('David', 21)] == GINQ {
+ from p in persons
+ orderby p.age in desc, p.name in desc
+ select p
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from orderby select - 6"() {
+ assertScript '''
+ @groovy.transform.EqualsAndHashCode
+ class Person {
+ String name
+ int age
+
+ Person(String name, int age) {
+ this.name = name
+ this.age = age
+ }
+ }
+ def persons = [new Person('Linda', 21), new Person('Daniel', 35), new Person('David', 21)]
+ assert [new Person('Linda', 21), new Person('David', 21), new Person('Daniel', 35)] == GINQ {
+ from p in persons
+ orderby p.age, p.name in desc
+ select p
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from orderby select - 7"() {
+ assertScript '''
+ @groovy.transform.EqualsAndHashCode
+ class Person {
+ String name
+ int age
+
+ Person(String name, int age) {
+ this.name = name
+ this.age = age
+ }
+ }
+ def persons = [new Person('Linda', 21), new Person('Daniel', 35), new Person('David', 21)]
+ assert [new Person('Linda', 21), new Person('David', 21), new Person('Daniel', 35)] == GINQ {
+ from p in persons
+ orderby p.age in asc, p.name in desc
+ select p
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from orderby select - 8"() {
+ assertScript '''
+ assert [1, 2, 5, 6] == GINQ {
+ from n in [1, 5, 2, 6]
+ orderby 1 / n in desc
+ select n
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from orderby select - 9"() {
+ assertScript '''
+ assert [1, 2, 5, 6] == GINQ {
+ from n in [1, 5, 2, 6]
+ orderby Math.pow(1 / n, 1) in desc
+ select n
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerjoin orderby select - 1"() {
+ assertScript '''
+ assert [2, 3] == GINQ {
+ from n1 in [1, 2, 3]
+ innerjoin n2 in [2, 3, 4] on n1 == n2
+ orderby n1
+ select n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerjoin orderby select - 2"() {
+ assertScript '''
+ assert [3, 2] == GINQ {
+ from n1 in [1, 2, 3]
+ innerjoin n2 in [2, 3, 4] on n1 == n2
+ orderby n1 in desc
+ select n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerjoin orderby select - 3"() {
+ assertScript '''
+ assert [[3, 3], [2, 2]] == GINQ {
+ from n1 in [1, 2, 3]
+ innerjoin n2 in [2, 3, 4] on n1 == n2
+ orderby n1 in desc, n2 in desc
+ select n1, n2
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from groupby select - 1"() {
+ assertScript '''
+ assert [[1, 2], [3, 2], [6, 3]] == GINQ {
+ from n in [1, 1, 3, 3, 6, 6, 6]
+ groupby n
+ select n, count() // reference the column `n` in the groupby clause, and `count()` is a built-in aggregate function
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from where groupby select - 1"() {
+ assertScript '''
+ assert [[1, 2], [6, 3]] == GINQ {
+ from n in [1, 1, 3, 3, 6, 6, 6]
+ where n != 3
+ groupby n
+ select n, count()
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from where groupby orderby select - 1"() {
+ assertScript '''
+ assert [[6, 3], [1, 2]] == GINQ {
+ from n in [1, 1, 3, 3, 6, 6, 6]
+ where n != 3
+ groupby n
+ orderby n in desc
+ select n, count()
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerjoin where groupby orderby select - 1"() {
+ assertScript '''
+ assert [[6, 9], [1, 4]] == GINQ {
+ from n in [1, 1, 3, 3, 6, 6, 6]
+ innerjoin m in [1, 1, 3, 3, 6, 6, 6] on n == m
+ where n != 3
+ groupby n
+ orderby n in desc
+ select n, count()
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerjoin where groupby orderby select - 2"() {
+ assertScript '''
+ assert [[1, 4], [6, 9]] == GINQ {
+ from n in [1, 1, 3, 3, 6, 6, 6]
+ innerjoin m in [1, 1, 3, 3, 6, 6, 6] on n == m
+ where n != 3
+ groupby n
+ orderby count() in asc
+ select n, count()
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerjoin where groupby orderby select - 3"() {
+ assertScript '''
+ assert [[2, 3, 1], [1, 2, 1]] == GINQ {
+ from n in [1, 2, 3]
+ innerjoin m in [2, 3, 4] on n + 1 == m
+ where n != 3
+ groupby n, m
+ orderby n in desc
+ select n, m, count()
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from innerjoin where groupby orderby select - 4"() {
+ assertScript '''
+ assert [[1, 2, 1], [2, 3, 1]] == GINQ {
+ from n in [1, 2, 3]
+ innerjoin m in [2, 3, 4] on n + 1 == m
+ where n != 3
+ groupby n, m
+ orderby m in asc
+ select n, m, count()
+ }.toList()
+ '''
+ }
+
+ @CompileDynamic
+ @Test
+ void "testGinq - query json - 1"() {
+ def parser = new JsonSlurper()
+ def json = parser.parseText('''
+ {
+ "persons": [
+ {"id": 1, "name": "Daniel"},
+ {"id": 2, "name": "Paul"},
+ {"id": 3, "name": "Eric"}
+ ],
+ "tasks": [
+ {"id": 1, "assignee": 1, "content": "task1", "manDay": 6},
+ {"id": 2, "assignee": 1, "content": "task2", "manDay": 1},
+ {"id": 3, "assignee": 2, "content": "task3", "manDay": 3},
+ {"id": 4, "assignee": 3, "content": "task4", "manDay": 5}
+ ]
+ }
+ ''')
+
+ def expected = [
+ [taskId: 1, taskContent: 'task1', assignee: 'Daniel', manDay: 6],
+ [taskId: 4, taskContent: 'task4', assignee: 'Eric', manDay: 5],
+ [taskId: 3, taskContent: 'task3', assignee: 'Paul', manDay: 3]
+ ]
+
+ assert expected == GINQ {
+ from p in json.persons
+ innerjoin t in json.tasks on t.assignee == p.id
+ where t.id in [1, 3, 4]
+ orderby t.manDay in desc
+ select (taskId: t.id, taskContent: t.content, assignee: p.name, manDay: t.manDay)
+ }.toList()
+ }
+}
diff --git a/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/provider/collection/NamedTupleTest.groovy b/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/provider/collection/NamedTupleTest.groovy
new file mode 100644
index 0000000..e3f9147
--- /dev/null
+++ b/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/provider/collection/NamedTupleTest.groovy
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.provider.collection
+
+
+import groovy.transform.CompileStatic
+import org.junit.Test
+
+@CompileStatic
+class NamedTupleTest {
+ @Test
+ void testToString() {
+ assert '(a:1, b:2, c:3)' == new NamedTuple([1, 2, 3], ['a', 'b', 'c']).toString()
+ }
+}
diff --git a/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/provider/collection/QueryableCollectionTest.groovy b/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/provider/collection/QueryableCollectionTest.groovy
new file mode 100644
index 0000000..0c31357
--- /dev/null
+++ b/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/provider/collection/QueryableCollectionTest.groovy
@@ -0,0 +1,406 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.linq.provider.collection
+
+
+import groovy.transform.CompileDynamic
+import groovy.transform.CompileStatic
+import groovy.transform.EqualsAndHashCode
+import groovy.transform.ToString
+import org.junit.Test
+
+import java.util.stream.Collectors
+import java.util.stream.Stream
+
+import static org.apache.groovy.linq.provider.collection.Queryable.from
+
+@CompileStatic
+class QueryableCollectionTest {
+ @Test
+ void testFrom() {
+ assert [1, 2, 3] == from(Stream.of(1, 2, 3)).toList()
+ assert [1, 2, 3] == from(Arrays.asList(1, 2, 3)).toList()
+ }
+
+ @Test
+ void testInnerJoin0() {
+ def nums1 = [1, 2, 3]
+ def nums2 = [1, 2, 3]
+ def result = from(nums1).innerJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[1, 1], [2, 2], [3, 3]] == result
+ }
+
+ @Test
+ void testInnerJoin1() {
+ def nums1 = [1, 2, 3]
+ def nums2 = [2, 3, 4]
+ def result = from(nums1).innerJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[2, 2], [3, 3]] == result
+ }
+
+ @Test
+ void testLeftJoin0() {
+ def nums1 = [1, 2, 3]
+ def nums2 = [1, 2, 3]
+ def result = from(nums1).leftJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[1, 1], [2, 2], [3, 3]] == result
+ }
+
+ @Test
+ void testRightJoin0() {
+ def nums2 = [1, 2, 3]
+ def nums1 = [1, 2, 3]
+ def result = from(nums1).rightJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[1, 1], [2, 2], [3, 3]] == result
+ }
+
+ @Test
+ void testLeftJoin1() {
+ def nums1 = [1, 2, 3]
+ def nums2 = [2, 3, 4]
+ def result = from(nums1).leftJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[1, null], [2, 2], [3, 3]] == result
+ }
+
+ @Test
+ void testRightJoin1() {
+ def nums2 = [1, 2, 3]
+ def nums1 = [2, 3, 4]
+ def result = from(nums1).rightJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[null, 1], [2, 2], [3, 3]] == result
+ }
+
+ @Test
+ void testLeftJoin2() {
+ def nums1 = [1, 2, 3, null]
+ def nums2 = [2, 3, 4]
+ def result = from(nums1).leftJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[1, null], [2, 2], [3, 3], [null, null]] == result
+ }
+
+ @Test
+ void testRightJoin2() {
+ def nums2 = [1, 2, 3, null]
+ def nums1 = [2, 3, 4]
+ def result = from(nums1).rightJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[null, 1], [2, 2], [3, 3], [null, null]] == result
+ }
+
+ @Test
+ void testLeftJoin3() {
+ def nums1 = [1, 2, 3, null]
+ def nums2 = [2, 3, 4, null]
+ def result = from(nums1).leftJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[1, null], [2, 2], [3, 3], [null, null]] == result
+ }
+
+ @Test
+ void testRightJoin3() {
+ def nums2 = [1, 2, 3, null]
+ def nums1 = [2, 3, 4, null]
+ def result = from(nums1).rightJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[null, 1], [2, 2], [3, 3], [null, null]] == result
+ }
+
+ @Test
+ void testLeftJoin4() {
+ def nums1 = [1, 2, 3]
+ def nums2 = [2, 3, 4, null]
+ def result = from(nums1).leftJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[1, null], [2, 2], [3, 3]] == result
+ }
+
+ @Test
+ void testRightJoin4() {
+ def nums2 = [1, 2, 3]
+ def nums1 = [2, 3, 4, null]
+ def result = from(nums1).rightJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[null, 1], [2, 2], [3, 3]] == result
+ }
+
+ @Test
+ void testLeftJoin5() {
+ def nums1 = [1, 2, 3, null, null]
+ def nums2 = [2, 3, 4]
+ def result = from(nums1).leftJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[1, null], [2, 2], [3, 3], [null, null], [null, null]] == result
+ }
+
+ @Test
+ void testRightJoin5() {
+ def nums2 = [1, 2, 3, null, null]
+ def nums1 = [2, 3, 4]
+ def result = from(nums1).rightJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[null, 1], [2, 2], [3, 3], [null, null], [null, null]] == result
+ }
+
+ @Test
+ void testLeftJoin6() {
+ def nums1 = [1, 2, 3, null, null]
+ def nums2 = [2, 3, 4, null]
+ def result = from(nums1).leftJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[1, null], [2, 2], [3, 3], [null, null], [null, null]] == result
+ }
+
+ @Test
+ void testRightJoin6() {
+ def nums2 = [1, 2, 3, null, null]
+ def nums1 = [2, 3, 4, null]
+ def result = from(nums1).rightJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[null, 1], [2, 2], [3, 3], [null, null], [null, null]] == result
+ }
+
+ @Test
+ void testLeftJoin7() {
+ def nums1 = [1, 2, 3, null, null]
+ def nums2 = [2, 3, 4, null, null]
+ def result = from(nums1).leftJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[1, null], [2, 2], [3, 3], [null, null], [null, null]] == result
+ }
+
+ @Test
+ void testRightJoin7() {
+ def nums2 = [1, 2, 3, null, null]
+ def nums1 = [2, 3, 4, null, null]
+ def result = from(nums1).rightJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[null, 1], [2, 2], [3, 3], [null, null], [null, null]] == result
+ }
+
+ @Test
+ void testLeftJoin8() {
+ def nums1 = [1, 2, 3, null]
+ def nums2 = [2, 3, 4, null, null]
+ def result = from(nums1).leftJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[1, null], [2, 2], [3, 3], [null, null]] == result
+ }
+
+ @Test
+ void testRightJoin8() {
+ def nums2 = [1, 2, 3, null]
+ def nums1 = [2, 3, 4, null, null]
+ def result = from(nums1).rightJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[null, 1], [2, 2], [3, 3], [null, null]] == result
+ }
+
+ @Test
+ void testLeftJoin9() {
+ def nums1 = [1, 2, 3]
+ def nums2 = [2, 3, 4, null, null]
+ def result = from(nums1).leftJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[1, null], [2, 2], [3, 3]] == result
+ }
+
+ @Test
+ void testRightJoin9() {
+ def nums2 = [1, 2, 3]
+ def nums1 = [2, 3, 4, null, null]
+ def result = from(nums1).rightJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[null, 1], [2, 2], [3, 3]] == result
+ }
+
+ @Test
+ void testFullJoin() {
+ def nums1 = [1, 2, 3]
+ def nums2 = [2, 3, 4]
+ def result = from(nums1).fullJoin(from(nums2), (a, b) -> a == b).toList()
+ assert [[1, null], [2, 2], [3, 3], [null, 4]] == result
+ }
+
+ @Test
+ void testCrossJoin() {
+ def nums1 = [1, 2, 3]
+ def nums2 = [3, 4, 5]
+ def result = from(nums1).crossJoin(from(nums2)).toList()
+ assert [[1, 3], [1, 4], [1, 5], [2, 3], [2, 4], [2, 5], [3, 3], [3, 4], [3, 5]] == result
+ }
+
+ @Test
+ void testWhere() {
+ def nums = [1, 2, 3, 4, 5]
+ def result = from(nums).where(e -> e > 3).toList()
+ assert [4, 5] == result
+ }
+
+ @Test
+ void testGroupBySelect0() {
+ def nums = [1, 2, 2, 3, 3, 4, 4, 5]
+ def result = from(nums).groupBy(e -> e).select(e -> Tuple.tuple(e.v1, e.v2.toList())).toList()
+ assert [[1, [1]], [2, [2, 2]], [3, [3, 3]], [4, [4, 4]], [5, [5]]] == result
+ }
+
+ @Test
+ void testGroupBySelect1() {
+ def nums = [1, 2, 2, 3, 3, 4, 4, 5]
+ def result = from(nums).groupBy(e -> e).select(e -> Tuple.tuple(e.v1, e.v2.count())).toList()
+ assert [[1, 1], [2, 2], [3, 2], [4, 2], [5, 1]] == result
+ }
+
+ @Test
+ void testGroupBySelect2() {
+ def nums = [1, 2, 2, 3, 3, 4, 4, 5]
+ def result =
+ from(nums).groupBy(e -> e)
+ .select(e ->
+ Tuple.tuple(
+ e.v1,
+ e.v2.count(),
+ e.v2.sum(n -> new BigDecimal(n))
+ )
+ ).toList()
+ assert [[1, 1, 1], [2, 2, 4], [3, 2, 6], [4, 2, 8], [5, 1, 5]] == result
+ }
+
+ @Test
+ @CompileDynamic
+ void testGroupBySelect3() {
+ def nums = [1, 2, 2, 3, 3, 4, 4, 5]
+ def result =
+ from(nums).groupBy(e -> e, (k, q) -> k > 2)
+ .select(e ->
+ Tuple.tuple(
+ e.v1,
+ e.v2.count(),
+ e.v2.sum(n -> new BigDecimal(n))
+ )
+ ).toList()
+ assert [[3, 2, 6], [4, 2, 8], [5, 1, 5]] == result
+ }
+
+ @Test
+ void testOrderBy() {
+ Person daniel = new Person('Daniel', 35)
+ Person peter = new Person('Peter', 10)
+ Person alice = new Person('Alice', 22)
+ Person john = new Person('John', 10)
+
+ def persons = [daniel, peter, alice, john]
+ def result = from(persons).orderBy(
+ new Queryable.Order<Person, Comparable>((Person e) -> e.age, true),
+ new Queryable.Order<Person, Comparable>((Person e) -> e.name, true)
+ ).toList()
+ assert [john, peter, alice, daniel] == result
+
+ result = from(persons).orderBy(
+ new Queryable.Order<Person, Comparable>((Person e) -> e.age, false),
+ new Queryable.Order<Person, Comparable>((Person e) -> e.name, true)
+ ).toList()
+ assert [daniel, alice, john, peter] == result
+
+ result = from(persons).orderBy(
+ new Queryable.Order<Person, Comparable>((Person e) -> e.age, true),
+ new Queryable.Order<Person, Comparable>((Person e) -> e.name, false)
+ ).toList()
+ assert [peter, john, alice, daniel] == result
+
+ result = from(persons).orderBy(
+ new Queryable.Order<Person, Comparable>((Person e) -> e.age, false),
+ new Queryable.Order<Person, Comparable>((Person e) -> e.name, false)
+ ).toList()
+ assert [daniel, alice, peter, john] == result
+ }
+
+ @Test
+ void testLimit() {
+ def nums = [1, 2, 3, 4, 5]
+ def result = from(nums).limit(1, 2).toList()
+ assert [2, 3] == result
+
+ result = from(nums).limit(2).toList()
+ assert [1, 2] == result
+ }
+
+ @Test
+ void testSelect() {
+ def nums = [1, 2, 3, 4, 5]
+ def result = from(nums).select(e -> e + 1).toList()
+ assert [2, 3, 4, 5, 6] == result
+ }
+
+ @Test
+ void testDistinct() {
+ def nums = [1, 2, 2, 3, 3, 2, 3, 4, 5, 5]
+ def result = from(nums).distinct().toList()
+ assert [1, 2, 3, 4, 5] == result
+ }
+
+ @Test
+ void testUnion() {
+ def nums1 = [1, 2, 3]
+ def nums2 = [2, 3, 4]
+ def result = from(nums1).union(from(nums2)).toList()
+ assert [1, 2, 3, 4] == result
+ }
+
+ @Test
+ void testUnionAll() {
+ def nums1 = [1, 2, 3]
+ def nums2 = [2, 3, 4]
+ def result = from(nums1).unionAll(from(nums2)).toList()
+ assert [1, 2, 3, 2, 3, 4] == result
+ }
+
+ @Test
+ void testIntersect() {
+ def nums1 = [1, 2, 2, 3]
+ def nums2 = [2, 3, 3, 4]
+ def result = from(nums1).intersect(from(nums2)).toList()
+ assert [2, 3] == result
+ }
+
+ @Test
+ void testMinus() {
+ def nums1 = [1, 1, 2, 3]
+ def nums2 = [2, 3, 4]
+ def result = from(nums1).minus(from(nums2)).toList()
+ assert [1] == result
+ }
+
+ @Test
+ void testFromWhereLimitSelect() {
+ def nums1 = [1, 2, 3, 4, 5]
+ def nums2 = [0, 1, 2, 3, 4, 5, 6]
+ def result =
+ from(nums1)
+ .innerJoin(from(nums2), (a, b) -> a == b)
+ .where(t -> t.v1 > 1)
+ .limit(1, 2)
+ .select(t -> t.v1 + 1)
+ .toList()
+ assert [4, 5] == result
+ }
+
+ @Test
+ void testStream() {
+ def nums = [1, 2, 3]
+ def result = from(nums).stream().collect(Collectors.toList())
+ assert nums == result
+ }
+
+ @ToString
+ @EqualsAndHashCode
+ static class Person {
+ String name
+ int age
+
+ Person(String name, int age) {
+ this.name = name
+ this.age = age
+ }
+ }
+}