This is an automated email from the ASF dual-hosted git repository.

paulk 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 28e473f0f6 GROOVY-11919: Provide set operators for GINQ, e.g. union, 
intersect
28e473f0f6 is described below

commit 28e473f0f6f403d935c63f24b9c5b68d485af632
Author: Paul King <[email protected]>
AuthorDate: Sat Apr 11 19:18:43 2026 +1000

    GROOVY-11919: Provide set operators for GINQ, e.g. union, intersect
---
 .../apache/groovy/ginq/dsl/GinqAstBaseVisitor.java |  8 ++
 .../org/apache/groovy/ginq/dsl/GinqAstBuilder.java | 65 +++++++++++++--
 .../org/apache/groovy/ginq/dsl/GinqAstVisitor.java |  2 +
 .../dsl/expression/SetOperationExpression.java     | 66 +++++++++++++++
 .../ginq/provider/collection/GinqAstWalker.groovy  | 18 +++++
 .../groovy-ginq/src/spec/doc/ginq-userguide.adoc   | 76 ++++++++++++-----
 .../test/org/apache/groovy/ginq/GinqTest.groovy    | 94 ++++++++++++++++++++++
 7 files changed, 301 insertions(+), 28 deletions(-)

diff --git 
a/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/GinqAstBaseVisitor.java
 
b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/GinqAstBaseVisitor.java
index 4f5ab27b65..2cad321b45 100644
--- 
a/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/GinqAstBaseVisitor.java
+++ 
b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/GinqAstBaseVisitor.java
@@ -28,6 +28,7 @@ import org.apache.groovy.ginq.dsl.expression.LimitExpression;
 import org.apache.groovy.ginq.dsl.expression.OnExpression;
 import org.apache.groovy.ginq.dsl.expression.OrderExpression;
 import org.apache.groovy.ginq.dsl.expression.SelectExpression;
+import org.apache.groovy.ginq.dsl.expression.SetOperationExpression;
 import org.apache.groovy.ginq.dsl.expression.ShutdownExpression;
 import org.apache.groovy.ginq.dsl.expression.WhereExpression;
 import org.codehaus.groovy.ast.CodeVisitorSupport;
@@ -128,6 +129,13 @@ public class GinqAstBaseVisitor extends CodeVisitorSupport 
implements GinqAstVis
         return null;
     }
 
+    @Override
+    public Void visitSetOperationExpression(SetOperationExpression 
setOperationExpression) {
+        setOperationExpression.getLeft().accept(this);
+        visit(setOperationExpression.getRight());
+        return null;
+    }
+
     @Override
     public Void visitShutdownExpression(ShutdownExpression shutdownExpression) 
{
         visit(shutdownExpression.getExpr());
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 74cfdf4301..a56feebf4b 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
@@ -31,6 +31,7 @@ import org.apache.groovy.ginq.dsl.expression.LimitExpression;
 import org.apache.groovy.ginq.dsl.expression.OnExpression;
 import org.apache.groovy.ginq.dsl.expression.OrderExpression;
 import org.apache.groovy.ginq.dsl.expression.SelectExpression;
+import org.apache.groovy.ginq.dsl.expression.SetOperationExpression;
 import org.apache.groovy.ginq.dsl.expression.ShutdownExpression;
 import org.apache.groovy.ginq.dsl.expression.WhereExpression;
 import org.codehaus.groovy.ast.ASTNode;
@@ -77,6 +78,8 @@ public class GinqAstBuilder extends CodeVisitorSupport 
implements SyntaxErrorRep
     }
 
     private final List<MethodCallExpression> ignoredMethodCallExpressionList = 
new ArrayList<>();
+    private AbstractGinqExpression setOperationLeft;
+    private String setOperationOp;
 
     private static final List<String> SHUTDOWN_OPTION_LIST = 
Arrays.asList("immediate", "abort");
     public AbstractGinqExpression buildAST(ASTNode astNode) {
@@ -110,20 +113,39 @@ public class GinqAstBuilder extends CodeVisitorSupport 
implements SyntaxErrorRep
         return getGinqExpression(astNode);
     }
 
