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 d65861ec70e19b45fe002941baa8f423f24e85b2 Author: Daniel Sun <sun...@apache.org> AuthorDate: Wed Oct 7 21:03:03 2020 +0800 GROOVY-8258: [GEP] Create a LINQ-like DSL(very basic version) --- settings.gradle | 1 + .../codehaus/groovy/ast/tools/GeneralUtils.java | 9 + subprojects/groovy-linq/build.gradle | 31 ++ .../apache/groovy/linq/GinqGroovyMethods.groovy | 47 ++ .../org/apache/groovy/linq/dsl/GinqAstBuilder.java | 149 ++++++ .../apache/groovy/linq/dsl/GinqSyntaxError.java | 43 ++ .../org/apache/groovy/linq/dsl/GinqVisitor.java | 43 ++ .../groovy/linq/dsl/SyntaxErrorReportable.java | 43 ++ .../dsl/expression/AbstractGinqExpression.java | 35 ++ .../linq/dsl/expression/DataSourceExpression.java | 43 ++ .../linq/dsl/expression/FilterExpression.java | 42 ++ .../linq/dsl/expression/FilterableExpression.java | 39 ++ .../groovy/linq/dsl/expression/FromExpression.java | 47 ++ .../groovy/linq/dsl/expression/GinqExpression.java | 30 ++ .../linq/dsl/expression/InnerJoinExpression.java | 39 ++ .../groovy/linq/dsl/expression/JoinExpression.java | 42 ++ .../groovy/linq/dsl/expression/OnExpression.java | 38 ++ .../linq/dsl/expression/SelectExpression.java | 51 ++ .../linq/dsl/expression/SimpleGinqExpression.java | 72 +++ .../linq/dsl/expression/WhereExpression.java | 38 ++ .../linq/provider/collection/GinqAstWalker.groovy | 321 ++++++++++++ .../linq/provider/collection/NamedList.groovy | 61 +++ .../groovy/linq/provider/collection/Queryable.java | 134 +++++ .../provider/collection/QueryableCollection.java | 253 +++++++++ .../org/apache/groovy/linq/GinqErrorTest.groovy | 82 +++ .../groovy/org/apache/groovy/linq/GinqTest.groovy | 577 +++++++++++++++++++++ .../collection/QueryableCollectionTest.groovy | 406 +++++++++++++++ 27 files changed, 2716 insertions(+) diff --git a/settings.gradle b/settings.gradle index b8a3622..3aca670 100644 --- a/settings.gradle +++ b/settings.gradle @@ -45,6 +45,7 @@ def subprojects = ['groovy-ant', 'groovy-jmx', 'groovy-json', 'groovy-jsr223', + 'groovy-linq', 'groovy-macro', 'groovy-macro-library', 'groovy-nio', diff --git a/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java b/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java index 6abbe61..15e4db6 100644 --- a/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java +++ b/src/main/java/org/codehaus/groovy/ast/tools/GeneralUtils.java @@ -43,6 +43,7 @@ import org.codehaus.groovy.ast.expr.ConstructorCallExpression; import org.codehaus.groovy.ast.expr.DeclarationExpression; import org.codehaus.groovy.ast.expr.Expression; import org.codehaus.groovy.ast.expr.FieldExpression; +import org.codehaus.groovy.ast.expr.LambdaExpression; import org.codehaus.groovy.ast.expr.ListExpression; import org.codehaus.groovy.ast.expr.MapEntryExpression; import org.codehaus.groovy.ast.expr.MapExpression; @@ -251,6 +252,14 @@ public class GeneralUtils { return closureX(Parameter.EMPTY_ARRAY, code); } + public static LambdaExpression lambdaX(final Parameter[] params, final Statement code) { + return new LambdaExpression(params, code); + } + + public static LambdaExpression lambdaX(final Statement code) { + return lambdaX(Parameter.EMPTY_ARRAY, code); + } + /** * Builds a binary expression that compares two values. * diff --git a/subprojects/groovy-linq/build.gradle b/subprojects/groovy-linq/build.gradle new file mode 100644 index 0000000..cc476c4 --- /dev/null +++ b/subprojects/groovy-linq/build.gradle @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +dependencies { + implementation rootProject + implementation project(':groovy-macro') + testImplementation rootProject.sourceSets.test.runtimeClasspath + testImplementation project(':groovy-test') +} + +task moduleDescriptor(type: org.codehaus.groovy.gradle.WriteExtensionDescriptorTask) { + extensionClasses = 'org.apache.groovy.linq.GinqGroovyMethods' +} + +compileJava.dependsOn moduleDescriptor diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/GinqGroovyMethods.groovy b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/GinqGroovyMethods.groovy new file mode 100644 index 0000000..dcb3d75 --- /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.SimpleGinqExpression +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) + SimpleGinqExpression simpleGinqExpression = ginqAstBuilder.getSimpleGinqExpression() + + GinqAstWalker ginqBuilder = new GinqAstWalker(ctx.getSourceUnit()) + MethodCallExpression selectMethodCallExpression = ginqBuilder.visitSimpleGinqExpression(simpleGinqExpression) + + return selectMethodCallExpression + } +} diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqAstBuilder.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqAstBuilder.java new file mode 100644 index 0000000..2e27d2b --- /dev/null +++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqAstBuilder.java @@ -0,0 +1,149 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.groovy.linq.dsl; + +import org.apache.groovy.linq.dsl.expression.FilterExpression; +import org.apache.groovy.linq.dsl.expression.FilterableExpression; +import org.apache.groovy.linq.dsl.expression.FromExpression; +import org.apache.groovy.linq.dsl.expression.GinqExpression; +import org.apache.groovy.linq.dsl.expression.InnerJoinExpression; +import org.apache.groovy.linq.dsl.expression.JoinExpression; +import org.apache.groovy.linq.dsl.expression.OnExpression; +import org.apache.groovy.linq.dsl.expression.SelectExpression; +import org.apache.groovy.linq.dsl.expression.SimpleGinqExpression; +import org.apache.groovy.linq.dsl.expression.WhereExpression; +import org.codehaus.groovy.GroovyBugError; +import org.codehaus.groovy.ast.CodeVisitorSupport; +import org.codehaus.groovy.ast.expr.ArgumentListExpression; +import org.codehaus.groovy.ast.expr.BinaryExpression; +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.expr.MethodCallExpression; +import org.codehaus.groovy.control.SourceUnit; +import org.codehaus.groovy.syntax.Types; + +/** + * Build the AST for GINQ + * + * @since 4.0.0 + */ +public class GinqAstBuilder extends CodeVisitorSupport implements SyntaxErrorReportable { + private SimpleGinqExpression currentSimpleGinqExpression; + private SimpleGinqExpression latestSimpleGinqExpression; + private GinqExpression ginqExpression; // store the return value + private final SourceUnit sourceUnit; + + public GinqAstBuilder(SourceUnit sourceUnit) { + this.sourceUnit = sourceUnit; + } + + public SimpleGinqExpression getSimpleGinqExpression() { + return latestSimpleGinqExpression; + } + + @Override + public void visitMethodCallExpression(MethodCallExpression call) { + super.visitMethodCallExpression(call); + final String methodName = call.getMethodAsString(); + + if ("from".equals(methodName)) { + currentSimpleGinqExpression = new SimpleGinqExpression(); // store the result + } + + if ("from".equals(methodName) || "innerJoin".equals(methodName)) { + ArgumentListExpression arguments = (ArgumentListExpression) call.getArguments(); + if (arguments.getExpressions().size() != 1) { + this.collectSyntaxError( + new GinqSyntaxError( + "Only 1 argument expected for `" + methodName + "`, e.g. `" + methodName + " n in nums`", + call.getLineNumber(), call.getColumnNumber() + ) + ); + } + final Expression expression = arguments.getExpression(0); + if (!(expression instanceof BinaryExpression + && ((BinaryExpression) expression).getOperation().getType() == Types.KEYWORD_IN)) { + this.collectSyntaxError( + new GinqSyntaxError( + "`in` is expected for `" + methodName + "`, e.g. `" + methodName + " n in nums`", + call.getLineNumber(), call.getColumnNumber() + ) + ); + } + BinaryExpression binaryExpression = (BinaryExpression) expression; + Expression aliasExpr = binaryExpression.getLeftExpression(); + Expression dataSourceExpr = null == latestSimpleGinqExpression ? binaryExpression.getRightExpression() : latestSimpleGinqExpression; + + FilterableExpression filterableExpression = null; + if ("from".equals(methodName)) { + filterableExpression = new FromExpression(aliasExpr, dataSourceExpr); + currentSimpleGinqExpression.setFromExpression((FromExpression) filterableExpression); + } else if ("innerJoin".equals(methodName)) { + filterableExpression = new InnerJoinExpression(aliasExpr, dataSourceExpr); + currentSimpleGinqExpression.addJoinExpression((JoinExpression) filterableExpression); + } + filterableExpression.setSourcePosition(call); + ginqExpression = filterableExpression; + + return; + } + + if ("where".equals(methodName) || "on".equals(methodName)) { + Expression filterExpr = ((ArgumentListExpression) call.getArguments()).getExpression(0); + + FilterExpression filterExpression = null; + if ("where".equals(methodName)) { + filterExpression = new WhereExpression(filterExpr); + } else if ("on".equals(methodName)) { + filterExpression = new OnExpression(filterExpr); + } + + if (null == filterExpression) { + throw new GroovyBugError("Unknown method: " + methodName); + } + + filterExpression.setSourcePosition(call); + + if (ginqExpression instanceof FilterableExpression) { // TODO more strict check + ((FilterableExpression) ginqExpression).addFilterExpression(filterExpression); + } else { + throw new GroovyBugError("The preceding expression is not a FilterableExpression: " + ginqExpression); + } + + return; + } + + if ("select".equals(methodName)) { + SelectExpression selectExpression = new SelectExpression(call.getArguments()); + selectExpression.setSourcePosition(call); + + currentSimpleGinqExpression.setSelectExpression(selectExpression); + ginqExpression = selectExpression; + + latestSimpleGinqExpression = currentSimpleGinqExpression; + currentSimpleGinqExpression = null; + + return; + } + } + + @Override + public SourceUnit getSourceUnit() { + return sourceUnit; + } +} diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqSyntaxError.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqSyntaxError.java new file mode 100644 index 0000000..652d835 --- /dev/null +++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqSyntaxError.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.groovy.linq.dsl; + +/** + * Represents GINQ syntax error + * + * @since 4.0.0 + */ +public class GinqSyntaxError extends AssertionError { + private final int line; + private final int column; + + public GinqSyntaxError(String message, int line, int column) { + super(message, null); + this.line = line; + this.column = column; + } + + public int getLine() { + return line; + } + + public int getColumn() { + return column; + } +} diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqVisitor.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqVisitor.java new file mode 100644 index 0000000..3e5f27e --- /dev/null +++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/GinqVisitor.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.groovy.linq.dsl; + +import org.apache.groovy.linq.dsl.expression.FromExpression; +import org.apache.groovy.linq.dsl.expression.GinqExpression; +import org.apache.groovy.linq.dsl.expression.InnerJoinExpression; +import org.apache.groovy.linq.dsl.expression.OnExpression; +import org.apache.groovy.linq.dsl.expression.SelectExpression; +import org.apache.groovy.linq.dsl.expression.SimpleGinqExpression; +import org.apache.groovy.linq.dsl.expression.WhereExpression; + +/** + * Represents the visitor for AST of GINQ + * + * @param <R> the type of visit result + * @since 4.0.0 + */ +public interface GinqVisitor<R> { + R visitSimpleGinqExpression(SimpleGinqExpression simpleGinqExpression); + R visitFromExpression(FromExpression fromExpression); + R visitInnerJoinExpression(InnerJoinExpression innerJoinExpression); + R visitOnExpression(OnExpression onExpression); + R visitWhereExpression(WhereExpression whereExpression); + R visitSelectExpression(SelectExpression selectExpression); + R visit(GinqExpression expression); +} diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/SyntaxErrorReportable.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/SyntaxErrorReportable.java new file mode 100644 index 0000000..5628451 --- /dev/null +++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/SyntaxErrorReportable.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.groovy.linq.dsl; + +import org.codehaus.groovy.control.SourceUnit; +import org.codehaus.groovy.control.messages.SyntaxErrorMessage; +import org.codehaus.groovy.syntax.SyntaxException; + +/** + * Supports reporting the syntax error of GINQ + * + * @since 4.0.0 + */ +public interface SyntaxErrorReportable { + SourceUnit getSourceUnit(); + + default void collectSyntaxError(GinqSyntaxError ginqSyntaxError) { + SourceUnit sourceUnit = getSourceUnit(); + + SyntaxException e = new SyntaxException( + ginqSyntaxError.getMessage(), + ginqSyntaxError, + ginqSyntaxError.getLine(), + ginqSyntaxError.getColumn()); + sourceUnit.getErrorCollector().addFatalError(new SyntaxErrorMessage(e, sourceUnit)); + } +} diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/AbstractGinqExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/AbstractGinqExpression.java new file mode 100644 index 0000000..f6b5242 --- /dev/null +++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/AbstractGinqExpression.java @@ -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. + */ +package org.apache.groovy.linq.dsl.expression; + +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 GinqExpression, NodeMetaDataHandler { + @Override + public Expression transformExpression(ExpressionTransformer transformer) { + throw new UnsupportedOperationException("transform GINQ expression is not supported yet"); + } +} diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/DataSourceExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/DataSourceExpression.java new file mode 100644 index 0000000..9961f14 --- /dev/null +++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/DataSourceExpression.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.groovy.linq.dsl.expression; + +import org.codehaus.groovy.ast.expr.Expression; + +/** + * Represents data source expression + * + * @since 4.0.0 + */ +public abstract class DataSourceExpression extends FilterableExpression { + protected Expression aliasExpr; + protected Expression dataSourceExpr; + + public DataSourceExpression(Expression aliasExpr, Expression dataSourceExpr) { + this.aliasExpr = aliasExpr; + this.dataSourceExpr = dataSourceExpr; + } + + public Expression getAliasExpr() { + return aliasExpr; + } + public Expression getDataSourceExpr() { + return dataSourceExpr; + } +} diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/FilterExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/FilterExpression.java new file mode 100644 index 0000000..0fb4dde --- /dev/null +++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/FilterExpression.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.groovy.linq.dsl.expression; + +import org.codehaus.groovy.ast.expr.Expression; + +/** + * Represents filter expression + * + * @since 4.0.0 + */ +public abstract class FilterExpression extends AbstractGinqExpression { + protected Expression filterExpr; + + public FilterExpression(Expression filterExpr) { + this.filterExpr = filterExpr; + } + + public Expression getFilterExpr() { + return filterExpr; + } + + public void setFilterExpr(Expression filterExpr) { + this.filterExpr = filterExpr; + } +} diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/FilterableExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/FilterableExpression.java new file mode 100644 index 0000000..2808438 --- /dev/null +++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/FilterableExpression.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.groovy.linq.dsl.expression; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents expression which could be filtered via {@code where} expression + * + * @since 4.0.0 + */ +public abstract class FilterableExpression extends AbstractGinqExpression { + protected List<FilterExpression> filterExpressionList = new ArrayList<>(); + + public List<FilterExpression> getFilterExpressionList() { + return filterExpressionList; + } + + public void addFilterExpression(FilterExpression filterExpression) { + this.filterExpressionList.add(filterExpression); + } +} diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/FromExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/FromExpression.java new file mode 100644 index 0000000..2ca2894 --- /dev/null +++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/FromExpression.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.groovy.linq.dsl.expression; + +import org.apache.groovy.linq.dsl.GinqVisitor; +import org.codehaus.groovy.ast.expr.Expression; + +/** + * Represents the from expression + * + * @since 4.0.0 + */ +public class FromExpression extends DataSourceExpression { + public FromExpression(Expression aliasExpr, Expression dataSourceExpr) { + super(aliasExpr, dataSourceExpr); + } + + @Override + public <R> R accept(GinqVisitor<R> visitor) { + return visitor.visitFromExpression(this); + } + + @Override + public String toString() { + return "FromExpression{" + + "aliasExpr=" + aliasExpr + + ", dataSourceExpr=" + dataSourceExpr + + ", whereExpression=" + (filterExpressionList.isEmpty() ? null : filterExpressionList.get(0)) + + '}'; + } +} diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/GinqExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/GinqExpression.java new file mode 100644 index 0000000..32ae24b --- /dev/null +++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/GinqExpression.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.groovy.linq.dsl.expression; + +import org.apache.groovy.linq.dsl.GinqVisitor; + +/** + * Represents the GINQ expression + * + * @since 4.0.0 + */ +public interface GinqExpression { + <R> R accept(GinqVisitor<R> visitor); +} diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/InnerJoinExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/InnerJoinExpression.java new file mode 100644 index 0000000..8df0377 --- /dev/null +++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/InnerJoinExpression.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.groovy.linq.dsl.expression; + +import org.apache.groovy.linq.dsl.GinqVisitor; +import org.codehaus.groovy.ast.expr.Expression; + +/** + * Represents inner join expression + * + * @since 4.0.0 + */ +public class InnerJoinExpression extends JoinExpression { + + public InnerJoinExpression(Expression aliasExpr, Expression dataSourceExpr) { + super(aliasExpr, dataSourceExpr); + } + + @Override + public <R> R accept(GinqVisitor<R> visitor) { + return visitor.visitInnerJoinExpression(this); + } +} diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/JoinExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/JoinExpression.java new file mode 100644 index 0000000..4a98b36 --- /dev/null +++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/JoinExpression.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.groovy.linq.dsl.expression; + +import org.codehaus.groovy.ast.expr.Expression; + +/** + * Represents join expression + * + * @since 4.0.0 + */ +public abstract class JoinExpression extends DataSourceExpression { + protected OnExpression onExpression; + + public JoinExpression(Expression aliasExpr, Expression dataSourceExpr) { + super(aliasExpr, dataSourceExpr); + } + + public OnExpression getOnExpression() { + return onExpression; + } + + public void setOnExpression(OnExpression onExpression) { + this.onExpression = onExpression; + } +} diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/OnExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/OnExpression.java new file mode 100644 index 0000000..98394d7 --- /dev/null +++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/OnExpression.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.groovy.linq.dsl.expression; + +import org.apache.groovy.linq.dsl.GinqVisitor; +import org.codehaus.groovy.ast.expr.Expression; + +/** + * Represents on expression + * + * @since 4.0.0 + */ +public class OnExpression extends FilterExpression { + public OnExpression(Expression filterExpr) { + super(filterExpr); + } + + @Override + public <R> R accept(GinqVisitor<R> visitor) { + return visitor.visitOnExpression(this); + } +} diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/SelectExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/SelectExpression.java new file mode 100644 index 0000000..82ec0c1 --- /dev/null +++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/SelectExpression.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.groovy.linq.dsl.expression; + +import org.apache.groovy.linq.dsl.GinqVisitor; +import org.codehaus.groovy.ast.expr.Expression; + +/** + * Represents the select expression + * + * @since 4.0.0 + */ +public class SelectExpression extends AbstractGinqExpression { + private final Expression projectionExpr; + + public SelectExpression(Expression projectionExpr) { + this.projectionExpr = projectionExpr; + } + + @Override + public <R> R accept(GinqVisitor<R> visitor) { + return visitor.visitSelectExpression(this); + } + + public Expression getProjectionExpr() { + return projectionExpr; + } + + @Override + public String toString() { + return "SelectExpression{" + + "projectionExpr=" + projectionExpr + + '}'; + } +} diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/SimpleGinqExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/SimpleGinqExpression.java new file mode 100644 index 0000000..43abe75 --- /dev/null +++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/SimpleGinqExpression.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.groovy.linq.dsl.expression; + +import org.apache.groovy.linq.dsl.GinqVisitor; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represent the root expression of GINQ + * + * @since 4.0.0 + */ +public class SimpleGinqExpression extends AbstractGinqExpression { + private FromExpression fromExpression; + private final List<JoinExpression> joinExpressionList = new ArrayList<>(); + private SelectExpression selectExpression; + + @Override + public <R> R accept(GinqVisitor<R> visitor) { + return visitor.visitSimpleGinqExpression(this); + } + + public FromExpression getFromExpression() { + return fromExpression; + } + + public void setFromExpression(FromExpression fromExpression) { + this.fromExpression = fromExpression; + } + + public List<JoinExpression> getJoinExpressionList() { + return joinExpressionList; + } + + public void addJoinExpression(JoinExpression joinExpression) { + joinExpressionList.add(joinExpression); + } + + public SelectExpression getSelectExpression() { + return selectExpression; + } + + public void setSelectExpression(SelectExpression selectExpression) { + this.selectExpression = selectExpression; + } + + @Override + public String toString() { + return "SimpleGinqExpression{" + + "fromExpression=" + fromExpression + + ", selectExpression=" + selectExpression + + '}'; + } +} diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/WhereExpression.java b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/WhereExpression.java new file mode 100644 index 0000000..66bde4f --- /dev/null +++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/dsl/expression/WhereExpression.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.groovy.linq.dsl.expression; + +import org.apache.groovy.linq.dsl.GinqVisitor; +import org.codehaus.groovy.ast.expr.Expression; + +/** + * Represent the where expression + * + * @since 4.0.0 + */ +public class WhereExpression extends FilterExpression { + public WhereExpression(Expression filterExpr) { + super(filterExpr); + } + + @Override + public <R> R accept(GinqVisitor<R> visitor) { + return visitor.visitWhereExpression(this); + } +} diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/provider/collection/GinqAstWalker.groovy b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/provider/collection/GinqAstWalker.groovy new file mode 100644 index 0000000..bd593dd --- /dev/null +++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/provider/collection/GinqAstWalker.groovy @@ -0,0 +1,321 @@ +/* + * 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.DataSourceExpression +import org.apache.groovy.linq.dsl.expression.FilterExpression +import org.apache.groovy.linq.dsl.expression.FromExpression +import org.apache.groovy.linq.dsl.expression.GinqExpression +import org.apache.groovy.linq.dsl.expression.InnerJoinExpression +import org.apache.groovy.linq.dsl.expression.JoinExpression +import org.apache.groovy.linq.dsl.expression.OnExpression +import org.apache.groovy.linq.dsl.expression.SelectExpression +import org.apache.groovy.linq.dsl.expression.SimpleGinqExpression +import org.apache.groovy.linq.dsl.expression.WhereExpression +import org.codehaus.groovy.ast.ClassHelper +import org.codehaus.groovy.ast.expr.ArgumentListExpression +import org.codehaus.groovy.ast.expr.CastExpression +import org.codehaus.groovy.ast.expr.ClassExpression +import org.codehaus.groovy.ast.expr.ConstantExpression +import org.codehaus.groovy.ast.expr.Expression +import org.codehaus.groovy.ast.expr.ExpressionTransformer +import org.codehaus.groovy.ast.expr.ListExpression +import org.codehaus.groovy.ast.expr.MethodCallExpression +import org.codehaus.groovy.ast.expr.TupleExpression +import org.codehaus.groovy.ast.expr.VariableExpression +import org.codehaus.groovy.control.SourceUnit + +import static org.codehaus.groovy.ast.tools.GeneralUtils.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 + + GinqAstWalker(SourceUnit sourceUnit) { + this.sourceUnit = sourceUnit + } + + @Override + MethodCallExpression visitSimpleGinqExpression(SimpleGinqExpression simpleGinqExpression) { + FromExpression fromExpression = simpleGinqExpression.getFromExpression() + MethodCallExpression fromMethodCallExpression = this.visitFromExpression(fromExpression) + + MethodCallExpression selectMethodReceiver = fromMethodCallExpression + + JoinExpression lastJoinExpression = null + MethodCallExpression lastJoinMethodCallExpression = null + for (JoinExpression joinExpression : simpleGinqExpression.getJoinExpressionList()) { + joinExpression.putNodeMetaData(__METHOD_CALL_RECEIVER, lastJoinMethodCallExpression ?: fromMethodCallExpression) + joinExpression.putNodeMetaData(__DATA_SOURCE_EXPRESSION, lastJoinExpression ?: fromExpression) + + lastJoinExpression = joinExpression + lastJoinMethodCallExpression = this.visitInnerJoinExpression((InnerJoinExpression) lastJoinExpression) + } + + if (lastJoinMethodCallExpression) { + selectMethodReceiver = lastJoinMethodCallExpression + } + + SelectExpression selectExpression = simpleGinqExpression.getSelectExpression() + selectExpression.putNodeMetaData(__METHOD_CALL_RECEIVER, selectMethodReceiver) + selectExpression.putNodeMetaData(__DATA_SOURCE_EXPRESSION, lastJoinExpression ?: fromExpression) + + MethodCallExpression selectMethodCallExpression = this.visitSelectExpression(selectExpression) + + + return selectMethodCallExpression + } + + @Override + MethodCallExpression visitFromExpression(FromExpression fromExpression) { + MethodCallExpression fromMethodCallExpression = constructFromMethodCallExpression(fromExpression) + + List<FilterExpression> filterExpressionList = fromExpression.getFilterExpressionList() + if (filterExpressionList) { + WhereExpression whereExpression = (WhereExpression) filterExpressionList.get(0) + whereExpression.putNodeMetaData(__DATA_SOURCE_EXPRESSION, fromExpression) + whereExpression.putNodeMetaData(__METHOD_CALL_RECEIVER, fromMethodCallExpression) + + return visitWhereExpression(whereExpression) + } + + return fromMethodCallExpression + } + + @Override + MethodCallExpression visitInnerJoinExpression(InnerJoinExpression innerJoinExpression) { + Expression receiver = innerJoinExpression.getNodeMetaData(__METHOD_CALL_RECEIVER) + DataSourceExpression dataSourceExpression = innerJoinExpression.getNodeMetaData(__DATA_SOURCE_EXPRESSION) + Expression receiverAliasExpr = dataSourceExpression.aliasExpr + List<FilterExpression> filterExpressionList = innerJoinExpression.getFilterExpressionList() + int filterExpressionListSize = filterExpressionList.size() + + if (0 == filterExpressionListSize) { + this.collectSyntaxError( + new GinqSyntaxError( + "`on` clause is expected for `innerJoin`", + innerJoinExpression.getLineNumber(), innerJoinExpression.getColumnNumber() + ) + ) + } + + OnExpression onExpression = (OnExpression) filterExpressionList.get(0) + + WhereExpression whereExpression = null + if (filterExpressionListSize > 1) { + whereExpression = (WhereExpression) filterExpressionList.get(1) + } + + MethodCallExpression innerJoinMethodCallExpression = constructInnerJoinMethodCallExpression(receiver, receiverAliasExpr, innerJoinExpression, onExpression, whereExpression) + + return innerJoinMethodCallExpression + } + + @Override + MethodCallExpression visitOnExpression(OnExpression onExpression) { + return null // do nothing + } + + @CompileDynamic + private MethodCallExpression constructFromMethodCallExpression(FromExpression fromExpression) { + MethodCallExpression fromMethodCallExpression = macro { + $v{ makeQueryableCollectionClassExpression() }.from($v { + if (fromExpression.dataSourceExpr instanceof SimpleGinqExpression) { + return this.visitSimpleGinqExpression((SimpleGinqExpression) fromExpression.dataSourceExpr) + } else { + return fromExpression.dataSourceExpr + } + }) + } + + return fromMethodCallExpression + } + + @CompileDynamic + private MethodCallExpression constructInnerJoinMethodCallExpression( + Expression receiver, Expression receiverAliasExpr, InnerJoinExpression innerJoinExpression, + OnExpression onExpression, WhereExpression whereExpression) { + + MethodCallExpression innerJoinMethodCallExpression = macro { + $v{receiver}.innerJoin($v{ makeQueryableCollectionClassExpression() }.from($v { innerJoinExpression.dataSourceExpr })) + } + + ((ArgumentListExpression) innerJoinMethodCallExpression.getArguments()).getExpressions().add( + lambdaX( + params( + param(ClassHelper.DYNAMIC_TYPE, receiverAliasExpr.text), + param(ClassHelper.DYNAMIC_TYPE, innerJoinExpression.aliasExpr.text) + ), + stmt(onExpression.getFilterExpr()) + ) + ) + + if (whereExpression) { + whereExpression.putNodeMetaData(__DATA_SOURCE_EXPRESSION, innerJoinExpression) + whereExpression.putNodeMetaData(__METHOD_CALL_RECEIVER, innerJoinMethodCallExpression) + return visitWhereExpression(whereExpression) + } + + return innerJoinMethodCallExpression + } + + @Override + MethodCallExpression visitWhereExpression(WhereExpression whereExpression) { + DataSourceExpression dataSourceExpression = whereExpression.getNodeMetaData(__DATA_SOURCE_EXPRESSION) + Expression fromMethodCallExpression = whereExpression.getNodeMetaData(__METHOD_CALL_RECEIVER) + Expression filterExpr = whereExpression.getFilterExpr() + + return callXWithLambda(fromMethodCallExpression, "where", dataSourceExpression, filterExpr) + } + + @Override + MethodCallExpression visitSelectExpression(SelectExpression selectExpression) { + Expression selectMethodReceiver = selectExpression.getNodeMetaData(__METHOD_CALL_RECEIVER) + DataSourceExpression dataSourceExpression = selectExpression.getNodeMetaData(__DATA_SOURCE_EXPRESSION) + Expression projectionExpr = selectExpression.getProjectionExpr() + + List<Expression> expressionList = ((TupleExpression) projectionExpr).getExpressions() + Expression lambdaCode + if (expressionList.size() > 1) { + List<Expression> elementExpressionList = [] + List<Expression> nameExpressionList = [] + for (Expression e : expressionList) { + Expression elementExpression + String elementName + if (e instanceof CastExpression) { + elementExpression = e.expression + elementName = e.type.text + } else { + elementExpression = e + elementName = e.text + } + elementExpressionList << elementExpression + nameExpressionList << new ConstantExpression(elementName) + } + + lambdaCode = ctorX(ClassHelper.make(NamedList.class), args(new ListExpression(elementExpressionList), new ListExpression(nameExpressionList))) + } else { + lambdaCode = expressionList.get(0) + } + + return callXWithLambda(selectMethodReceiver, "select", dataSourceExpression, lambdaCode) + } + + private static Expression correctVariablesOfGinqExpression(JoinExpression joinExpression, Expression expr) { + DataSourceExpression dataSourceExpression = joinExpression.getNodeMetaData(__DATA_SOURCE_EXPRESSION) + final Expression firstAliasExpr = dataSourceExpression.aliasExpr + final Expression secondAliasExpr = joinExpression.aliasExpr + + // The synthetic lambda parameter `__t` represents the element from the result datasource of joining, e.g. `n1` innerJoin `n2` + // The element from first datasource(`n1`) is referenced via `_t.v1` + // and the element from second datasource(`n2`) is referenced via `_t.v2` + expr = expr.transformExpression(new ExpressionTransformer() { + @Override + Expression transform(Expression expression) { + if (expression instanceof VariableExpression) { + Expression transformedExpression = null + if (firstAliasExpr.text == expression.text) { + // replace `n1` with `__t.v1` + transformedExpression = constructFirstAliasVariableAccess() + } else if (secondAliasExpr.text == expression.text) { + // replace `n2` with `__t.v2` + transformedExpression = constructSecondAliasVariableAccess() + } + + if (null != transformedExpression) { + return transformedExpression + } + } + + return expression.transformExpression(this) + } + }) + return expr + } + + @Override + Object visit(GinqExpression expression) { + return expression.accept(this) + } + + private static MethodCallExpression callXWithLambda(Expression receiver, String methodName, DataSourceExpression dataSourceExpression, Expression lambdaCode) { + String lambdaParamName + if (dataSourceExpression instanceof JoinExpression) { + lambdaParamName = __T + lambdaCode = correctVariablesOfGinqExpression((JoinExpression) dataSourceExpression, lambdaCode) + } else { + lambdaParamName = dataSourceExpression.aliasExpr.text + } + + callXWithLambda(receiver, methodName, lambdaParamName, lambdaCode) + } + + private static MethodCallExpression callXWithLambda(Expression receiver, String methodName, String lambdaParamName, Expression lambdaCode) { + callX( + receiver, + methodName, + lambdaX( + params(param(ClassHelper.DYNAMIC_TYPE, lambdaParamName)), + stmt(lambdaCode) + ) + ) + } + + private static Expression constructFirstAliasVariableAccess() { + constructAliasVariableAccess('v1') + } + + private static Expression constructSecondAliasVariableAccess() { + constructAliasVariableAccess('v2') + } + + private static Expression constructAliasVariableAccess(String name) { + propX(new VariableExpression(__T), name) + } + + 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" +} diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/provider/collection/NamedList.groovy b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/provider/collection/NamedList.groovy new file mode 100644 index 0000000..6fbd45c --- /dev/null +++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/provider/collection/NamedList.groovy @@ -0,0 +1,61 @@ +/* + * 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 NamedList<E> extends AbstractList<E> { + private final List<E> elementList + private final List<String> nameList + + private NamedList(List<E> elementList, List<String> nameList) { + this.elementList = elementList + 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 + E get(int index) { + return elementList.get(index) + } + + @Override + int size() { + return elementList.size() + } +} 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..3f44c60 --- /dev/null +++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/provider/collection/Queryable.java @@ -0,0 +1,134 @@ +/* + * 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, l) -> true); + } + + <U extends Comparable<? super U>> Queryable<T> orderBy(Order<? super T, ? extends U>... orders); + + Queryable<T> limit(int offset, int size); + + default Queryable<T> limit(int size) { + return limit(0, size); + } + + <U> Queryable<U> select(Function<? super T, ? extends U> mapper); + + Queryable<T> distinct(); + + default Queryable<T> union(Queryable<? extends T> queryable) { + return this.unionAll(queryable).distinct(); + } + + Queryable<T> unionAll(Queryable<? extends T> queryable); + + Queryable<T> intersect(Queryable<? extends T> queryable); + + Queryable<T> minus(Queryable<? extends T> queryable); + + List<T> toList(); + + default Stream<T> stream() { + return toList().stream(); + } + + // Built-in aggregate functions { + int count(); + BigDecimal sum(Function<? super T, BigDecimal> mapper); + // } Built-in aggregate functions + + class Order<T, U extends Comparable<? super U>> { + private final Function<? super T, ? extends U> keyExtractor; + private final boolean asc; + + public Order(Function<? super T, ? extends U> keyExtractor, boolean asc) { + this.keyExtractor = keyExtractor; + this.asc = asc; + } + + public Function<? super T, ? extends U> getKeyExtractor() { + return keyExtractor; + } + + public boolean isAsc() { + return asc; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Order)) return false; + Order<?, ?> order = (Order<?, ?>) o; + return asc == order.asc && + keyExtractor.equals(order.keyExtractor); + } + + @Override + public int hashCode() { + return Objects.hash(keyExtractor, asc); + } + } +} diff --git a/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/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..645bb08 --- /dev/null +++ b/subprojects/groovy-linq/src/main/groovy/org/apache/groovy/linq/provider/collection/QueryableCollection.java @@ -0,0 +1,253 @@ +/* + * 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; + private Stream<T> sourceStream; + + QueryableCollection(Iterable<T> sourceIterable) { + if (sourceIterable instanceof QueryableCollection) { + QueryableCollection<T> queryableCollection = (QueryableCollection<T>) sourceIterable; + this.sourceIterable = queryableCollection.sourceIterable; + this.sourceStream = queryableCollection.sourceStream; + } else { + this.sourceIterable = sourceIterable; + this.sourceStream = toStream(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); + } + + @Override + public <U extends Comparable<? super U>> Queryable<T> orderBy(Order<? super T, ? extends U>... orders) { + Comparator<T> comparator = null; + for (int i = 0, n = orders.length; i < n; i++) { + Order<? super T, ? extends U> order = orders[i]; + Comparator<U> ascOrDesc = order.isAsc() ? Comparator.naturalOrder() : Comparator.reverseOrder(); + comparator = + 0 == i + ? Comparator.comparing(order.getKeyExtractor(), ascOrDesc) + : comparator.thenComparing(order.getKeyExtractor(), ascOrDesc); + } + + if (null == comparator) { + return this; + } + + return from(this.stream().sorted(comparator)); + } + + @Override + public Queryable<T> limit(int offset, int size) { + Stream<T> stream = this.stream().skip(offset).limit(size); + + return from(stream); + } + + @Override + public <U> Queryable<U> select(Function<? super T, ? extends U> mapper) { + Stream<U> stream = this.stream().map(mapper); + + return from(stream); + } + + @Override + public Queryable<T> distinct() { + Stream<? extends T> stream = this.stream().distinct(); + + return from(stream); + } + + @Override + public Queryable<T> unionAll(Queryable<? extends T> queryable) { + Stream<T> stream = Stream.concat(this.stream(), queryable.stream()); + + return from(stream); + } + + @Override + public Queryable<T> intersect(Queryable<? extends T> queryable) { + Stream<T> stream = this.stream().filter(a -> queryable.stream().anyMatch(b -> b.equals(a))).distinct(); + + return from(stream); + } + + @Override + public Queryable<T> minus(Queryable<? extends T> queryable) { + Stream<T> stream = this.stream().filter(a -> queryable.stream().noneMatch(b -> b.equals(a))).distinct(); + + return from(stream); + } + + @Override + public List<T> toList() { + return stream().collect(Collectors.toList()); + } + + @Override + public Stream<T> stream() { + try { + sourceStream = sourceStream.peek(e -> {}); // check whether the stream is usable + } catch (IllegalStateException ex) { + sourceStream = toStream(sourceIterable); // we have to create new stream every time because Java stream can not be reused + } + + return sourceStream; + } + + @Override + public int count() { + return toList().size(); + } + + @Override + public BigDecimal sum(Function<? super T, BigDecimal> mapper) { + return this.stream().map(mapper).reduce(BigDecimal.ZERO, BigDecimal::add); + } + + private static <T, U> Queryable<Tuple2<T, U>> outerJoin(Queryable<? extends T> queryable1, Queryable<? extends U> queryable2, BiPredicate<? super T, ? super U> joiner) { + Stream<Tuple2<T, U>> stream = + queryable1.stream() + .flatMap(p -> + queryable2.stream() + .map(c -> joiner.test(p, c) ? c : null) + .reduce(new 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::iterator; + } + + @Override + public String toString() { + return toList().toString(); + } +} diff --git a/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/GinqErrorTest.groovy b/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/GinqErrorTest.groovy new file mode 100644 index 0000000..0fedabd --- /dev/null +++ b/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/GinqErrorTest.groovy @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.groovy.linq + +import groovy.transform.CompileStatic +import org.junit.Test + +import static groovy.test.GroovyAssert.shouldFail + +@CompileStatic +class GinqErrorTest { + @Test + void "testGinq - from select - 1"() { + def err = shouldFail '''\ + def numbers = [0, 1, 2] + GINQ { + from numbers + select n + } + ''' + + assert err.toString().contains('`in` is expected for `from`, e.g. `from n in nums` @ line 3, column 17.') + } + + @Test + void "testGinq - from select - 2"() { + def err = shouldFail '''\ + def numbers = [0, 1, 2] + GINQ { + from n as numbers + select n + } + ''' + + assert err.toString().contains('`in` is expected for `from`, e.g. `from n in nums` @ line 3, column 17.') + } + + @Test + void "testGinq - from select - 3"() { + def err = shouldFail '''\ + def numbers = [0, 1, 2] + GINQ { + from n, numbers + select n + } + ''' + + assert err.toString().contains('Only 1 argument expected for `from`, e.g. `from n in nums` @ line 3, column 17.') + } + + @Test + void "testGinq - from innerJoin select - 1"() { + def err = shouldFail '''\ + def nums1 = [1, 2, 3] + def nums2 = [1, 2, 3] + assert [[1, 1], [2, 2], [3, 3]] == GINQ { + from n1 in nums1 + innerJoin n2 in nums2 + select n1, n2 + }.toList() + ''' + + assert err.toString().contains('`on` clause is expected for `innerJoin` @ line 5, column 17.') + } + +} diff --git a/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/GinqTest.groovy b/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/GinqTest.groovy new file mode 100644 index 0000000..edda315 --- /dev/null +++ b/subprojects/groovy-linq/src/test/groovy/org/apache/groovy/linq/GinqTest.groovy @@ -0,0 +1,577 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.groovy.linq + + +import groovy.transform.CompileStatic +import org.junit.Test + +import static groovy.test.GroovyAssert.assertScript + + +@CompileStatic +class GinqTest { + @Test + void "testGinq - from select - 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 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() + ''' + } +} 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 + } + } +}