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)


Reply via email to