-    private GinqExpression getGinqExpression(ASTNode astNode) {
+    private AbstractGinqExpression getGinqExpression(ASTNode astNode) {
         if (null == latestGinqExpression) {
             ASTNode node = ginqExpressionStack.isEmpty() ? astNode : 
ginqExpressionStack.peek();
-            this.collectSyntaxError(new GinqSyntaxError("`select` clause is 
missing",
-                    node.getLineNumber(), node.getColumnNumber()));
+            if (setOperationLeft != null) {
+                this.collectSyntaxError(new GinqSyntaxError(
+                        "Right-hand query is missing after `" + setOperationOp 
+ "`",
+                        node.getLineNumber(), node.getColumnNumber()));
+            } else {
+                this.collectSyntaxError(new GinqSyntaxError("`select` clause 
is missing",
+                        node.getLineNumber(), node.getColumnNumber()));
+            }
+            return setOperationLeft != null ? setOperationLeft : new 
GinqExpression();
         }
 
-        latestGinqExpression.visit(new GinqAstBaseVisitor() {
+        AbstractGinqExpression result;
+        if (setOperationLeft != null) {
+            latestGinqExpression.putNodeMetaData(ROOT_GINQ_EXPRESSION, 
latestGinqExpression);
+            SetOperationExpression setOpExpr = new 
SetOperationExpression(setOperationLeft, setOperationOp, latestGinqExpression);
+            setOpExpr.setSourcePosition(setOperationLeft);
+            result = setOpExpr;
+        } else {
+            latestGinqExpression.putNodeMetaData(ROOT_GINQ_EXPRESSION, 
latestGinqExpression);
+            result = latestGinqExpression;
+        }
+
+        GinqAstBaseVisitor cleanupVisitor = new GinqAstBaseVisitor() {
             @Override
             public void visitMethodCallExpression(MethodCallExpression call) {
                 ignoredMethodCallExpressionList.remove(call);
                 super.visitMethodCallExpression(call);
             }
-        });
+        };
+        result.accept(cleanupVisitor);
 
         if (!ignoredMethodCallExpressionList.isEmpty()) {
             MethodCallExpression methodCallExpression = 
ignoredMethodCallExpressionList.get(0);
@@ -131,9 +153,7 @@ public class GinqAstBuilder extends CodeVisitorSupport 
implements SyntaxErrorRep
                     methodCallExpression.getLineNumber(), 
methodCallExpression.getColumnNumber()));
         }
 
