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(