From 7b7a6421f112d2ce73a8532fa0f6d69be235bd77 Mon Sep 17 00:00:00 2001
From: dbulahov
Date: Fri, 30 Oct 2026 16:03:21 +0300
Subject: [PATCH] DAX engine.impl filter by measures of an inner CALCULATETABLE
within the outer filters
For measures without groups, as of ROW, the only filter of the innermost
CALCULATETABLE, by measures (e.g. of the years where Profit is not BLANK),
is computed within the outer filters (e.g. of the customer): those are
slicer members with SOLVE_ORDER = -200, it one with -100. Before, several
filters with one by a measure were not supported.
Signed-off-by: dbulahov
---
.../dax/engine/impl/mdx/MdxGenerator.java | 31 +++++++++++--
.../daanse/dax/engine/impl/plan/Binder.java | 46 +++++++++++++++----
.../dax/engine/impl/mdx/MdxGeneratorTest.java | 40 ++++++++++++++++
3 files changed, 104 insertions(+), 13 deletions(-)
diff --git a/engine.impl/src/main/java/org/eclipse/daanse/dax/engine/impl/mdx/MdxGenerator.java b/engine.impl/src/main/java/org/eclipse/daanse/dax/engine/impl/mdx/MdxGenerator.java
index 3b8d3e8..89b6df4 100644
--- a/engine.impl/src/main/java/org/eclipse/daanse/dax/engine/impl/mdx/MdxGenerator.java
+++ b/engine.impl/src/main/java/org/eclipse/daanse/dax/engine/impl/mdx/MdxGenerator.java
@@ -132,7 +132,7 @@ public static MdxQuery summarize(String cube, Summarize summarize, Map computed = computedMeasures(summarize);
summarize = inFilters(summarize, deepest, kept, mdx);
- String slicer = slicer(sliced, computed, mdx);
+ String slicer = slicer(sliced, computed, deepest.isEmpty(), mdx);
Measures measures = measures(summarize, mdx, sources, !groups.isEmpty());
String keeping = measures.keeping();
@@ -182,16 +182,25 @@ private static Set computedMeasures(Summarize summarize) {
* {@code [Time].CurrentMember.PrevMember}, the others left at their
* default members.
*
+ *
+ * Of measures without groups, as of {@code ROW}, a filter table by
+ * measures beside others is computed within the members they keep, as of
+ * an inner CALCULATETABLE: each is a calculated member of a lower solve
+ * order than its one, which then computes its condition over theirs.
+ *
*
* @param sliced the filter tables, by the hierarchies they are on
* @param computed the measures computed
+ * @param alone whether the measures are computed without groups
* @return the slicer; {@code null} without filter tables
*/
private static String slicer(Map, List> sliced, Set computed,
- StringBuilder mdx) {
+ boolean alone, StringBuilder mdx) {
List sets = new ArrayList<>();
List members = new ArrayList<>();
Set taken = new LinkedHashSet<>();
+ boolean byMeasures = alone && sliced.size() > 1
+ && sliced.values().stream().anyMatch(f -> f.stream().anyMatch(MdxGenerator::byMeasures));
for (Map.Entry, List> filters : sliced.entrySet()) {
String set = intersection(filters.getValue());
// the tuples of several hierarchies are one member of the first
@@ -199,6 +208,9 @@ private static String slicer(Map, List> sliced, Set hierarchy = filters.getKey().stream()
.filter(h -> !taken.contains(h) && computed.stream().anyMatch(m -> m.takesCurrentMember(h)))
.findFirst();
+ if (byMeasures && hierarchy.isEmpty()) {
+ hierarchy = filters.getKey().stream().filter(h -> !taken.contains(h)).findFirst();
+ }
if (hierarchy.isEmpty()) {
sets.add(set);
continue;
@@ -207,7 +219,8 @@ private static String slicer(Map, List> sliced, Set outer = List.copyOf(filters);
addFilters(call.arguments().subList(1, call.arguments().size()), filters, false);
+ List added = filters.stream().filter(f -> !outer.contains(f)).toList();
+ within = added.size() == 1 && filters.size() > 1 ? Optional.of(added.getFirst()) : Optional.empty();
source = call.arguments().get(0);
}
TablePlan table = table(source);
if (filters.isEmpty()) {
return table;
}
- return calculated(table, filters);
+ return calculated(table, filters, within);
}
/**
@@ -1117,10 +1122,13 @@ private void collectColumns(DaxExpression expression, Set columns)
}
/**
+ * @param within the filter table computed within all the others, as the only
+ * one of an inner CALCULATETABLE; empty if none is
* @return the table computed with the filter tables too, as CALCULATETABLE
* computes it: they filter all the cube computes for it
*/
- private static TablePlan calculated(TablePlan table, List filters) throws DaxSemanticException {
+ private static TablePlan calculated(TablePlan table, List filters, Optional within)
+ throws DaxSemanticException {
return switch (table) {
case Summarize summarize -> {
List all = new ArrayList<>(summarize.filters());
@@ -1142,7 +1150,8 @@ private static TablePlan calculated(TablePlan table, List filters) th
}
all = together;
// a filter by a measure would be computed within the others, or they within it
- if (all.size() > 1 && all.stream().anyMatch(Binder::filtersByMeasure)) {
+ if (all.size() > 1 && all.stream().anyMatch(Binder::filtersByMeasure)
+ && !computedWithinOthers(summarize, all, within)) {
throw notSupported("CALCULATETABLE with several filters, of them one by a measure");
}
Summarize calculated = new Summarize(summarize.groupBy(), summarize.measures(), summarize.condition(),
@@ -1168,23 +1177,23 @@ private static TablePlan calculated(TablePlan table, List filters) th
yield calculated;
}
// the rows are computed as before, of the filtered source
- case Filter filter -> new Filter(calculated(filter.source(), filters), filter.condition());
- case TopN topN -> new TopN(calculated(topN.source(), filters), topN.count(), topN.keys(), topN.partition());
- case Sample sample -> new Sample(calculated(sample.source(), filters), sample.count(), sample.keys());
+ case Filter filter -> new Filter(calculated(filter.source(), filters, within), filter.condition());
+ case TopN topN -> new TopN(calculated(topN.source(), filters, within), topN.count(), topN.keys(), topN.partition());
+ case Sample sample -> new Sample(calculated(sample.source(), filters, within), sample.count(), sample.keys());
case Rollup rollup -> {
List levels = new ArrayList<>();
for (TablePlan level : rollup.levels()) {
- levels.add(calculated(level, filters));
+ levels.add(calculated(level, filters, within));
}
yield new Rollup(levels, rollup.keys(), rollup.rolled());
}
case AddColumns addColumns ->
- new AddColumns(calculated(addColumns.source(), filters), addColumns.width(), addColumns.added());
+ new AddColumns(calculated(addColumns.source(), filters, within), addColumns.width(), addColumns.added());
// constants are not filtered
case ConstantTable constant -> constant;
// by a property without its level, the grouping by all its columns filtered as one
case Generate generate when computesByProperties(generate) && grouping(generate).isPresent() ->
- calculated(grouping(generate).get(), filters);
+ calculated(grouping(generate).get(), filters, within);
// the outer rows the filters keep, each joined with the inner ones computed for it
case Generate generate -> {
TablePlan outer = generate.outer();
@@ -1228,6 +1237,25 @@ case Generate generate when computesByProperties(generate) && grouping(generate)
};
}
+ /**
+ * @param filters the filter tables of the measures without groups, as of
+ * {@code ROW}
+ * @param within the filter table computed within all the others
+ * @return whether it is the only one by a measure, on hierarchies none of
+ * the others is: its members are the slicer, computed within those
+ * the others keep, see MdxGenerator
+ */
+ private static boolean computedWithinOthers(Summarize summarize, List filters,
+ Optional within) {
+ if (!summarize.groupBy().isEmpty() || within.isEmpty() || !filters.contains(within.get())) {
+ return false;
+ }
+ Set hierarchies = new HashSet<>();
+ Summarize.filterColumns(within.get()).forEach(c -> hierarchies.add(c.hierarchy()));
+ return filters.stream().filter(f -> !f.equals(within.get())).noneMatch(f -> filtersByMeasure(f)
+ || Summarize.filterColumns(f).stream().anyMatch(c -> hierarchies.contains(c.hierarchy())));
+ }
+
/**
* @param filter a filter table
* @return whether it is on the text of columns of tables other than those
diff --git a/engine.impl/src/test/java/org/eclipse/daanse/dax/engine/impl/mdx/MdxGeneratorTest.java b/engine.impl/src/test/java/org/eclipse/daanse/dax/engine/impl/mdx/MdxGeneratorTest.java
index ee642bb..e8d9327 100644
--- a/engine.impl/src/test/java/org/eclipse/daanse/dax/engine/impl/mdx/MdxGeneratorTest.java
+++ b/engine.impl/src/test/java/org/eclipse/daanse/dax/engine/impl/mdx/MdxGeneratorTest.java
@@ -827,6 +827,46 @@ EVALUATE CALCULATETABLE(ROW("MeasuresEmployeeSalary", 'Measures'[Employee Salary
+ "WHERE ([Employees].[Employees1].[DAX slicer 1])");
}
+ @Test
+ void filterByMeasuresOfAnInnerCalculateTableIsComputedWithinTheOuterFilters() throws Exception {
+ ModelColumn year = new ModelColumn("Time", "Time.Time.Year", "[Time].[Time]", "[Time].[Time].[Year]", 1,
+ DaxType.STRING);
+ TabularModel model = new TabularModel("[Sales]", List.of(new ModelTable("Time", List.of(year)),
+ new ModelTable("Customers", List.of(customerLevel("Name", 4)))),
+ List.of(new ModelMeasure("Profit", "[Measures].[Profit]"),
+ new ModelMeasure("Store Cost", "[Measures].[Store Cost]", true),
+ new ModelMeasure("Unit Sales", "[Measures].[Unit Sales]", true)));
+ String dax = """
+ EVALUATE CALCULATETABLE(CALCULATETABLE(ROW("M", 'Measures'[%1$s]),
+ KEEPFILTERS(FILTER(KEEPFILTERS(VALUES('Time'[Time.Time.Year])),
+ NOT(ISBLANK('Measures'[%2$s]))))),
+ KEEPFILTERS(FILTER(KEEPFILTERS(VALUES('Customers'[Customers.Customers.Name])),
+ 'Customers'[Customers.Customers.Name] = "A. Catherine Binkley")))""";
+ String customer = "MEMBER [Customers].[Customers].[DAX slicer 1] AS Aggregate(Filter("
+ + "[Customers].[Customers].[Name].Members, UCase([Customers].[Customers].CurrentMember.Name) = "
+ + "\"A. CATHERINE BINKLEY\")), SOLVE_ORDER = -200 ";
+ String slicer = " WHERE ([Customers].[Customers].[DAX slicer 1], [Time].[Time].[DAX slicer 2])";
+ // the years where the customer's profit is not BLANK: the customer aggregated within them
+ QueryPlan plan = bindCustomers(model, dax.formatted("Profit", "Profit"));
+ assertThat(MdxGenerator.summarize("[Sales]", (Summarize) plan.evaluates().get(0).table()).text())
+ .isEqualTo("WITH " + customer + "MEMBER [Time].[Time].[DAX slicer 2] AS Aggregate(Filter("
+ + "[Time].[Time].[Year].Members, NOT IsEmpty([Measures].[Profit]))), SOLVE_ORDER = -100 "
+ + "SELECT {[Measures].[Profit]} ON COLUMNS FROM [Sales]" + slicer);
+ // of stored measures too, which take no current member
+ plan = bindCustomers(model, dax.formatted("Store Cost", "Unit Sales"));
+ assertThat(MdxGenerator.summarize("[Sales]", (Summarize) plan.evaluates().get(0).table()).text())
+ .isEqualTo("WITH " + customer + "MEMBER [Time].[Time].[DAX slicer 2] AS Aggregate(Filter("
+ + "[Time].[Time].[Year].Members, NOT IsEmpty([Measures].[Unit Sales]))), SOLVE_ORDER = -100 "
+ + "SELECT {[Measures].[Store Cost]} ON COLUMNS FROM [Sales]" + slicer);
+ // beside the other filter it is computed without it
+ assertThatThrownBy(() -> bindCustomers(model, """
+ EVALUATE CALCULATETABLE(ROW("M", 'Measures'[Profit]),
+ KEEPFILTERS(FILTER(KEEPFILTERS(VALUES('Time'[Time.Time.Year])), NOT(ISBLANK('Measures'[Profit])))),
+ KEEPFILTERS(FILTER(KEEPFILTERS(VALUES('Customers'[Customers.Customers.Name])),
+ 'Customers'[Customers.Customers.Name] = "A. Catherine Binkley")))"""))
+ .hasMessageContaining("CALCULATETABLE with several filters, of them one by a measure");
+ }
+
@Test
void filtersOfGenerateOnColumnsOfItsInnerTableRestrictTheInnerTable() throws Exception {
TabularModel model = new TabularModel("[C]", List.of(new ModelTable("Customers", List.of(