-        latestGinqExpression.putNodeMetaData(ROOT_GINQ_EXPRESSION, 
latestGinqExpression);
-
-        return latestGinqExpression;
+        return result;
     }
 
     private void setLatestGinqExpressionClause(AbstractGinqExpression 
ginqExpressionClause) {
@@ -420,6 +440,28 @@ public class GinqAstBuilder extends CodeVisitorSupport 
implements SyntaxErrorRep
 
     @Override
     public void visitVariableExpression(VariableExpression expression) {
+        if (SET_OP_SET.contains(expression.getText())) {
+            if (latestGinqExpression == null) {
+                this.collectSyntaxError(new GinqSyntaxError(
+                        "`" + expression.getText() + "` must follow a complete 
GINQ expression (from...select)",
+                        expression.getLineNumber(), 
expression.getColumnNumber()));
+            } else {
+                if (setOperationLeft != null) {
+                    // chaining: wrap previous left + op + current right into 
a SetOperationExpression
+                    latestGinqExpression.putNodeMetaData(ROOT_GINQ_EXPRESSION, 
latestGinqExpression);
+                    SetOperationExpression prev = new 
SetOperationExpression(setOperationLeft, setOperationOp, latestGinqExpression);
+                    prev.setSourcePosition(setOperationLeft);
+                    setOperationLeft = prev;
+                } else {
+                    latestGinqExpression.putNodeMetaData(ROOT_GINQ_EXPRESSION, 
latestGinqExpression);
+                    setOperationLeft = latestGinqExpression;
+                }
+                setOperationOp = expression.getText();
+                latestGinqExpression = null;
+            }
+            return;
+        }
+
         if (KEYWORD_SET.contains(expression.getText())) {
             this.collectSyntaxError(
                     new GinqSyntaxError(
@@ -519,12 +561,19 @@ public class GinqAstBuilder extends CodeVisitorSupport 
implements SyntaxErrorRep
     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_UNION = "union";
+    private static final String KW_UNIONALL = "unionall";
+    private static final String KW_INTERSECT = "intersect";
+    private static final String KW_MINUS = "minus";
     private static final String KW_SHUTDOWN = "shutdown";
     private static final Set<String> KEYWORD_SET;
+    private static final Set<String> SET_OP_SET = Collections.unmodifiableSet(
+            new HashSet<>(Arrays.asList(KW_UNION, KW_UNIONALL, KW_INTERSECT, 
KW_MINUS)));
     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_INTO, KW_SHUTDOWN));
+        keywordSet.addAll(SET_OP_SET);
         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/GinqAstVisitor.java
 
b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/GinqAstVisitor.java
index 24dccec980..fbbb52d639 100644
--- 
a/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/GinqAstVisitor.java
+++ 
b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/GinqAstVisitor.java
@@ -28,6 +28,7 @@ import org.apache.groovy.ginq.dsl.expression.LimitExpression;
 import org.apache.groovy.ginq.dsl.expression.OnExpression;
 import org.apache.groovy.ginq.dsl.expression.OrderExpression;
 import org.apache.groovy.ginq.dsl.expression.SelectExpression;
+import org.apache.groovy.ginq.dsl.expression.SetOperationExpression;
 import org.apache.groovy.ginq.dsl.expression.ShutdownExpression;
 import org.apache.groovy.ginq.dsl.expression.WhereExpression;
 
@@ -51,6 +52,7 @@ public interface GinqAstVisitor<R> {
     R visitOrderExpression(OrderExpression orderExpression);
     R visitLimitExpression(LimitExpression limitExpression);
     R visitSelectExpression(SelectExpression selectExpression);
+    R visitSetOperationExpression(SetOperationExpression 
setOperationExpression);
     R visitShutdownExpression(ShutdownExpression shutdownExpression);
     R visit(AbstractGinqExpression expression);
     default void setConfiguration(Map<String, String> configuration) {}
diff --git 
a/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/SetOperationExpression.java
 
b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/SetOperationExpression.java
new file mode 100644
index 0000000000..76bb5e96a9
--- /dev/null
+++ 
b/subprojects/groovy-ginq/src/main/groovy/org/apache/groovy/ginq/dsl/expression/SetOperationExpression.java
@@ -0,0 +1,66 @@
+/*
+ *  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.dsl.expression;
+
+import org.apache.groovy.ginq.dsl.GinqAstVisitor;
+
+/**
+ * Represents a set operation (union, unionall, intersect, minus) combining 
two GINQ expressions.
+ *
+ * @since 6.0.0
+ */
+public class SetOperationExpression extends AbstractGinqExpression {
+
+    private final AbstractGinqExpression left;
+    private final String operation;
+    private final GinqExpression right;
+
+    public SetOperationExpression(AbstractGinqExpression left, String 
operation, GinqExpression right) {
+        this.left = left;
+        this.operation = operation;
+        this.right = right;
+    }
+
+    public AbstractGinqExpression getLeft() {
+        return left;
+    }
+
+    public String getOperation() {
+        return operation;
+    }
+
+    public GinqExpression getRight() {
+        return right;
+    }
+
+    @Override
+    public <R> R accept(GinqAstVisitor<R> visitor) {
+        return visitor.visitSetOperationExpression(this);
+    }
+
+    @Override
+    public String getText() {
+        return left.getText() + " " + operation + " " + right.getText();
+    }
+
+    @Override
+    public String toString() {
+        return 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 3541bd7550..9d62d13f11 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
@@ -36,6 +36,7 @@ import org.apache.groovy.ginq.dsl.expression.LimitExpression
 import org.apache.groovy.ginq.dsl.expression.OnExpression
 import org.apache.groovy.ginq.dsl.expression.OrderExpression
 import org.apache.groovy.ginq.dsl.expression.SelectExpression
+import org.apache.groovy.ginq.dsl.expression.SetOperationExpression
 import org.apache.groovy.ginq.dsl.expression.ShutdownExpression
 import org.apache.groovy.ginq.dsl.expression.WhereExpression
 import org.apache.groovy.ginq.provider.collection.runtime.NamedRecord
@@ -870,6 +871,23 @@ class GinqAstWalker implements GinqAstVisitor<Expression>, 
SyntaxErrorReportable
         return selectMethodCallExpression
     }
 
+    @Override
+    Expression visitSetOperationExpression(SetOperationExpression 
setOperationExpression) {
+        Expression leftExpr = visit(setOperationExpression.left)
+        Expression rightExpr = 
visitGinqExpression(setOperationExpression.right)
+
+        String methodName
+        switch (setOperationExpression.operation) {
+            case 'union': methodName = 'union'; break
+            case 'unionall': methodName = 'unionAll'; break
+            case 'intersect': methodName = 'intersect'; break
+            case 'minus': methodName = 'minus'; break
+            default: throw new GroovyBugError("Unknown set operation: 
${setOperationExpression.operation}")
+        }
+
+        return callX(leftExpr, methodName, args(rightExpr))
+    }
+
     @Override
     Expression visitShutdownExpression(ShutdownExpression shutdownExpression) {
         return callX(classX(makeCached(QueryableHelper)), 'shutdown', 
constX(shutdownExpression.mode))
diff --git a/subprojects/groovy-ginq/src/spec/doc/ginq-userguide.adoc 
b/subprojects/groovy-ginq/src/spec/doc/ginq-userguide.adoc
index c3a9ec2273..802bc69a11 100644
--- a/subprojects/groovy-ginq/src/spec/doc/ginq-userguide.adoc
+++ b/subprojects/groovy-ginq/src/spec/doc/ginq-userguide.adoc
@@ -48,7 +48,9 @@ A GINQ expression can include the following clauses:
 * **`limit`** — restricts output to a given size, with optional offset for 
pagination.
 * **`select`** — the projection (required). Defines the output columns, with 
optional `as` aliases.
 
-GINQ also supports window functions, nested subqueries, and set operations 
(`union`, `intersect`, `minus`) — see <<Advanced Topics>> for details.
+* **`union`**, **`unionall`**, **`intersect`**, **`minus`** — set operations 
that combine two complete `from...select` expressions.
+
+GINQ also supports window functions and nested subqueries — see <<Advanced 
Topics>> for details.
 
 ****
 *How it works*: Under the covers GINQ transforms the query into calls within a 
fluent API. For example, the above query is transformed into:
@@ -555,6 +557,36 @@ 
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]
 ----
 
+=== Set Operations
+Set operations combine the results of two complete GINQ expressions.
+
+`union` returns distinct rows from both queries (duplicates removed):
+[source, groovy]
+----
+include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_setop_union,indent=0]
+----
+
+`unionall` returns all rows from both queries (duplicates preserved):
+[source, groovy]
+----
+include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_setop_unionall,indent=0]
+----
+
+`intersect` returns only rows that appear in both queries:
+[source, groovy]
+----
+include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_setop_intersect,indent=0]
+----
+
+`minus` returns rows from the first query that do not appear in the second:
+[source, groovy]
+----
+include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_setop_minus,indent=0]
+----
+
+Each side of a set operation is a complete `from...select` expression and can 
include
+`where`, `groupby`, `orderby`, `limit`, and any other clauses.
+
 == Common Recipes
 
 Recipes for everyday tasks: row numbering, list comprehensions, querying JSON,
@@ -1141,28 +1173,32 @@ 
include::../test/org/apache/groovy/ginq/GinqTest.groovy[tags=ginq_optimize_01,in
 The full GINQ clause structure for reference:
 ```sql
 GQ, i.e. abbreviation for GINQ
-|__ from
-|   |__ <data_source_alias> in <data_source>
-|__ [join/innerjoin/leftjoin/rightjoin/fulljoin/crossjoin]*
-|   |__ <data_source_alias> in <data_source>
-|   |__ on <condition> ((&& | ||) <condition>)* (NOTE: `crossjoin` does not 
need `on` clause)
-|__ [where]
-|   |__ <condition> ((&& | ||) <condition>)*
-|__ [groupby]
-|   |__ <expression> [as <alias>] (, <expression> [as <alias>])* [into 
<group_alias>]
-|   |__ [having]
-|       |__ <condition> ((&& | ||) <condition>)*
-|__ [orderby]
-|   |__ <expression> [in (asc|desc)] (, <expression> [in (asc|desc)])*
-|__ [limit]
-|   |__ [<offset>,] <size>
-|__ select
-    |__ <expression> [as <alias>] (, <expression> [as <alias>])*
+|__ <query>
+|   |__ from
+|   |   |__ <data_source_alias> in <data_source>
+|   |__ [join/innerjoin/leftjoin/rightjoin/fulljoin/crossjoin]*
+|   |   |__ <data_source_alias> in <data_source>
+|   |   |__ on <condition> ((&& | ||) <condition>)* (NOTE: `crossjoin` does 
not need `on` clause)
+|   |__ [where]
+|   |   |__ <condition> ((&& | ||) <condition>)*
+|   |__ [groupby]
+|   |   |__ <expression> [as <alias>] (, <expression> [as <alias>])* [into 
<group_alias>]
+|   |   |__ [having]
+|   |       |__ <condition> ((&& | ||) <condition>)*
+|   |__ [orderby]
+|   |   |__ <expression> [in (asc|desc)] (, <expression> [in (asc|desc)])*
+|   |__ [limit]
+|   |   |__ [<offset>,] <size>
+|   |__ select
+|       |__ <expression> [as <alias>] (, <expression> [as <alias>])*
+|__ [union/unionall/intersect/minus]*
+    |__ <query>
 ```
 [NOTE]
-`[]` 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,
+`[]` means the related clause is optional, `*` means zero or more times, and 
`+` means one or more times. Also, the clauses within each query are order 
sensitive,
 so the order of clauses should be kept as the above structure.
-__ONLY ONE__ `from` clause is required in GINQ. Multiple data sources are 
supported through `from` and the related joins.
+__ONLY ONE__ `from` clause is required per query. Multiple data sources are 
supported through `from` and the related joins.
+Set operations chain left-to-right, so `Q1 union Q2 minus Q3` is evaluated as 
`(Q1 union Q2) minus Q3`.
 
 === Known Limitations
 
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 d27bcd8e53..822da7659a 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
@@ -3845,6 +3845,100 @@ class GinqTest {
         '''
     }
 
+    // set operation tests
+
+    @Test
+    void "testGinq - union"() {
+        assertGinqScript '''
+// tag::ginq_setop_union[]
+            assert [1, 2, 3, 4, 5] == GQ {
+                from n in [1, 2, 3]
+                select n
+                union
+                from m in [3, 4, 5]
+                select m
+            }.toList()
+// end::ginq_setop_union[]
+        '''
+    }
+
+    @Test
+    void "testGinq - unionall"() {
+        assertGinqScript '''
+// tag::ginq_setop_unionall[]
+            assert [1, 2, 3, 3, 4, 5] == GQ {
+                from n in [1, 2, 3]
+                select n
+                unionall
+                from m in [3, 4, 5]
+                select m
+            }.toList()
+// end::ginq_setop_unionall[]
+        '''
+    }
+
+    @Test
+    void "testGinq - intersect"() {
+        assertGinqScript '''
+// tag::ginq_setop_intersect[]
+            assert [3, 4] == GQ {
+                from n in [1, 2, 3, 4]
+                select n
+                intersect
+                from m in [3, 4, 5, 6]
+                select m
+            }.toList()
+// end::ginq_setop_intersect[]
+        '''
+    }
+
+    @Test
+    void "testGinq - minus"() {
+        assertGinqScript '''
+// tag::ginq_setop_minus[]
+            assert [1, 2] == GQ {
+                from n in [1, 2, 3, 4]
+                select n
+                minus
+                from m in [3, 4, 5, 6]
+                select m
+            }.toList()
+// end::ginq_setop_minus[]
+        '''
+    }
+
+    @Test
+    void "testGinq - chained set operations"() {
+        assertGinqScript '''
+            // [1,2,3] union [3,4,5] = [1,2,3,4,5], then minus [4,5,6] = 
[1,2,3]
+            assert [1, 2, 3] == GQ {
+                from n in [1, 2, 3]
+                select n
+                union
+                from m in [3, 4, 5]
+                select m
+                minus
+                from k in [4, 5, 6]
+                select k
+            }.toList()
+        '''
+    }
+
+    @Test
+    void "testGinq - union with where"() {
+        assertGinqScript '''
+            assert [2, 4, 5] == GQ {
+                from n in [1, 2, 3]
+                where n % 2 == 0
+                select n
+                union
+                from m in [3, 4, 5]
+                where m > 3
+                select m
+            }.toList()
+        '''
+    }
+
     @Test
     void "testGinq - GINQ examples - 1"() {
         assertGinqScript '''

Reply via email to