From 23cae7c6c5ddab0bc59c3c0abb06658a04730960 Mon Sep 17 00:00:00 2001 From: Julian Hyde Date: Fri, 3 Dec 2021 13:02:52 -0800 Subject: [PATCH] [MOREL-69] Add 'compute' clause, for monoid comprehensions Fixes #69 --- README.md | 48 ++++++++--- docs/reference.md | 6 +- .../java/net/hydromatic/morel/ast/Ast.java | 80 ++++++++++++++++--- .../net/hydromatic/morel/ast/AstBuilder.java | 5 ++ .../java/net/hydromatic/morel/ast/Op.java | 1 + .../net/hydromatic/morel/ast/Shuttle.java | 9 ++- .../net/hydromatic/morel/ast/Visitor.java | 4 + .../hydromatic/morel/compile/Resolver.java | 41 +++++++--- .../morel/compile/TypeResolver.java | 58 +++++++++++--- src/main/javacc/MorelParser.jj | 5 ++ .../java/net/hydromatic/morel/MainTest.java | 40 ++++++++++ src/test/resources/script/relational.sml | 11 +++ src/test/resources/script/relational.sml.out | 17 ++++ 13 files changed, 277 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 8825ab56..46c4cfbd 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,7 @@ Implemented: `exception Option`, `datatype 'a option = NONE | SOME of 'a`, `getOpt`, `isSome`, `valOf`, `filter`, `join`, `app`, + `flatten`, `valOf`, `map`, `mapPartial`, `compose`, `composePartial` * [String](https://smlfamily.github.io/Basis/string.html): `eqtype char`, @@ -143,6 +144,13 @@ Implemented: `maxLen`, `fromList`, `tabulate`, `length`, `sub`, `update`, `concat`, `appi`, `app`, `mapi`, `map`, `foldli`, `foldri`, `foldl`, `foldr`, `findi`, `find`, `exists`, `all`, `collate` +* Non-basis built-ins: + * Interact: + `use` + * Relational: + `count`, `only`, `max`, `min`, `sum` + * System: + `env`, `plan`, `set`, `show`, `unset` Not implemented: * `type`, `eqtype`, `exception` @@ -203,18 +211,19 @@ In the relational extensions, `group` and `compute` expressions also use implicit labels. For instance, ``` from e in emps -group e.deptno compute sum of e.salary, count + group e.deptno compute sum of e.salary, count ``` is short-hand for ``` from e in emps -group deptno = e.deptno compute sum = sum of e.salary, count = count + group deptno = e.deptno compute sum = sum of e.salary, count = count ``` and both expressions have type `{count:int,deptno:int,sum:int} list`. ### Relational extensions -The `from` expression (and associated `in`, `where` and `yield` keywords) +The `from` expression (and associated `in`, `join`, `where`, +`group`, `compute`, `order` and `yield` keywords) is a language extension to support relational algebra. It iterates over a list and generates another list. @@ -262,7 +271,8 @@ You can iterate over more than one collection, and therefore generate a join or a cartesian product: ``` -from e in emps, d in depts +from e in emps, + d in depts where e.deptno = d.deptno yield {e.id, e.deptno, ename = e.name, dname = d.name}; ``` @@ -277,10 +287,10 @@ let | in_ e (h :: t) = e = h orelse (in_ e t) in from e in emps - where in_ e.deptno (from d in depts - where d.name = "Engineering" - yield d.deptno) - yield e.name + where in_ e.deptno (from d in depts + where d.name = "Engineering" + yield d.deptno) + yield e.name end; let @@ -288,10 +298,10 @@ let | exists (hd :: tl) = true in from e in emps - where exists (from d in depts - where d.deptno = e.deptno - andalso d.name = "Engineering") - yield e.name + where exists (from d in depts + where d.deptno = e.deptno + andalso d.name = "Engineering") + yield e.name end; ``` @@ -300,6 +310,20 @@ correlated (references the `e` variable from the enclosing query) and skips the `yield` clause (because it doesn't matter which columns the sub-query returns, just whether it returns any rows). +There are now built-in operators `elem` and `exists`, so you can write +``` +from e in emps + where e.deptno elem (from d in depts + where d.name = "Engineering" + yield d.deptno) + yield e.name; + +from e in emps + where exists (from d in depts + where d.deptno = e.deptno + andalso d.name = "Engineering"); +``` + ## More information * License: Apache License, Version 2.0 diff --git a/docs/reference.md b/docs/reference.md index babc2d14..88ecec9f 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -37,8 +37,8 @@ just because they take effort to build. Contributions are welcome! In Morel but not Standard ML: -* `from` expression with `where`, `group`, `order`, `yield` clauses - (and `compute`, `desc` sub-clauses) +* `from` expression with `join`, `where`, `group`, `compute`, `order`, `yield` + clauses * `union`, `except`, `intersect`, `elem`, `notElem` operators * "*lab* `=`" is optional in `exprow` @@ -152,6 +152,8 @@ In Standard ML but not in Morel: | group groupKey1 , ... , groupKeyg [ compute agg1 , ... , agga ] group clause (g ≥ 0, a ≥ 1) + | compute agg1 , ... , agga + compute clause (a > 1) | order orderItem1 , ... , orderItemo order clause (o ≥ 1) | yield exp diff --git a/src/main/java/net/hydromatic/morel/ast/Ast.java b/src/main/java/net/hydromatic/morel/ast/Ast.java index 8cc6f358..b0890fd8 100644 --- a/src/main/java/net/hydromatic/morel/ast/Ast.java +++ b/src/main/java/net/hydromatic/morel/ast/Ast.java @@ -98,6 +98,7 @@ AstWriter unparse(AstWriter w, int left, int right) { /** Literal pattern, the pattern analog of the {@link Literal} expression. * *

For example, "0" in "fun fact 0 = 1 | fact n = n * fact (n - 1)".*/ + @SuppressWarnings("rawtypes") public static class LiteralPat extends Pat { public final Comparable value; @@ -147,8 +148,7 @@ public static class WildcardPat extends Pat { } @Override public boolean equals(Object o) { - return o == this - || o instanceof WildcardPat; + return o instanceof WildcardPat; } public Pat accept(Shuttle shuttle) { @@ -764,6 +764,7 @@ AstWriter unparse(AstWriter w, int left, int right) { } /** Parse tree node of a literal (constant). */ + @SuppressWarnings("rawtypes") public static class Literal extends Exp { public final Comparable value; @@ -1476,10 +1477,24 @@ boolean record = true; record = nextFields.size() != 1; break; + case COMPUTE: + final Compute compute = (Compute) step; + final List aggregates2 = compute.aggregates; + + // The type of + // from emps as e compute c = sum of e3 + // is the same as the type of + // {a = e1, b = e2, c = sum (map (fn e => e3) [])} + nextFields.clear(); + aggregates2.forEach(aggregate -> nextFields.add(aggregate.id)); + fields = nextFields; + record = fields.size() != 1; + break; + case GROUP: final Group group = (Group) step; - final ImmutableList> groupExps = group.groupExps; - final ImmutableList aggregates = group.aggregates; + final List> groupExps = group.groupExps; + final List aggregates = group.aggregates; // The type of // from emps as e group by a = e1, b = e2 compute c = sum of e3 @@ -1547,6 +1562,14 @@ public From copy(List steps, @Nullable Exp implicitYieldExp) { ? this : ast.from(pos, steps, implicitYieldExp); } + + /** Returns whether this {@code from} expression ends with a {@code compute} + * step. If so, it is a monoid comprehension, not a monad + * comprehension, and its type is a scalar value (or record), not a list. */ + public boolean isCompute() { + return !steps.isEmpty() + && steps.get(steps.size() - 1).op == Op.COMPUTE; + } } /** A step in a {@code from} expression - {@code where}, {@code group} @@ -1734,16 +1757,52 @@ public enum Direction { DESC } + /** A {@code compute} or {@code group} clause in a {@code from} expression. */ + abstract static class AbstractCompute extends FromStep { + public final ImmutableList aggregates; + + AbstractCompute(Pos pos, Op op, ImmutableList aggregates) { + super(pos, op); + this.aggregates = aggregates; + } + + @Override AstWriter unparse(AstWriter w, int left, int right) { + Ord.forEach(aggregates, (aggregate, i) -> + w.append(i == 0 ? " compute " : ", ") + .append(aggregate, 0, 0)); + return w; + } + } + + /** A {@code compute} clause in a {@code from} expression. */ + public static class Compute extends AbstractCompute { + Compute(Pos pos, ImmutableList aggregates) { + super(pos, Op.COMPUTE, aggregates); + } + + @Override public AstNode accept(Shuttle shuttle) { + return shuttle.visit(this); + } + + @Override public void accept(Visitor visitor) { + visitor.visit(this); + } + + public Compute copy(List aggregates) { + return this.aggregates.equals(aggregates) + ? this + : ast.compute(pos, aggregates); + } + } + /** A {@code group} clause in a {@code from} expression. */ - public static class Group extends FromStep { + public static class Group extends AbstractCompute { public final ImmutableList> groupExps; - public final ImmutableList aggregates; Group(Pos pos, ImmutableList> groupExps, ImmutableList aggregates) { - super(pos, Op.GROUP); + super(pos, Op.GROUP, aggregates); this.groupExps = groupExps; - this.aggregates = aggregates; } @Override AstWriter unparse(AstWriter w, int left, int right) { @@ -1753,10 +1812,7 @@ public static class Group extends FromStep { .append(id, 0, 0) .append(" = ") .append(exp, 0, 0)); - Ord.forEach(aggregates, (aggregate, i) -> - w.append(i == 0 ? " compute " : ", ") - .append(aggregate, 0, 0)); - return w; + return super.unparse(w, 0, right); } @Override public AstNode accept(Shuttle shuttle) { diff --git a/src/main/java/net/hydromatic/morel/ast/AstBuilder.java b/src/main/java/net/hydromatic/morel/ast/AstBuilder.java index 328e8495..2d8ba54e 100644 --- a/src/main/java/net/hydromatic/morel/ast/AstBuilder.java +++ b/src/main/java/net/hydromatic/morel/ast/AstBuilder.java @@ -133,6 +133,7 @@ public Ast.Pat idPat(Pos pos, String name) { } } + @SuppressWarnings("rawtypes") public Ast.LiteralPat literalPat(Pos pos, Op op, Comparable value) { return new Ast.LiteralPat(pos, op, value); } @@ -450,6 +451,10 @@ public Ast.OrderItem orderItem(Pos pos, Ast.Exp exp, Ast.Direction direction) { return new Ast.OrderItem(pos, exp, direction); } + public Ast.Compute compute(Pos pos, List aggregates) { + return new Ast.Compute(pos, ImmutableList.copyOf(aggregates)); + } + public Ast.Group group(Pos pos, List> groupExps, List aggregates) { return new Ast.Group(pos, ImmutableList.copyOf(groupExps), diff --git a/src/main/java/net/hydromatic/morel/ast/Op.java b/src/main/java/net/hydromatic/morel/ast/Op.java index 01b599de..008a3c7a 100644 --- a/src/main/java/net/hydromatic/morel/ast/Op.java +++ b/src/main/java/net/hydromatic/morel/ast/Op.java @@ -127,6 +127,7 @@ public enum Op { INNER_JOIN(" join "), WHERE, GROUP, + COMPUTE, ORDER, ORDER_ITEM, YIELD, diff --git a/src/main/java/net/hydromatic/morel/ast/Shuttle.java b/src/main/java/net/hydromatic/morel/ast/Shuttle.java index 0223ecbc..65a6827e 100644 --- a/src/main/java/net/hydromatic/morel/ast/Shuttle.java +++ b/src/main/java/net/hydromatic/morel/ast/Shuttle.java @@ -41,6 +41,7 @@ public Shuttle(TypeSystem typeSystem) { protected List visitList(List nodes) { final List list = new ArrayList<>(); for (E node : nodes) { + //noinspection unchecked list.add((E) node.accept(this)); } return list; @@ -48,6 +49,7 @@ protected List visitList(List nodes) { protected Map visitMap(Map nodes) { final Map map = new LinkedHashMap<>(); + //noinspection unchecked nodes.forEach((k, v) -> map.put(k, (E) v.accept(this))); return map; } @@ -55,6 +57,7 @@ protected Map visitMap(Map nodes) { protected SortedMap visitSortedMap( SortedMap nodes) { final SortedMap map = new TreeMap<>(nodes.comparator()); + //noinspection unchecked nodes.forEach((k, v) -> map.put(k, (E) v.accept(this))); return map; } @@ -235,6 +238,10 @@ protected AstNode visit(Ast.Yield yield) { return ast.yield(yield.pos, yield.exp.accept(this)); } + protected AstNode visit(Ast.Compute compute) { + return ast.compute(compute.pos, compute.aggregates); + } + protected AstNode visit(Ast.Group group) { return ast.group(group.pos, group.groupExps, group.aggregates); } @@ -349,7 +356,7 @@ protected Core.Pat visit(Core.RecordPat recordPat) { } protected Core.Exp visit(Core.Fn fn) { - return fn.copy((Core.IdPat) fn.idPat.accept(this), fn.exp.accept(this)); + return fn.copy(fn.idPat.accept(this), fn.exp.accept(this)); } protected Core.Exp visit(Core.Case caseOf) { diff --git a/src/main/java/net/hydromatic/morel/ast/Visitor.java b/src/main/java/net/hydromatic/morel/ast/Visitor.java index 3fd74a5e..971230d3 100644 --- a/src/main/java/net/hydromatic/morel/ast/Visitor.java +++ b/src/main/java/net/hydromatic/morel/ast/Visitor.java @@ -205,6 +205,10 @@ protected void visit(Ast.Yield yield) { yield.exp.accept(this); } + protected void visit(Ast.Compute compute) { + compute.aggregates.forEach(this::accept); + } + protected void visit(Ast.Group group) { group.groupExps.forEach(p -> { p.left.accept(this); diff --git a/src/main/java/net/hydromatic/morel/compile/Resolver.java b/src/main/java/net/hydromatic/morel/compile/Resolver.java index fe8ef5b4..68405db2 100644 --- a/src/main/java/net/hydromatic/morel/compile/Resolver.java +++ b/src/main/java/net/hydromatic/morel/compile/Resolver.java @@ -393,6 +393,7 @@ private Core.Exp flattenLet(List decls, Ast.Exp exp) { return resolvedDecl.toExp(e2); } + @SuppressWarnings("SwitchStatementWithTooFewBranches") private void flatten(Map matches, Ast.Pat pat, Ast.Exp exp) { switch (pat.op) { @@ -497,11 +498,21 @@ private Core.Match toCore(Ast.Match match) { return core.match(pat, exp); } - Core.From toCore(Ast.From from) { + Core.Exp toCore(Ast.From from) { final List bindings = new ArrayList<>(); - final ListType type = (ListType) typeMap.getType(from); - return fromStepToCore(bindings, type, from.steps, - ImmutableList.of()); + final Type type = typeMap.getType(from); + if (from.isCompute()) { + final ListType listType = typeMap.typeSystem.listType(type); + final Core.From coreFrom = + fromStepToCore(bindings, listType, from.steps, + ImmutableList.of()); + return core.apply(type, + core.functionLiteral(typeMap.typeSystem, BuiltIn.RELATIONAL_ONLY), + coreFrom); + } else { + return fromStepToCore(bindings, (ListType) type, from.steps, + ImmutableList.of()); + } } /** Returns a list with one element appended. @@ -560,15 +571,27 @@ private Core.From fromStepToCore(List bindings, ListType type, Util.skip(steps), append(coreSteps, coreOrder)); case GROUP: - final Ast.Group group = (Ast.Group) step; + case COMPUTE: final ImmutableSortedMap.Builder groupExps = ImmutableSortedMap.naturalOrder(); final ImmutableSortedMap.Builder aggregates = ImmutableSortedMap.naturalOrder(); - Pair.forEach(group.groupExps, (id, exp) -> - groupExps.put(toCorePat(id), r.toCore(exp))); - group.aggregates.forEach(aggregate -> - aggregates.put(toCorePat(aggregate.id), r.toCore(aggregate))); + switch (step.op) { + case GROUP: + final Ast.Group group = (Ast.Group) step; + Pair.forEach(group.groupExps, (id, exp) -> + groupExps.put(toCorePat(id), r.toCore(exp))); + group.aggregates.forEach(aggregate -> + aggregates.put(toCorePat(aggregate.id), r.toCore(aggregate))); + break; + + case COMPUTE: + default: + final Ast.Compute compute = (Ast.Compute) step; + compute.aggregates.forEach(aggregate -> + aggregates.put(toCorePat(aggregate.id), r.toCore(aggregate))); + break; + } final Core.Group coreGroup = core.group(groupExps.build(), aggregates.build()); return fromStepToCore(coreGroup.bindings, type, diff --git a/src/main/java/net/hydromatic/morel/compile/TypeResolver.java b/src/main/java/net/hydromatic/morel/compile/TypeResolver.java index f1dc2269..424890a7 100644 --- a/src/main/java/net/hydromatic/morel/compile/TypeResolver.java +++ b/src/main/java/net/hydromatic/morel/compile/TypeResolver.java @@ -122,6 +122,7 @@ private Resolved deduceType_(Environment env, Ast.Decl decl) { final Map termMap = new LinkedHashMap<>(); final Ast.Decl node2 = deduceDeclType(typeEnv, decl, termMap); final boolean debug = false; + //noinspection ConstantConditions final Unifier.Tracer tracer = debug ? Tracers.printTracer(System.out) : Tracers.nullTracer(); @@ -281,9 +282,13 @@ private Ast.Exp deduceType(TypeEnv env, Ast.Exp node, Unifier.Variable v) { env2 = env; final Map fieldVars = new LinkedHashMap<>(); final List fromSteps = new ArrayList<>(); - for (Ast.FromStep step : from.steps) { + for (Ord step : Ord.zip(from.steps)) { Pair p = - deduceStepType(env, step, v3, env2, fieldVars, fromSteps); + deduceStepType(env, step.e, v3, env2, fieldVars, fromSteps); + if (step.e.op == Op.COMPUTE + && step.i != from.steps.size() - 1) { + throw new AssertionError("'compute' step must be last in 'from'"); + } env2 = p.left; v3 = p.right; } @@ -298,7 +303,9 @@ private Ast.Exp deduceType(TypeEnv env, Ast.Exp node, Unifier.Variable v) { final Ast.From from2 = from.copy(fromSteps, from.implicitYieldExp != null ? yieldExp2 : null); - return reg(from2, v, unifier.apply(LIST_TY_CON, v3)); + final Unifier.Term term1 = + from.isCompute() ? v3 : unifier.apply(LIST_TY_CON, v3); + return reg(from2, v, term1); case ID: final Ast.Id id = (Ast.Id) node; @@ -466,14 +473,29 @@ private Pair deduceStepType(TypeEnv env, return Pair.of(env2, v); case GROUP: - final Ast.Group group = (Ast.Group) step; - validateGroup(group); + case COMPUTE: + final List> astGroupExps; + final List astAggregates; + switch (step.op) { + case GROUP: + final Ast.Group group = (Ast.Group) step; + astGroupExps = group.groupExps; + astAggregates = group.aggregates; + break; + + case COMPUTE: + default: + final Ast.Compute compute = (Ast.Compute) step; + astGroupExps = ImmutableList.of(); + astAggregates = compute.aggregates; + } + validateGroup(astGroupExps, astAggregates); TypeEnv env3 = env; final Map inFieldVars = ImmutableMap.copyOf(fieldVars); fieldVars.clear(); final List> groupExps = new ArrayList<>(); - for (Pair groupExp : group.groupExps) { + for (Pair groupExp : astGroupExps) { final Ast.Id id = groupExp.getKey(); final Ast.Exp exp = groupExp.getValue(); final Unifier.Variable v7 = unifier.variable(); @@ -484,7 +506,7 @@ private Pair deduceStepType(TypeEnv env, groupExps.add(Pair.of(id, exp2)); } final List aggregates = new ArrayList<>(); - for (Ast.Aggregate aggregate : group.aggregates) { + for (Ast.Aggregate aggregate : astAggregates) { final Ast.Id id = aggregate.id; final Unifier.Variable v8 = unifier.variable(); reg(id, null, v8); @@ -512,7 +534,17 @@ private Pair deduceStepType(TypeEnv env, aggregates.add(aggregate2); reg(aggregate2, null, v8); } - fromSteps.add(group.copy(groupExps, aggregates)); + switch (step.op) { + case GROUP: + final Ast.Group group = (Ast.Group) step; + fromSteps.add(group.copy(groupExps, aggregates)); + break; + + case COMPUTE: + default: + final Ast.Compute compute = (Ast.Compute) step; + fromSteps.add(compute.copy(aggregates)); + } return Pair.of(env3, v); default: @@ -522,10 +554,11 @@ private Pair deduceStepType(TypeEnv env, /** Validates a {@code Group}. Throws if there are duplicate names among * the keys and aggregates. */ - private void validateGroup(Ast.Group group) { + private void validateGroup(List> groupExps, + List aggregates) { final List names = new ArrayList<>(); - group.groupExps.forEach(pair -> names.add(pair.left.name)); - group.aggregates.forEach(aggregate -> names.add(aggregate.id.name)); + groupExps.forEach(pair -> names.add(pair.left.name)); + aggregates.forEach(aggregate -> names.add(aggregate.id.name)); int duplicate = Util.firstDuplicate(names); if (duplicate >= 0) { throw new RuntimeException("Duplicate field name '" @@ -842,6 +875,7 @@ private static Ast.Exp idTuple(List vars) { } /** Converts a list of patterns to a singleton pattern or tuple pattern. */ + @SuppressWarnings("SwitchStatementWithTooFewBranches") private Ast.Pat patTuple(TypeEnv env, List patList) { final List list2 = new ArrayList<>(); for (int i = 0; i < patList.size(); i++) { @@ -1097,7 +1131,7 @@ private Unifier.Term toTerm(Type type, Subst subst) { .map(type1 -> toTerm(type1, subst)).collect(toImmutableList())); case RECORD_TYPE: final RecordType recordType = (RecordType) type; - //noinspection unchecked + @SuppressWarnings({"rawtypes", "unchecked"}) final NavigableSet labelNames = (NavigableSet) recordType.argNameTypes.keySet(); final String result; diff --git a/src/main/javacc/MorelParser.jj b/src/main/javacc/MorelParser.jj index 1f2f1259..6af86a1a 100644 --- a/src/main/javacc/MorelParser.jj +++ b/src/main/javacc/MorelParser.jj @@ -385,6 +385,11 @@ void fromStep(List steps) : { steps.add(ast.group(span.end(this), groupExps, aggregates)); } +| + { span = Span.of(getPos()); } aggregates = aggregateCommaList() + { + steps.add(ast.compute(span.end(this), aggregates)); + } | { span = Span.of(getPos()); } orderItems = orderItemCommaList() { steps.add(ast.order(span.end(this), orderItems)); diff --git a/src/test/java/net/hydromatic/morel/MainTest.java b/src/test/java/net/hydromatic/morel/MainTest.java index 1dd64c51..e1d179d6 100644 --- a/src/test/java/net/hydromatic/morel/MainTest.java +++ b/src/test/java/net/hydromatic/morel/MainTest.java @@ -19,6 +19,7 @@ package net.hydromatic.morel; import net.hydromatic.morel.ast.Ast; +import net.hydromatic.morel.parse.ParseException; import net.hydromatic.morel.type.TypeVar; import com.google.common.collect.ImmutableList; @@ -1713,6 +1714,45 @@ public class MainTest { is("Duplicate field name 'c' in group"))); } + /** Tests query with 'compute' without 'group'. Such a query does not return + * a collection, but returns the value of the aggregate function. Technically, + * it is a monoid comprehension, and an aggregate function is a monoid. */ + @Test void testCompute() { + ml("from i in [1, 2, 3] compute sum of i") + .assertParse("from i in [1, 2, 3] compute sum = sum of i") + .assertType("int") + .assertEval(is(6)); + ml("from i in [1, 2, 3] compute sum of i, count") + .assertParse("from i in [1, 2, 3] " + + "compute sum = sum of i, count = count") + .assertType("{count:int, sum:int}"); + // there must be at least one aggregate function + ml("from i in [1, 2, 3] compute") + .assertParseThrows( + throwsA(ParseException.class, + startsWith("Encountered \"\" at "))); + + // Theoretically a "group" without a "compute" can be followed by a + // "compute" step. So, the following is ambiguous. We treat it as a single + // "group ... compute" step. Under the two-step interpretation, the type + // would have been "int". + ml("from (i, j) in [(1, 1), (2, 3), (3, 4)]\n" + + " group j = i mod 2\n" + + " compute sum of j") + .assertType("{j:int, sum:int} list") + .assertEval(is(list(list(1, 5), list(0, 3)))); + + // "compute" must not be followed by other steps + ml("from i in [1, 2, 3] compute s = sum of i yield s + 2") + .assertTypeThrows( + throwsA(AssertionError.class, + is("'compute' step must be last in 'from'"))); + // similar, but valid + ml("(from i in [1, 2, 3] compute s = sum of i) + 2") + .assertType(is("int")) + .assertEval(is(8)); + } + @Test void testGroupYield() { final String ml = "from r in [{a=2,b=3}]\n" + "group r.a compute sb = sum of r.b\n" diff --git a/src/test/resources/script/relational.sml b/src/test/resources/script/relational.sml index 45c92dd3..a9f77d33 100644 --- a/src/test/resources/script/relational.sml +++ b/src/test/resources/script/relational.sml @@ -694,6 +694,17 @@ from e in emps dname = (only (from d in depts where d.deptno = e.deptno)).name}; +(*) Single-row aggregates using 'only' function and 'compute' clause +from e in emps + group compute si = sum of e.id, c = count, si1 = sum of e.id + 1; +(*) similar to previous, but the 'only' function converts a singleton set to a record +only ( + from e in emps + group compute si = sum of e.id, c = count, si1 = sum of e.id + 1); +(*) equivalent to previous, using group-less 'compute' rather than 'only' +from e in emps + compute si = sum of e.id, c = count, si1 = sum of e.id + 1; + (*) Empty from from; diff --git a/src/test/resources/script/relational.sml.out b/src/test/resources/script/relational.sml.out index 87f3ef70..ea3d3087 100644 --- a/src/test/resources/script/relational.sml.out +++ b/src/test/resources/script/relational.sml.out @@ -1086,6 +1086,23 @@ val it = : {dname:string, ename:string} list +(*) Single-row aggregates using 'only' function and 'compute' clause +from e in emps + group compute si = sum of e.id, c = count, si1 = sum of e.id + 1; +val it = [{c=4,si=406,si1=410}] : {c:int, si:int, si1:int} list + +(*) similar to previous, but the 'only' function converts a singleton set to a record +only ( + from e in emps + group compute si = sum of e.id, c = count, si1 = sum of e.id + 1); +val it = {c=4,si=406,si1=410} : {c:int, si:int, si1:int} + +(*) equivalent to previous, using group-less 'compute' rather than 'only' +from e in emps + compute si = sum of e.id, c = count, si1 = sum of e.id + 1; +val it = {c=4,si=406,si1=410} : {c:int, si:int, si1:int} + + (*) Empty from from; val it = [()] : unit list