Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions parser.ccc/src/main/ccc/Grammer.ccc
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ TOKEN :
TOKEN :
< ASTERISK : "*" >
| < BANG : "!" >
| < CARET : "^" >
| < COLON : ":" >
| < CONCAT : "||" >
| < EQ : "=" >
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand Down
11 changes: 11 additions & 0 deletions parser.cccx/src/main/ccc/Expressions.inc.ccc
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,18 @@ UnaryExpression#(>1) :
// This is only in Pentaho, it seems.
<EXISTING>
)?
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
[
(
<CARET>
UnaryExpression
)#BinaryExpression(+1)
]
;

// inline set alias: names this operand, binds more tightly than * + - : (SSAS); not inside a Cast operand
Expand Down
1 change: 1 addition & 0 deletions parser.cccx/src/main/ccc/Lexer.inc.ccc
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ TOKEN #ReservedWord :
TOKEN #Operator :
< ASTERISK : "*" >
| < BANG : "!" >
| < CARET : "^" >
| < COLON : ":" >
| < CONCAT : "||" >
| < EQ : "=" >
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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));
}
}
Loading