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
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ public static MdxQuery summarize(String cube, Summarize summarize, Map<String, L
}
Set<ModelMeasure> 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();

Expand Down Expand Up @@ -182,23 +182,35 @@ private static Set<ModelMeasure> computedMeasures(Summarize summarize) {
* {@code [Time].CurrentMember.PrevMember}, the others left at their
* default members.
* </p>
* <p>
* 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.
* </p>
*
* @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<String>, List<TablePlan>> sliced, Set<ModelMeasure> computed,
StringBuilder mdx) {
boolean alone, StringBuilder mdx) {
List<String> sets = new ArrayList<>();
List<String> members = new ArrayList<>();
Set<String> taken = new LinkedHashSet<>();
boolean byMeasures = alone && sliced.size() > 1
&& sliced.values().stream().anyMatch(f -> f.stream().anyMatch(MdxGenerator::byMeasures));
for (Map.Entry<List<String>, List<TablePlan>> filters : sliced.entrySet()) {
String set = intersection(filters.getValue());
// the tuples of several hierarchies are one member of the first
// whose current member is taken, the others left at their default
Optional<String> 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;
Expand All @@ -207,7 +219,8 @@ private static String slicer(Map<List<String>, List<TablePlan>> sliced, Set<Mode
String member = hierarchy.get() + "." + MdxNames.quote("DAX slicer " + (members.size() + 1));
// computed before the measures, as members of the hierarchy each
mdx.append(mdx.isEmpty() ? "WITH " : " ").append("MEMBER ").append(member).append(" AS Aggregate(")
.append(set).append("), SOLVE_ORDER = -100");
.append(set).append("), SOLVE_ORDER = ")
.append(byMeasures && filters.getValue().stream().noneMatch(MdxGenerator::byMeasures) ? -200 : -100);
members.add(member);
}
if (sets.isEmpty() && !members.isEmpty()) {
Expand All @@ -221,6 +234,16 @@ private static String slicer(Map<List<String>, List<TablePlan>> sliced, Set<Mode
return slicer;
}

/** @return whether the filter table keeps members by measures computed for them */
private static boolean byMeasures(TablePlan filter) {
return switch (filter) {
case Summarize summarize -> summarize.condition().isPresent() || summarize.top().isPresent();
case Filter f -> byMeasures(f.source());
case Generate generate -> byMeasures(generate.outer()) || byMeasures(generate.inner());
default -> false;
};
}

/**
* Filter tables on a level of a hierarchy grouped by deeper than the groups
* restrict what the measures compute for a group to its members of that
Expand Down Expand Up @@ -419,7 +442,7 @@ public static MdxQuery generate(String cube, Generate generate) {
}
Set<ModelMeasure> computed = computedMeasures(inner);
inner = inFilters(inner, deepest(grouped), deeper, mdx);
String slicer = slicer(sliced, computed, mdx);
String slicer = slicer(sliced, computed, false, mdx);
Measures measures = measures(inner, mdx, sources, false);

String set = filterSet(generate.outer());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -928,17 +928,22 @@ private TablePlan calculateTable(List<DaxExpression> arguments) throws DaxSemant
// without outer filters there is nothing KEEPFILTERS keeps
addFilters(arguments.subList(1, arguments.size()), filters, true);
DaxExpression source = arguments.get(0);
// the only filter of the innermost CALCULATETABLE, if inner, is computed within all others
Optional<TablePlan> within = Optional.empty();
// the filters of an inner CALCULATETABLE apply within the outer ones
while (source instanceof FunctionCall call && call.functionName().equalsIgnoreCase("CALCULATETABLE")
&& !call.arguments().isEmpty()) {
List<TablePlan> outer = List.copyOf(filters);
addFilters(call.arguments().subList(1, call.arguments().size()), filters, false);
List<TablePlan> 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);
}

/**
Expand Down Expand Up @@ -1117,10 +1122,13 @@ private void collectColumns(DaxExpression expression, Set<ModelColumn> 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<TablePlan> filters) throws DaxSemanticException {
private static TablePlan calculated(TablePlan table, List<TablePlan> filters, Optional<TablePlan> within)
throws DaxSemanticException {
return switch (table) {
case Summarize summarize -> {
List<TablePlan> all = new ArrayList<>(summarize.filters());
Expand All @@ -1142,7 +1150,8 @@ private static TablePlan calculated(TablePlan table, List<TablePlan> 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(),
Expand All @@ -1168,23 +1177,23 @@ private static TablePlan calculated(TablePlan table, List<TablePlan> 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<TablePlan> 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();
Expand Down Expand Up @@ -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<TablePlan> filters,
Optional<TablePlan> within) {
if (!summarize.groupBy().isEmpty() || within.isEmpty() || !filters.contains(within.get())) {
return false;
}
Set<String> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading