From 2620a6d16bb51b2c9bb07b05618ac22995d1efa3 Mon Sep 17 00:00:00 2001 From: dbulahov Date: Mon, 9 Nov 2026 10:12:44 +0300 Subject: [PATCH] ccc, cccx: the power operator ^, as in SSAS Analysis Services reads ^ as the power operator; both grammars read it as an INVALID token. Both now have the token CARET and a power level between the sign and the operand: ^ binds more tightly than * / and a leading sign, groups from right to left, and its exponent may carry a sign (2 ^ 3 ^ 2 is 2 ^ (3 ^ 2), -2 ^ 2 is -(2 ^ 2), 2 ^ -1 is read). The parsed form is an infix call of ^, as * is; the unparser already wrote ^ back. Tests: parser.tck PowerOperatorTest (a power in a calculated member and in the slicer, grouping, precedence over * and /, the leading sign, a signed and a fractional exponent, NULL ^ 2); unparser.simple SimpleUnparserPowerTest writes the same models back. TCK green on ccc and cccx (218 tests), rolap green against this build. Signed-off-by: dbulahov --- parser.ccc/src/main/ccc/Grammer.ccc | 31 +++- parser.cccx/src/main/ccc/Expressions.inc.ccc | 11 ++ parser.cccx/src/main/ccc/Lexer.inc.ccc | 1 + .../mdx/parser/tck/PowerOperatorTest.java | 162 ++++++++++++++++++ .../simple/SimpleUnparserPowerTest.java | 121 +++++++++++++ 5 files changed, 322 insertions(+), 4 deletions(-) create mode 100644 parser.tck/src/main/java/org/eclipse/daanse/mdx/parser/tck/PowerOperatorTest.java create mode 100644 unparser.simple/src/test/java/org/eclipse/daanse/mdx/unparser/simple/SimpleUnparserPowerTest.java diff --git a/parser.ccc/src/main/ccc/Grammer.ccc b/parser.ccc/src/main/ccc/Grammer.ccc index 4253ed2..51da534 100644 --- a/parser.ccc/src/main/ccc/Grammer.ccc +++ b/parser.ccc/src/main/ccc/Grammer.ccc @@ -346,6 +346,7 @@ TOKEN : TOKEN : < ASTERISK : "*" > | < BANG : "!" > +| < CARET : "^" > | < COLON : ":" > | < CONCAT : "||" > | < EQ : "=" > @@ -1372,22 +1373,22 @@ MdxExpression parseFactor : { MdxExpression p; } - p = parsePrimary + p = parsePower { return p; } -| < PLUS > p = parsePrimary +| < PLUS > p = parsePower { return p; } -| < MINUS > p = parsePrimary +| < MINUS > p = parsePower { return new CallExpressionR(new PrefixOperationAtom("-"), List.of( p ) ); } -| < EXISTING > p = parsePrimary +| < EXISTING > p = parsePower { return new CallExpressionR(new PrefixOperationAtom("Existing"), List.of( p @@ -1397,6 +1398,28 @@ MdxExpression parseFactor : ; +// power: binds more tightly than * / and a leading sign, groups from right to left, the exponent may carry a sign (SSAS) +MdxExpression parsePower : +{ + MdxExpression x; + MdxExpression y; +} + x = parsePrimary + [ + < CARET > y = parseFactor + { + x = new CallExpressionR(new InfixOperationAtom("^"), List.of( + x, y + ) + ); + } + ] + { + return x; + } +; + + MdxExpression parsePrimary : { MdxExpression expression; diff --git a/parser.cccx/src/main/ccc/Expressions.inc.ccc b/parser.cccx/src/main/ccc/Expressions.inc.ccc index 605aa25..b51b199 100644 --- a/parser.cccx/src/main/ccc/Expressions.inc.ccc +++ b/parser.cccx/src/main/ccc/Expressions.inc.ccc @@ -330,7 +330,18 @@ UnaryExpression#(>1) : // This is only in Pentaho, it seems. )? + PowerExpression +; + +// power: binds more tightly than * / and a leading sign, groups from right to left, the exponent may carry a sign (SSAS) +PowerExpression#void : AliasedPrimary + [ + ( + + UnaryExpression + )#BinaryExpression(+1) + ] ; // inline set alias: names this operand, binds more tightly than * + - : (SSAS); not inside a Cast operand diff --git a/parser.cccx/src/main/ccc/Lexer.inc.ccc b/parser.cccx/src/main/ccc/Lexer.inc.ccc index acff685..f50a9b0 100644 --- a/parser.cccx/src/main/ccc/Lexer.inc.ccc +++ b/parser.cccx/src/main/ccc/Lexer.inc.ccc @@ -246,6 +246,7 @@ TOKEN #ReservedWord : TOKEN #Operator : < ASTERISK : "*" > | < BANG : "!" > +| < CARET : "^" > | < COLON : ":" > | < CONCAT : "||" > | < EQ : "=" > diff --git a/parser.tck/src/main/java/org/eclipse/daanse/mdx/parser/tck/PowerOperatorTest.java b/parser.tck/src/main/java/org/eclipse/daanse/mdx/parser/tck/PowerOperatorTest.java new file mode 100644 index 0000000..50f4bb0 --- /dev/null +++ b/parser.tck/src/main/java/org/eclipse/daanse/mdx/parser/tck/PowerOperatorTest.java @@ -0,0 +1,162 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* dbulahov - initial +*/ +package org.eclipse.daanse.mdx.parser.tck; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.daanse.mdx.parser.tck.CubeTest.propertyWords; + +import java.math.BigDecimal; + +import org.eclipse.daanse.mdx.model.api.MdxStatement; +import org.eclipse.daanse.mdx.model.api.SelectStatement; +import org.eclipse.daanse.mdx.model.api.expression.CallExpression; +import org.eclipse.daanse.mdx.model.api.expression.CompoundId; +import org.eclipse.daanse.mdx.model.api.expression.MdxExpression; +import org.eclipse.daanse.mdx.model.api.expression.NameObjectIdentifier; +import org.eclipse.daanse.mdx.model.api.expression.NullLiteral; +import org.eclipse.daanse.mdx.model.api.expression.NumericLiteral; +import org.eclipse.daanse.mdx.model.api.select.CreateMemberBodyClause; +import org.eclipse.daanse.mdx.model.api.select.SelectSlicerAxisClause; +import org.eclipse.daanse.mdx.parser.api.MdxParserException; +import org.eclipse.daanse.mdx.parser.api.MdxParserProvider; +import org.junit.jupiter.api.Test; +import org.osgi.service.component.annotations.RequireServiceComponentRuntime; +import org.osgi.test.common.annotation.InjectService; + +/** + * Analysis Services reads {@code ^} as the power operator: {@code 2 ^ 3} is 8. The parsed form is an infix call of the + * operator {@code ^}, as {@code *} is. + */ +@RequireServiceComponentRuntime +class PowerOperatorTest { + + @Test + void aPowerInACalculatedMember(@InjectService MdxParserProvider mdxParserProvider) throws MdxParserException { + SelectStatement select = select(mdxParserProvider, + "WITH MEMBER [Measures].[A] AS 2 ^ 3 SELECT {[Measures].[A]} ON 0 FROM [Sales]"); + CreateMemberBodyClause member = (CreateMemberBodyClause) select.selectWithClauses().get(0); + + CallExpression power = call(member.expression(), "^"); + number(arg(power, 0), 2); + number(arg(power, 1), 3); + } + + @Test + void aPowerInTheSlicer(@InjectService MdxParserProvider mdxParserProvider) throws MdxParserException { + SelectStatement select = select(mdxParserProvider, + "SELECT {[Measures].[A]} ON 0 FROM [Sales] WHERE ([Measures].[Sales] / 1000) ^ 2"); + assertThat(select.selectSlicerAxisClause()).isPresent(); + SelectSlicerAxisClause slicer = select.selectSlicerAxisClause().get(); + + CallExpression power = call(slicer.expression(), "^"); + CallExpression parentheses = call(arg(power, 0), "()"); + CallExpression division = call(arg(parentheses, 0), "/"); + name(arg(division, 0), "Sales"); + number(arg(division, 1), 1000); + number(arg(power, 1), 2); + } + + @Test + void thePowerGroupsFromRightToLeft(@InjectService MdxParserProvider provider) throws MdxParserException { + CallExpression outer = call(parse(provider, "2 ^ 3 ^ 2"), "^"); + number(arg(outer, 0), 2); + CallExpression inner = call(arg(outer, 1), "^"); + number(arg(inner, 0), 3); + number(arg(inner, 1), 2); + } + + @Test + void thePowerBindsMoreTightlyThanMultiplicationAndDivision(@InjectService MdxParserProvider provider) + throws MdxParserException { + CallExpression times = call(parse(provider, "2 ^ 2 * 3"), "*"); + power(arg(times, 0), 2, 2); + number(arg(times, 1), 3); + + CallExpression timesRight = call(parse(provider, "2 * 3 ^ 2"), "*"); + number(arg(timesRight, 0), 2); + power(arg(timesRight, 1), 3, 2); + + CallExpression division = call(parse(provider, "12 / 2 ^ 2"), "/"); + number(arg(division, 0), 12); + power(arg(division, 1), 2, 2); + } + + @Test + void theBaseMayBeNull(@InjectService MdxParserProvider provider) throws MdxParserException { + CallExpression power = call(parse(provider, "NULL ^ 2"), "^"); + assertThat(arg(power, 0)).isInstanceOf(NullLiteral.class); + number(arg(power, 1), 2); + } + + @Test + void theLeadingSignAppliesToThePower(@InjectService MdxParserProvider provider) throws MdxParserException { + CallExpression minus = call(parse(provider, "-2 ^ 2"), "-"); + assertThat(minus.expressions()).hasSize(1); + CallExpression power = call(arg(minus, 0), "^"); + number(arg(power, 0), 2); + number(arg(power, 1), 2); + } + + @Test + void theExponentMayCarryASign(@InjectService MdxParserProvider provider) throws MdxParserException { + CallExpression power = call(parse(provider, "2 ^ -1"), "^"); + number(arg(power, 0), 2); + CallExpression minus = call(arg(power, 1), "-"); + assertThat(minus.expressions()).hasSize(1); + number(arg(minus, 0), 1); + + CallExpression root = call(parse(provider, "2 ^ 0.5"), "^"); + assertThat(arg(root, 1)).isInstanceOf(NumericLiteral.class); + assertThat(((NumericLiteral) arg(root, 1)).value()).isEqualByComparingTo(new BigDecimal("0.5")); + } + + private static MdxExpression parse(MdxParserProvider provider, String mdx) throws MdxParserException { + return provider.newParser(mdx, propertyWords).parseExpression(); + } + + private static SelectStatement select(MdxParserProvider provider, String mdx) throws MdxParserException { + MdxStatement statement = provider.newParser(mdx, propertyWords).parseMdxStatement(); + assertThat(statement).isInstanceOf(SelectStatement.class); + return (SelectStatement) statement; + } + + private static CallExpression call(MdxExpression expression, String operator) { + assertThat(expression).isInstanceOf(CallExpression.class); + CallExpression call = (CallExpression) expression; + assertThat(call.operationAtom().name()).as("operator").isEqualTo(operator); + return call; + } + + private static MdxExpression arg(CallExpression call, int index) { + assertThat(call.expressions()).as("arguments of " + call.operationAtom().name()).hasSizeGreaterThan(index); + return call.expressions().get(index); + } + + private static void power(MdxExpression expression, int base, int exponent) { + CallExpression power = call(expression, "^"); + number(arg(power, 0), base); + number(arg(power, 1), exponent); + } + + private static void number(MdxExpression expression, int value) { + assertThat(expression).isInstanceOf(NumericLiteral.class); + assertThat(((NumericLiteral) expression).value()).isEqualByComparingTo(BigDecimal.valueOf(value)); + } + + private static void name(MdxExpression expression, String name) { + assertThat(expression).isInstanceOf(CompoundId.class); + var identifiers = ((CompoundId) expression).objectIdentifiers(); + assertThat(identifiers.getLast()).isInstanceOf(NameObjectIdentifier.class); + assertThat(((NameObjectIdentifier) identifiers.getLast()).name()).isEqualTo(name); + } +} diff --git a/unparser.simple/src/test/java/org/eclipse/daanse/mdx/unparser/simple/SimpleUnparserPowerTest.java b/unparser.simple/src/test/java/org/eclipse/daanse/mdx/unparser/simple/SimpleUnparserPowerTest.java new file mode 100644 index 0000000..b41998c --- /dev/null +++ b/unparser.simple/src/test/java/org/eclipse/daanse/mdx/unparser/simple/SimpleUnparserPowerTest.java @@ -0,0 +1,121 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* dbulahov - initial +*/ +package org.eclipse.daanse.mdx.unparser.simple; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigDecimal; +import java.util.List; + +import org.eclipse.daanse.mdx.model.api.expression.MdxExpression; +import org.eclipse.daanse.mdx.model.api.expression.ObjectIdentifier.Quoting; +import org.eclipse.daanse.mdx.model.api.expression.operation.InfixOperationAtom; +import org.eclipse.daanse.mdx.model.api.expression.operation.ParenthesesOperationAtom; +import org.eclipse.daanse.mdx.model.api.expression.operation.PrefixOperationAtom; +import org.eclipse.daanse.mdx.model.record.expression.CallExpressionR; +import org.eclipse.daanse.mdx.model.record.expression.CompoundIdR; +import org.eclipse.daanse.mdx.model.record.expression.NameObjectIdentifierR; +import org.eclipse.daanse.mdx.model.record.expression.NullLiteralR; +import org.eclipse.daanse.mdx.model.record.expression.NumericLiteralR; +import org.junit.jupiter.api.Test; + +/** + * The power operator {@code ^} is written back as an infix operator. The models are the ones the parsers build for the + * SSAS cases: {@code ^} groups from right to left and binds more tightly than {@code * /} and a leading sign, so each + * text reads back as the same model; a grouping against that order is held by an explicit parentheses node. + */ +class SimpleUnparserPowerTest { + + private final SimpleUnparser unparser = new SimpleUnparser(); + + @Test + void aPower() { + assertUnparsed(power(n(2), n(3)), "2 ^ 3"); + } + + @Test + void aPowerOfAPowerGroupsFromRightToLeft() { + assertUnparsed(power(n(2), power(n(3), n(2))), "2 ^ 3 ^ 2"); + } + + @Test + void aPowerOfAPowerFromLeftToRightKeepsItsParentheses() { + assertUnparsed(power(parentheses(power(n(2), n(3))), n(2)), "(2 ^ 3) ^ 2"); + } + + @Test + void aPowerTimesANumber() { + assertUnparsed(infix("*", power(n(2), n(2)), n(3)), "2 ^ 2 * 3"); + } + + @Test + void aNumberTimesAPower() { + assertUnparsed(infix("*", n(2), power(n(3), n(2))), "2 * 3 ^ 2"); + } + + @Test + void aNumberDividedByAPower() { + assertUnparsed(infix("/", n(12), power(n(2), n(2))), "12 / 2 ^ 2"); + } + + @Test + void theLeadingSignOfAPower() { + assertUnparsed(minus(power(n(2), n(2))), "- 2 ^ 2"); + } + + @Test + void aSignedExponent() { + assertUnparsed(power(n(2), minus(n(1))), "2 ^ - 1"); + } + + @Test + void aFractionalExponent() { + assertUnparsed(power(n(2), new NumericLiteralR(new BigDecimal("0.5"))), "2 ^ 0.5"); + } + + @Test + void aPowerOfNull() { + assertUnparsed(power(new NullLiteralR(), n(2)), "NULL ^ 2"); + } + + @Test + void aPowerOfAQuotientInParentheses() { + MdxExpression sales = new CompoundIdR(List.of(new NameObjectIdentifierR("Measures", Quoting.QUOTED), + new NameObjectIdentifierR("Sales", Quoting.QUOTED))); + assertUnparsed(power(parentheses(infix("/", sales, n(1000))), n(2)), "([Measures].[Sales] / 1000) ^ 2"); + } + + private void assertUnparsed(MdxExpression expression, String mdx) { + assertThat(unparser.unparseExpression(expression)).asString().isEqualTo(mdx); + } + + private static MdxExpression power(MdxExpression base, MdxExpression exponent) { + return infix("^", base, exponent); + } + + private static MdxExpression infix(String operator, MdxExpression left, MdxExpression right) { + return new CallExpressionR(new InfixOperationAtom(operator), List.of(left, right)); + } + + private static MdxExpression minus(MdxExpression operand) { + return new CallExpressionR(new PrefixOperationAtom("-"), List.of(operand)); + } + + private static MdxExpression parentheses(MdxExpression expression) { + return new CallExpressionR(new ParenthesesOperationAtom(), List.of(expression)); + } + + private static MdxExpression n(int value) { + return new NumericLiteralR(BigDecimal.valueOf(value)); + } +}