This is an automated email from the ASF dual-hosted git repository.
sunlan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/master by this push:
new b1e7ac814d GROOVY-11915: GINQ: Add groupby...into with first-class
GroupResult type (#2453)
b1e7ac814d is described below
commit b1e7ac814d9a26b8e36d236b8a8bf1498ed45097
Author: Paul King <[email protected]>
AuthorDate: Sat Apr 11 11:10:43 2026 +1000
GROOVY-11915: GINQ: Add groupby...into with first-class GroupResult type
(#2453)
---
.../org/apache/groovy/ginq/dsl/GinqAstBuilder.java | 30 ++-
.../ginq/dsl/expression/GroupExpression.java | 10 +
.../ginq/provider/collection/GinqAstWalker.groovy | 39 +++
.../provider/collection/runtime/GroupResult.java | 79 ++++++
.../collection/runtime/GroupResultImpl.java | 59 +++++
.../provider/collection/runtime/Queryable.java | 24 ++
.../collection/runtime/QueryableCollection.java | 18 ++
.../groovy-ginq/src/spec/doc/ginq-userguide.adoc | 294 +++++++++++++--------
.../test/org/apache/groovy/ginq/GinqTest.groovy | 86 ++++++
.../org/apache/groovy/ginq/GinqErrorTest.groovy | 27 ++
.../runtime/QueryableCollectionTest.groovy | 21 ++
11 files changed, 570 insertions(+), 117 deletions(-)
diff --git
a/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/GinqAstBuilder.java
b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/GinqAstBuilder.java
index 6c1792cdde..74cfdf4301 100644
---
a/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/GinqAstBuilder.java
+++
b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/GinqAstBuilder.java
@@ -231,6 +231,12 @@ public class GinqAstBuilder extends CodeVisitorSupport
implements SyntaxErrorRep
if (latestGinqExpressionClause instanceof JoinExpression &&
filterExpression instanceof OnExpression) {
((JoinExpression)
latestGinqExpressionClause).setOnExpression((OnExpression) filterExpression);
+ } else if (latestGinqExpressionClause instanceof GroupExpression
&& filterExpression instanceof WhereExpression
+ && ((GroupExpression)
latestGinqExpressionClause).getIntoAlias() != null) {
+ this.collectSyntaxError(new GinqSyntaxError(
+ "`where` after `groupby...into` is not yet supported;
use `having` instead",
+ call.getLineNumber(), call.getColumnNumber()
+ ));
} else if (latestGinqExpressionClause instanceof DataSourceHolder
&& filterExpression instanceof WhereExpression) {
if (null != currentGinqExpression.getGroupExpression() || null
!= currentGinqExpression.getOrderExpression() || null !=
currentGinqExpression.getLimitExpression()) {
this.collectSyntaxError(new GinqSyntaxError(
@@ -297,6 +303,27 @@ public class GinqAstBuilder extends CodeVisitorSupport
implements SyntaxErrorRep
return;
}
+ if (KW_INTO.equals(methodName)) {
+ if (!(latestGinqExpressionClause instanceof GroupExpression)) {
+ this.collectSyntaxError(new GinqSyntaxError(
+ "`into` is only supported after `groupby`",
+ call.getLineNumber(), call.getColumnNumber()
+ ));
+ return;
+ }
+ ArgumentListExpression arguments = (ArgumentListExpression)
call.getArguments();
+ if (arguments.getExpressions().size() != 1 ||
!(arguments.getExpression(0) instanceof VariableExpression)) {
+ this.collectSyntaxError(new GinqSyntaxError(
+ "`into` requires a single alias name, e.g. `groupby x
into g`",
+ call.getLineNumber(), call.getColumnNumber()
+ ));
+ return;
+ }
+ String aliasName = ((VariableExpression)
arguments.getExpression(0)).getName();
+ ((GroupExpression)
latestGinqExpressionClause).setIntoAlias(aliasName);
+ return;
+ }
+
if (KW_ORDERBY.equals(methodName) && !visitingOverClause) {
OrderExpression orderExpression = new
OrderExpression(call.getArguments());
orderExpression.setSourcePosition(call.getMethod());
@@ -491,12 +518,13 @@ public class GinqAstBuilder extends CodeVisitorSupport
implements SyntaxErrorRep
private static final String KW_WITHINGROUP = "withingroup"; // reserved
keyword
private static final String KW_OVER = "over";
private static final String KW_AS = "as";
+ private static final String KW_INTO = "into";
private static final String KW_SHUTDOWN = "shutdown";
private static final Set<String> KEYWORD_SET;
static {
Set<String> keywordSet = new HashSet<>();
keywordSet.addAll(Arrays.asList(KW_WITH, KW_FROM, KW_IN, KW_ON,
KW_WHERE, KW_EXISTS, KW_GROUPBY, KW_HAVING, KW_ORDERBY,
- KW_LIMIT, KW_OFFSET, KW_SELECT,
KW_DISTINCT, KW_WITHINGROUP, KW_OVER, KW_AS, KW_SHUTDOWN));
+ KW_LIMIT, KW_OFFSET, KW_SELECT,
KW_DISTINCT, KW_WITHINGROUP, KW_OVER, KW_AS, KW_INTO, KW_SHUTDOWN));
keywordSet.addAll(JoinExpression.JOIN_NAME_LIST);
KEYWORD_SET = Collections.unmodifiableSet(keywordSet);
}
diff --git
a/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/GroupExpression.java
b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/GroupExpression.java
index 1866a29f28..1ad3a9c34c 100644
---
a/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/GroupExpression.java
+++
b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/GroupExpression.java
@@ -29,6 +29,7 @@ import org.codehaus.groovy.ast.expr.Expression;
public class GroupExpression extends ProcessExpression {
private final Expression classifierExpr;
private HavingExpression havingExpression;
+ private String intoAlias;
public GroupExpression(Expression classifierExpr) {
this.classifierExpr = classifierExpr;
@@ -51,9 +52,18 @@ public class GroupExpression extends ProcessExpression {
this.havingExpression = havingExpression;
}
+ public String getIntoAlias() {
+ return intoAlias;
+ }
+
+ public void setIntoAlias(String intoAlias) {
+ this.intoAlias = intoAlias;
+ }
+
@Override
public String getText() {
return "groupby " + classifierExpr.getText() +
+ (null == intoAlias ? "" : " into " + intoAlias) +
(null == havingExpression ? "" : " " +
havingExpression.getText());
}
diff --git
a/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/GinqAstWalker.groovy
b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/GinqAstWalker.groovy
index a0690e13f2..3541bd7550 100644
---
a/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/GinqAstWalker.groovy
+++
b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/GinqAstWalker.groovy
@@ -588,6 +588,24 @@ class GinqAstWalker implements GinqAstVisitor<Expression>,
SyntaxErrorReportable
getCurrentGinqExpression().putNodeMetaData(__GROUPBY_VISITED, true)
+ String intoAlias = groupExpression.intoAlias
+ if (intoAlias) {
+ getCurrentGinqExpression().putNodeMetaData(__GROUPBY_INTO_ALIAS,
intoAlias)
+
+ HavingExpression havingExpression =
groupExpression.havingExpression
+ if (havingExpression) {
+ // In into-mode, the having lambda parameter is the alias (a
GroupResult)
+ def havingLambda = lambdaX(
+ params(param(dynamicType(), intoAlias)),
+ stmt(havingExpression.filterExpr))
+ argList << havingLambda
+ }
+
+ MethodCallExpression groupMethodCallExpression =
callX(groupMethodCallReceiver, "groupByInto", args(argList))
+ groupMethodCallExpression.setSourcePosition(groupExpression)
+ return groupMethodCallExpression
+ }
+
HavingExpression havingExpression = groupExpression.havingExpression
if (havingExpression) {
Expression filterExpr = havingExpression.filterExpr
@@ -1015,6 +1033,9 @@ class GinqAstWalker implements
GinqAstVisitor<Expression>, SyntaxErrorReportable
}
private void validateGroupCols(List<Expression> expressionList) {
+ if (groupByIntoAlias) {
+ return // In into-mode, access is through the alias; validation
handled by the type system
+ }
if (groupByVisited) {
for (Expression expression : expressionList) {
new
ListExpression(Collections.singletonList(expression)).transformExpression(new
ExpressionTransformer() {
@@ -1451,6 +1472,11 @@ class GinqAstWalker implements
GinqAstVisitor<Expression>, SyntaxErrorReportable
private String getLambdaParamName(DataSourceExpression
dataSourceExpression, Expression lambdaCode) {
boolean groupByVisited = isGroupByVisited()
+ String intoAlias = groupByIntoAlias
+ if (groupByVisited && intoAlias) {
+ lambdaCode.putNodeMetaData(__LAMBDA_PARAM_NAME, intoAlias)
+ return intoAlias
+ }
String lambdaParamName
if (dataSourceExpression instanceof JoinExpression || groupByVisited
|| visitingWindowFunction) {
lambdaParamName = lambdaCode.getNodeMetaData(__LAMBDA_PARAM_NAME)
@@ -1466,8 +1492,16 @@ class GinqAstWalker implements
GinqAstVisitor<Expression>, SyntaxErrorReportable
private Tuple3<String, List<DeclarationExpression>, Expression>
correctVariablesOfLambdaExpression(DataSourceExpression dataSourceExpression,
Expression lambdaCode) {
boolean groupByVisited = isGroupByVisited()
+ String intoAlias = groupByIntoAlias
List<DeclarationExpression> declarationExpressionList =
Collections.emptyList()
String lambdaParamName = getLambdaParamName(dataSourceExpression,
lambdaCode)
+
+ // In into-mode, the lambda parameter IS the alias (a GroupResult).
+ // No variable rewriting or __sourceRecord/__group injection needed.
+ if (groupByVisited && intoAlias) {
+ return tuple(lambdaParamName, declarationExpressionList,
lambdaCode)
+ }
+
if (dataSourceExpression instanceof JoinExpression || groupByVisited) {
Tuple2<List<DeclarationExpression>, Expression>
declarationAndLambdaCode =
correctVariablesOfGinqExpression(dataSourceExpression, lambdaCode)
if (!visitingAggregateFunctionStack) {
@@ -1516,6 +1550,10 @@ class GinqAstWalker implements
GinqAstVisitor<Expression>, SyntaxErrorReportable
return currentGinqExpression.getNodeMetaData(__GROUPBY_VISITED)
}
+ private String getGroupByIntoAlias() {
+ return (String)
currentGinqExpression.getNodeMetaData(__GROUPBY_INTO_ALIAS)
+ }
+
private boolean isVisitingSelect() {
currentGinqExpression.getNodeMetaData(__VISITING_SELECT)
}
@@ -1613,6 +1651,7 @@ class GinqAstWalker implements
GinqAstVisitor<Expression>, SyntaxErrorReportable
private static final String __SUPPLY_ASYNC_LAMBDA_PARAM_NAME_PREFIX =
"__salp_"
private static final String __SOURCE_RECORD = "__sourceRecord"
private static final String __GROUP = "__group"
+ private static final String __GROUPBY_INTO_ALIAS = "__GROUPBY_INTO_ALIAS"
private static final String MD_GROUP_NAME_LIST = "groupNameList"
private static final String MD_SELECT_NAME_LIST = "selectNameList"
private static final String MD_ALIAS_NAME_LIST = 'aliasNameList'
diff --git
a/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/runtime/GroupResult.java
b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/runtime/GroupResult.java
new file mode 100644
index 0000000000..f6e8f4e5be
--- /dev/null
+++
b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/runtime/GroupResult.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.ginq.provider.collection.runtime;
+
+import groovy.transform.Internal;
+
+/**
+ * Represents a group result from a {@code groupby...into} clause.
+ * Extends {@link Queryable} to provide aggregate and query methods
+ * on the group's elements, with a {@code key} property for accessing
+ * the group key.
+ *
+ * @param <K> the type of the group key
+ * @param <T> the type of the grouped elements
+ * @since 6.0.0
+ */
+@Internal
+public interface GroupResult<K, T> extends Queryable<T> {
+
+ /**
+ * Returns the group key.
+ * For single-key groupby, this is the raw key value.
+ * For multi-key groupby, this is a {@link NamedRecord} with named access.
+ *
+ * @return the group key
+ */
+ K getKey();
+
+ /**
+ * Returns a named component of the group key.
+ * Enables {@code g.name} property-style access and {@code g.get("name")}
calls.
+ * For multi-key groupby, looks up the named component in the key record.
+ *
+ * @param name the key component name (from {@code as} alias in groupby)
+ * @return the value of the named key component
+ * @throws UnsupportedOperationException if this is a single-key group
without aliases
+ */
+ Object get(String name);
+
+ /**
+ * Subscript operator for accessing named key components.
+ * Enables {@code g["name"]} syntax.
+ *
+ * @param name the key component name
+ * @return the value of the named key component
+ */
+ default Object getAt(String name) {
+ return get(name);
+ }
+
+ /**
+ * Factory method to create a {@link GroupResult} instance.
+ *
+ * @param key the group key
+ * @param group the grouped elements as a Queryable
+ * @param <K> the type of the group key
+ * @param <T> the type of the grouped elements
+ * @return a new GroupResult
+ */
+ static <K, T> GroupResult<K, T> of(K key, Queryable<T> group) {
+ return new GroupResultImpl<>(key, group);
+ }
+}
diff --git
a/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/runtime/GroupResultImpl.java
b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/runtime/GroupResultImpl.java
new file mode 100644
index 0000000000..b9106a41a3
--- /dev/null
+++
b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/runtime/GroupResultImpl.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.ginq.provider.collection.runtime;
+
+import java.io.Serial;
+
+/**
+ * Default implementation of {@link GroupResult}.
+ *
+ * @param <K> the type of the group key
+ * @param <T> the type of the grouped elements
+ * @since 6.0.0
+ */
+class GroupResultImpl<K, T> extends QueryableCollection<T> implements
GroupResult<K, T> {
+ @Serial private static final long serialVersionUID = -4637595210702145661L;
+
+ private final K key;
+
+ GroupResultImpl(K key, Queryable<T> group) {
+ super(group.toList());
+ this.key = key;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public K getKey() {
+ // For single-key groupby, the classifier wraps the key in a
NamedRecord;
+ // unwrap it so g.key returns the raw value rather than a
single-element tuple
+ if (key instanceof NamedRecord && ((NamedRecord<?, ?>) key).size() ==
1) {
+ return (K) ((NamedRecord<?, ?>) key).get(0);
+ }
+ return key;
+ }
+
+ @Override
+ public Object get(String name) {
+ if (key instanceof NamedRecord) {
+ return ((NamedRecord<?, ?>) key).get(name);
+ }
+ throw new UnsupportedOperationException(
+ "get(String) is only supported for groupby with named keys
(using 'as' aliases). Use getKey() for single-key.");
+ }
+}
diff --git
a/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/runtime/Queryable.java
b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/runtime/Queryable.java
index 703d0d9364..5f4809d9a3 100644
---
a/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/runtime/Queryable.java
+++
b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/runtime/Queryable.java
@@ -245,6 +245,30 @@ public interface Queryable<T> {
return groupBy(classifier, null);
}
+ /**
+ * Group by {@link Queryable} instance, returning {@link GroupResult}
instances
+ * for use with the {@code groupby...into} syntax.
+ *
+ * @param classifier the classifier for group by
+ * @param having the filter condition (may be null)
+ * @param <K> the type of the group key
+ * @return the result of group by as GroupResult instances
+ * @since 6.0.0
+ */
+ <K> Queryable<GroupResult<K, T>> groupByInto(Function<? super T, ? extends
K> classifier, Predicate<? super GroupResult<K, T>> having);
+
+ /**
+ * Group by {@link Queryable} instance without {@code having} clause,
returning {@link GroupResult} instances.
+ *
+ * @param classifier the classifier for group by
+ * @param <K> the type of the group key
+ * @return the result of group by as GroupResult instances
+ * @since 6.0.0
+ */
+ default <K> Queryable<GroupResult<K, T>> groupByInto(Function<? super T, ?
extends K> classifier) {
+ return groupByInto(classifier, null);
+ }
+
/**
* Sort {@link Queryable} instance, similar to SQL's {@code order by}
*
diff --git
a/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/runtime/QueryableCollection.java
b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/runtime/QueryableCollection.java
index fcfe7111bb..124aa10f11 100644
---
a/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/runtime/QueryableCollection.java
+++
b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/provider/collection/runtime/QueryableCollection.java
@@ -262,6 +262,24 @@ class QueryableCollection<T> implements Queryable<T>,
Serializable {
return Group.of(stream);
}
+ @Override
+ public <K> Queryable<GroupResult<K, T>> groupByInto(Function<? super T, ?
extends K> classifier, Predicate<? super GroupResult<K, T>> having) {
+ Collector<T, ?, ? extends Map<K, List<T>>> groupingBy =
+ isParallel() ? Collectors.groupingByConcurrent(classifier,
Collectors.toList())
+ : Collectors.groupingBy(classifier,
Collectors.toList());
+
+ // Materialize group elements as lists so they can be iterated
multiple times
+ // (e.g., having g.count() > 1 followed by select g.count())
+ Stream<GroupResult<K, T>> stream =
+ this.stream()
+ .collect(groupingBy)
+ .entrySet().stream()
+ .map(m -> GroupResult.<K, T>of(m.getKey(),
from(m.getValue())))
+ .filter(gr -> null == having || having.test(gr));
+
+ return from(stream);
+ }
+
@SafeVarargs
@Override
public final <U extends Comparable<? super U>> Queryable<T>
orderBy(Order<? super T, ? extends U>... orders) {
diff --git a/subprojects/groovy-ginq/src/spec/doc/ginq-userguide.adoc
b/subprojects/groovy-ginq/src/spec/doc/ginq-userguide.adoc
index 353a11a1c0..7d91e58512 100644
--- a/subprojects/groovy-ginq/src/spec/doc/ginq-userguide.adoc
+++ b/subprojects/groovy-ginq/src/spec/doc/ginq-userguide.adoc
@@ -21,14 +21,37 @@
= Querying collections in SQL-like style
-Groovy's `groovy-ginq` module provides a higher-level abstraction over
collections.
-It could perform queries against in-memory collections of objects in SQL-like
style.
-Also, querying XML, JSON, YAML, etc. could also be supported because they can
be parsed into collections.
-As GORM and jOOQ are powerful enough to support querying DB, we will cover
collections first.
+GINQ (Groovy-Integrated Query) lets you query in-memory collections using
familiar SQL-like syntax.
+It also works with parsed XML, JSON, YAML, and other formats that produce
collections.
+
+Here is a quick taste:
+[source, groovy]
+----
+include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_execution_01,indent=0]
+----
+
+For a list result, use `GQL` (short for `GQ {...}.toList()`):
+[source, groovy]
+----
+include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_execution_02,indent=0]
+----
== GINQ a.k.a. Groovy-Integrated Query
-GINQ is a DSL for querying with SQL-like syntax, which consists of the
following structure:
+A GINQ expression is wrapped in a `GQ` block and returns a lazy `Queryable`
result:
+```groovy
+def result = GQ {
+ /* GINQ CODE */
+}
+def stream = result.stream() // get the stream from GINQ result
+def list = result.toList() // get the list from GINQ result
+```
+[WARNING]
+Currently GINQ can not work well when STC is enabled.
+
+=== GINQ Syntax
+
+GINQ consists of the following clauses, which must appear in this order:
```sql
GQ, i.e. abbreviation for GINQ
|__ from
@@ -39,7 +62,7 @@ GQ, i.e. abbreviation for GINQ
|__ [where]
| |__ <condition> ((&& | ||) <condition>)*
|__ [groupby]
-| |__ <expression> [as <alias>] (, <expression> [as <alias>])*
+| |__ <expression> [as <alias>] (, <expression> [as <alias>])* [into
<group_alias>]
| |__ [having]
| |__ <condition> ((&& | ||) <condition>)*
|__ [orderby]
@@ -53,7 +76,7 @@ GQ, i.e. abbreviation for GINQ
`[]` means the related clause is optional, `*` means zero or more times, and
`+` means one or more times. Also, the clauses of GINQ are order sensitive,
so the order of clauses should be kept as the above structure
-As we could see, the simplest GINQ consists of a `from` clause and a `select`
clause, which looks like:
+The simplest GINQ consists of a `from` clause and a `select` clause:
[source, sql]
----
include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_simplest,indent=0]
@@ -61,65 +84,6 @@
include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_simplest,inden
[NOTE]
__ONLY ONE__ `from` clause is required in GINQ. Also, GINQ supports multiple
data sources through `from` and the related joins.
-As a DSL, GINQ should be wrapped with the following block to be executed:
-```groovy
-GQ { /* GINQ CODE */ }
-```
-For example,
-[source, groovy]
-----
-include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_execution_01,indent=0]
-----
-
-[source, groovy]
-----
-include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_execution_02,indent=0]
-----
-And it is strongly recommended to use `def` to define the variable for the
result of GINQ execution,
-which is a `Queryable` instance that is lazy.
-```groovy
-def result = GQ {
- /* GINQ CODE */
-}
-def stream = result.stream() // get the stream from GINQ result
-def list = result.toList() // get the list from GINQ result
-```
-[WARNING]
-Currently GINQ can not work well when STC is enabled.
-
-Also, GINQ could be written in a method marked with `@GQ`:
-```groovy
-@GQ
-def someGinqMethod() {
- /* GINQ CODE */
-}
-```
-For example,
-
-* Mark the `ginq` method as a GINQ method with `@GQ` annotation:
-
-[source, groovy]
-----
-include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_method_01,indent=0]
-----
-
-* Specify the result type as `List`:
-
-[source, groovy]
-----
-include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_method_03,indent=0]
-----
-[NOTE]
-GINQ supports many result types, e.g. `List`, `Set`, `Collection`, `Iterable`,
`Iterator`, `java.util.stream.Stream` and array types.
-
-* Enable parallel querying:
-
-[source, groovy]
-----
-include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_method_02,indent=0]
-----
-
-=== GINQ Syntax
==== Data Source
The data source for GINQ could be specified by `from` clause, which is
equivalent to SQL's `FROM`.
Currently GINQ supports `Iterable`, `Stream`, array and GINQ result set as its
data source:
@@ -300,33 +264,56 @@ Only binary expressions(`==`, `&&`) are allowed in the
`on` clause of hash join
==== Grouping
`groupby` is equivalent to SQL's `GROUP BY`, and `having` is equivalent to
SQL's `HAVING`.
-Each field in any nonaggregate expression in the `select` clause **must** be
included in the `groupby` clause.
+The `into` clause binds the grouped result to a named variable, enabling direct
+method calls for key access and aggregates. The variable is a `GroupResult`
which
+extends `Queryable`, so all aggregate methods (`count()`, `sum()`, `toList()`,
etc.)
+are available as real method calls:
[source, sql]
----
-include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_grouping_01,indent=0]
+assert [[1, 2], [3, 2], [6, 3]] == GQ {
+ from n in [1, 1, 3, 3, 6, 6, 6]
+ groupby n into g
+ select g.key, g.count()
+}.toList()
----
+The group variable supports all `Queryable` aggregate methods:
[source, sql]
----
-include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_grouping_02,indent=0]
+assert [[1, 2], [3, 6], [6, 18]] == GQ {
+ from n in [1, 1, 3, 3, 6, 6, 6]
+ groupby n into g
+ select g.key, g.sum(n -> n)
+}.toList()
----
+For multi-key grouping, individual keys can be named with `as` and accessed
+as properties or via subscript:
[source, sql]
----
-include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_grouping_10,indent=0]
+def result = GQ {
+ from e in employees
+ groupby e.dept as department, e.role as role into g
+ select g.department, g.role, g.count()
+}.toList()
----
-The group columns could be renamed with `as` clause:
-[source, sql]
-----
-include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_grouping_08,indent=0]
-----
+Subscript access (`g['department']`) and explicit `g.get('department')` are
also supported.
+`having` works with `into` — the group variable can be used directly in the
condition:
[source, sql]
----
-include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_grouping_09,indent=0]
+assert [[6, 3]] == GQ {
+ from n in [1, 1, 3, 3, 6, 6, 6]
+ groupby n into g
+ having g.count() > 2
+ select g.key, g.count()
+}.toList()
----
+NOTE: The `where` clause after `groupby...into` is reserved for future use;
+use `having` for now.
+
===== Aggregate Functions
GINQ provides some built-in aggregate functions:
|===
@@ -549,40 +536,68 @@
include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_pagination_01,
include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_pagination_02,indent=0]
----
-==== Nested GINQ
-
-===== Nested GINQ in `from` clause
+=== Common Patterns
+==== Row Number
+`_rn` is the implicit variable representing row number for each record in the
result set. It starts with `0`
[source, sql]
----
-include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_nested_01,indent=0]
+include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_tips_05,indent=0]
----
-===== Nested GINQ in `where` clause
-[source, sql]
+==== List Comprehension
+List comprehension is an elegant way to define and create lists based on
existing lists:
+[source, groovy]
----
-include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_nested_02,indent=0]
+include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_tips_01,indent=0]
----
-[source, sql]
+[source, groovy]
----
-include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_filtering_04,indent=0]
+include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_tips_02,indent=0]
----
-===== Nested GINQ in `select` clause
[source, groovy]
----
-include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_nested_03,indent=0]
+include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_tips_06,indent=0]
----
[NOTE]
-It's recommended to use `limit 1` to restrict the count of sub-query result
-because `TooManyValuesException` will be thrown if more than one values
returned
+`GQL {...}` is the abbreviation of `GQ {...}.toList()`
-We could use `as` clause to name the sub-query result
+GINQ could be used as list comprehension in the loops directly:
[source, groovy]
----
-include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_nested_04,indent=0]
+include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_tips_03,indent=0]
+----
+
+==== Query JSON
+[source, groovy]
+----
+include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_tips_04,indent=0]
----
+==== Query & Update
+This is like `update` statement in SQL
+[source, groovy]
+----
+include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_tips_07,indent=0]
+----
+
+==== Alternative for `with` clause
+GINQ does not support `with` clause for now, but we could define a temporary
variable to workaround:
+[source, groovy]
+----
+include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_tips_12,indent=0]
+----
+
+==== Alternative for `case-when`
+`case-when` of SQL could be replaced with switch expression:
+[source, groovy]
+----
+include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_tips_13,indent=0]
+----
+
+=== Advanced Topics
+
==== Window Functions
Window can be defined by `partitionby`, `orderby`, `rows` and `range`:
@@ -938,64 +953,111 @@
include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_winfunction_38
include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_winfunction_42,indent=0]
----
-=== GINQ Tips
-==== Row Number
-`_rn` is the implicit variable representing row number for each record in the
result set. It starts with `0`
+==== Nested GINQ
+
+===== Nested GINQ in `from` clause
[source, sql]
----
-include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_tips_05,indent=0]
+include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_nested_01,indent=0]
----
-==== List Comprehension
-List comprehension is an elegant way to define and create lists based on
existing lists:
-[source, groovy]
+===== Nested GINQ in `where` clause
+[source, sql]
----
-include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_tips_01,indent=0]
+include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_nested_02,indent=0]
----
-[source, groovy]
+[source, sql]
----
-include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_tips_02,indent=0]
+include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_filtering_04,indent=0]
----
+===== Nested GINQ in `select` clause
[source, groovy]
----
-include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_tips_06,indent=0]
+include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_nested_03,indent=0]
----
[NOTE]
-`GQL {...}` is the abbreviation of `GQ {...}.toList()`
+It's recommended to use `limit 1` to restrict the count of sub-query result
+because `TooManyValuesException` will be thrown if more than one values
returned
-GINQ could be used as list comprehension in the loops directly:
+We could use `as` clause to name the sub-query result
[source, groovy]
----
-include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_tips_03,indent=0]
+include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_nested_04,indent=0]
----
-==== Query & Update
-This is like `update` statement in SQL
+==== Classic groupby style
+GINQ also supports an older style without the `into` keyword which looks
simpler
+for some cases but has some limitations — aggregate functions use a special
syntax
+rather than real method calls, and the group cannot be accessed as a
composable collection.
+Each field in any nonaggregate expression in the `select` clause **must** be
included
+in the `groupby` clause:
+[source, sql]
+----
+include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_grouping_01,indent=0]
+----
+
+[source, sql]
+----
+include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_grouping_02,indent=0]
+----
+
+[source, sql]
+----
+include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_grouping_10,indent=0]
+----
+
+The group columns could be renamed with `as` clause:
+[source, sql]
+----
+include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_grouping_08,indent=0]
+----
+
+[source, sql]
+----
+include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_grouping_09,indent=0]
+----
+
+==== Using the Queryable API directly
+The `groupByInto` method is also available on the `Queryable` API directly,
+which can be useful when building queries programmatically:
[source, groovy]
----
-include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_tips_07,indent=0]
+include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_groupby_into_api,indent=0]
----
-==== Alternative for `with` clause
-GINQ does not support `with` clause for now, but we could define a temporary
variable to workaround:
+==== `@GQ` Annotation
+GINQ could be written in a method marked with `@GQ`:
+```groovy
+@GQ
+def someGinqMethod() {
+ /* GINQ CODE */
+}
+```
+For example,
+
+* Mark the `ginq` method as a GINQ method with `@GQ` annotation:
+
[source, groovy]
----
-include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_tips_12,indent=0]
+include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_method_01,indent=0]
----
-==== Alternative for `case-when`
-`case-when` of SQL could be replaced with switch expression:
+* Specify the result type as `List`:
+
[source, groovy]
----
-include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_tips_13,indent=0]
+include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_method_03,indent=0]
----
+[NOTE]
+GINQ supports many result types, e.g. `List`, `Set`, `Collection`, `Iterable`,
`Iterator`, `java.util.stream.Stream` and array types.
+
+* Enable parallel querying:
-==== Query JSON
[source, groovy]
----
-include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_tips_04,indent=0]
+include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_method_02,indent=0]
----
==== Parallel Querying
diff --git
a/subprojects/groovy-ginq/src/spec/test/org/apache/groovy/ginq/GinqTest.groovy
b/subprojects/groovy-ginq/src/spec/test/org/apache/groovy/ginq/GinqTest.groovy
index 80aae79201..afdfeba38f 100644
---
a/subprojects/groovy-ginq/src/spec/test/org/apache/groovy/ginq/GinqTest.groovy
+++
b/subprojects/groovy-ginq/src/spec/test/org/apache/groovy/ginq/GinqTest.groovy
@@ -3469,6 +3469,92 @@ class GinqTest {
'''
}
+ // groupby...into tests
+
+ @Test
+ void "testGinq - from groupby into select - 1"() {
+ assertGinqScript '''
+ assert [[1, 2], [3, 2], [6, 3]] == GQ {
+ from n in [1, 1, 3, 3, 6, 6, 6]
+ groupby n into g
+ select g.key, g.count()
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from groupby into select - 2"() {
+ assertGinqScript '''
+ assert [[1, [1, 1]], [3, [3, 3]], [6, [6, 6, 6]]] == GQ {
+ from n in [1, 1, 3, 3, 6, 6, 6]
+ groupby n into g
+ select g.key, g.toList()
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from groupby into select - 3"() {
+ assertGinqScript '''
+ assert [[1, 2], [3, 6], [6, 18]] == GQ {
+ from n in [1, 1, 3, 3, 6, 6, 6]
+ groupby n into g
+ select g.key, g.sum(n -> n)
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from groupby into having select - 1"() {
+ assertGinqScript '''
+ assert [[6, 3]] == GQ {
+ from n in [1, 1, 3, 3, 6, 6, 6]
+ groupby n into g
+ having g.count() > 2
+ select g.key, g.count()
+ }.toList()
+ '''
+ }
+
+ @Test
+ void "testGinq - from groupby into select - multi-key with property
access"() {
+ assertGinqScript '''
+ def result = GQ {
+ from n in [[name: 'a', val: 1], [name: 'b', val: 2]]
+ groupby n.name as name, n.val as val into g
+ select g.name, g.val, g.count()
+ }.toList().collect { it.toList() }.sort()
+ assert result == [['a', 1, 1], ['b', 2, 1]]
+ '''
+ }
+
+ @Test
+ void "testGinq - from groupby into select - multi-key with subscript"() {
+ assertGinqScript '''
+ def result = GQ {
+ from n in [[name: 'a', val: 1], [name: 'b', val: 2]]
+ groupby n.name as name, n.val as val into g
+ select g["name"], g["val"], g.count()
+ }.toList().collect { it.toList() }.sort()
+ assert result == [['a', 1, 1], ['b', 2, 1]]
+ '''
+ }
+
+ @Test
+ void "testGinq - from groupby into select - direct API"() {
+ assertScript '''
+ import static
org.apache.groovy.ginq.provider.collection.runtime.Queryable.from
+// tag::ginq_groupby_into_api[]
+ def nums = [1, 2, 2, 3, 3, 4, 4, 5]
+ def result = from(nums)
+ .groupByInto(e -> e, g -> g.count() > 1)
+ .select((g, q) -> Tuple.tuple(g.key, g.count()))
+ .toList()
+ assert [[2, 2], [3, 2], [4, 2]] == result
+// end::ginq_groupby_into_api[]
+ '''
+ }
+
@Test
void "testGinq - query json - 1"() {
assertGinqScript """
diff --git
a/subprojects/groovy-ginq/src/test/groovy/org/apache/groovy/ginq/GinqErrorTest.groovy
b/subprojects/groovy-ginq/src/test/groovy/org/apache/groovy/ginq/GinqErrorTest.groovy
index 25ef0d7e2b..7b42a39591 100644
---
a/subprojects/groovy-ginq/src/test/groovy/org/apache/groovy/ginq/GinqErrorTest.groovy
+++
b/subprojects/groovy-ginq/src/test/groovy/org/apache/groovy/ginq/GinqErrorTest.groovy
@@ -361,6 +361,33 @@ final class GinqErrorTest {
assert err.message.toString().contains('`this.hello(n)` is not an
aggregate function @ line 4, column 27.')
}
+ @Test
+ void "testGinq - from groupby into where - not yet supported"() {
+ def err = shouldFail '''\
+ GQ {
+ from n in [1, 2, 3]
+ groupby n into g
+ where g.count() > 1
+ select g.key, g.count()
+ }.toList()
+ '''
+
+ assert err.message.toString().contains('`where` after `groupby...into`
is not yet supported')
+ }
+
+ @Test
+ void "testGinq - into without groupby"() {
+ def err = shouldFail '''\
+ GQ {
+ from n in [1, 2, 3]
+ into g
+ select g
+ }.toList()
+ '''
+
+ assert err.message.toString().contains('`into` is only supported after
`groupby`')
+ }
+
@Test
void "testGinq - subQuery - 13"() {
def err = shouldFail '''\
diff --git
a/subprojects/groovy-ginq/src/test/groovy/org/apache/groovy/ginq/provider/collection/runtime/QueryableCollectionTest.groovy
b/subprojects/groovy-ginq/src/test/groovy/org/apache/groovy/ginq/provider/collection/runtime/QueryableCollectionTest.groovy
index 017ccae6b5..e9256da436 100644
---
a/subprojects/groovy-ginq/src/test/groovy/org/apache/groovy/ginq/provider/collection/runtime/QueryableCollectionTest.groovy
+++
b/subprojects/groovy-ginq/src/test/groovy/org/apache/groovy/ginq/provider/collection/runtime/QueryableCollectionTest.groovy
@@ -875,6 +875,27 @@ class QueryableCollectionTest {
assert [null] == result
}
+ @Test
+ void testGroupByIntoSelect0() {
+ def nums = [1, 2, 2, 3, 3, 4, 4, 5]
+ def result = from(nums).groupByInto(e -> e).select((g, q) ->
Tuple.tuple(g.key, g.toList())).toList()
+ assert [[1, [1]], [2, [2, 2]], [3, [3, 3]], [4, [4, 4]], [5, [5]]] ==
result
+ }
+
+ @Test
+ void testGroupByIntoSelect1() {
+ def nums = [1, 2, 2, 3, 3, 4, 4, 5]
+ def result = from(nums).groupByInto(e -> e).select((g, q) ->
Tuple.tuple(g.key, g.count())).toList()
+ assert [[1, 1], [2, 2], [3, 2], [4, 2], [5, 1]] == result
+ }
+
+ @Test
+ void testGroupByIntoWithHaving() {
+ def nums = [1, 2, 2, 3, 3, 4, 4, 5]
+ def result = from(nums).groupByInto(e -> e, g -> g.count() >
1).select((g, q) -> Tuple.tuple(g.key, g.count())).toList()
+ assert [[2, 2], [3, 2], [4, 2]] == result
+ }
+
@Test
void testOrderBy() {
Person daniel = new Person('Daniel', 35)