Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Filter events by DE existence in event [DHIS2-15954] #19778

Open
wants to merge 31 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
613859b
feat: Filter events by existing data element [DHIS2-15954]
muilpp Jan 23, 2025
06269e3
feat: Filter events by existing data element [DHIS2-15954]
muilpp Jan 23, 2025
1ddbce8
feat: Avoid using dataElements map when sorting DEs [DHIS2-15954]
muilpp Jan 24, 2025
f19e3be
feat: Add order fields in select clause from params order [DHIS2-15954]
muilpp Jan 24, 2025
2315c98
feat: Replace params for order list in method signature [DHIS2-15954]
muilpp Jan 24, 2025
edb8750
feat: Fix data element tests [DHIS2-15954]
muilpp Jan 24, 2025
da9c871
feat: Filter events by DE existence in event [DHIS2-15954]
muilpp Jan 27, 2025
0187a04
feat: Assure EX operator is used only once in DE filter [DHIS2-15954]
muilpp Jan 27, 2025
96625e5
feat: Filter by DE when fetching event file [DHIS2-15954]
muilpp Jan 27, 2025
0e0afad
feat: Merge statements [DHIS2-15954]
muilpp Jan 27, 2025
2b83bc0
feat: Improve error message [DHIS2-15954]
muilpp Jan 28, 2025
37bbc74
Merge remote-tracking branch 'origin/master' into DHIS2-15954-filter-…
muilpp Jan 28, 2025
c31ec3e
feat: Add test to verify mapping logic [DHIS2-15945]
muilpp Jan 29, 2025
cdaf3ef
feat: Add test to verify mapping logic [DHIS2-15945]
muilpp Jan 29, 2025
bad030e
feat: Replace operator enum value [DHIS2-15945]
muilpp Jan 30, 2025
91a898f
feat: Introduce tracker unary operators [DHIS2-15945]
muilpp Feb 9, 2025
036b459
feat: Introduce tracker unary operators (2) [DHIS2-15945]
muilpp Feb 9, 2025
edc5a56
feat: Check for null operator [DHIS2-15945]
muilpp Feb 9, 2025
768adbf
feat: Save unary operators in working list [DHIS2-15945]
muilpp Feb 10, 2025
06cfe93
feat: Use is null instead of jsonb specific ?? operator [DHIS2-15945]
muilpp Feb 10, 2025
b999c40
feat: Add test to validate filter with UID only is mapped [DHIS2-15945]
muilpp Feb 10, 2025
b551f7b
feat: Add program stage working list controller test [DHIS2-15945]
muilpp Feb 11, 2025
686963d
feat: Rename fields in EventDataFilter [DHIS2-15945]
muilpp Feb 11, 2025
72dd62a
feat: Test !null operator in WL controller [DHIS2-15945]
muilpp Feb 11, 2025
f00fa90
feat: Improve filter parsing [DHIS2-15945]
muilpp Feb 12, 2025
8f67dc3
Merge branch 'master' into DHIS2-15954-filter-events-by-DE-existence
muilpp Feb 13, 2025
a3b7b41
Merge branch 'master' into DHIS2-15954-filter-events-by-DE-existence
muilpp Feb 13, 2025
aab93bb
feat: Remove option set join when fetching events [DHIS2-15945]
muilpp Feb 13, 2025
6afd0f6
Merge remote-tracking branch 'origin/DHIS2-15954-filter-events-by-DE-…
muilpp Feb 13, 2025
c3c1d81
feat: Remove option value join [DHIS2-15954]
muilpp Feb 13, 2025
9fb70d0
feat: Remove unused param [DHIS2-15954]
muilpp Feb 13, 2025
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 @@ -43,6 +43,8 @@
import static org.hisp.dhis.common.QueryOperator.NIEQ;
import static org.hisp.dhis.common.QueryOperator.NILIKE;
import static org.hisp.dhis.common.QueryOperator.NLIKE;
import static org.hisp.dhis.common.QueryOperator.NNULL;
import static org.hisp.dhis.common.QueryOperator.NULL;
import static org.hisp.dhis.common.QueryOperator.SW;

import com.google.common.collect.ImmutableMap;
Expand Down Expand Up @@ -80,6 +82,8 @@ public class QueryFilter {
.put(EW, unused -> "like")
.put(NLIKE, unused -> "not like")
.put(IN, unused -> "in")
.put(NULL, unused -> NULL.getValue())
.put(NNULL, unused -> NNULL.getValue())
.build();

protected QueryOperator operator;
Expand All @@ -92,6 +96,10 @@ public class QueryFilter {

public QueryFilter() {}

public QueryFilter(QueryOperator operator) {
this.operator = operator;
}

public QueryFilter(QueryOperator operator, String filter) {
this.operator = operator;
this.filter = filter;
Expand All @@ -105,10 +113,6 @@ public boolean isFilter() {
return operator != null && filter != null && !filter.isEmpty();
}

public boolean isOperator(QueryOperator op) {
return operator != null && operator.equals(op);
}

public String getSqlOperator() {
return getSqlOperator(false);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ public enum QueryOperator {
IN("in", true),
SW("sw"),
EW("ew"),
NULL("is null"),
NNULL("is not null"),
// Analytics specifics
IEQ("==", true),
NE("!=", true),
Expand All @@ -68,6 +70,8 @@ public enum QueryOperator {

private static final Set<QueryOperator> COMPARISON_OPERATORS = EnumSet.of(GT, GE, LT, LE);

private static final Set<QueryOperator> UNARY_OPERATORS = EnumSet.of(NULL, NNULL);

private final String value;

private final boolean nullAllowed;
Expand Down Expand Up @@ -113,4 +117,8 @@ public boolean isIn() {
public boolean isComparison() {
return COMPARISON_OPERATORS.contains(this);
}

public boolean isUnary() {
return UNARY_OPERATORS.contains(this);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ public class EventDataFilter implements Serializable {
/** Like */
private String like;

/** Null */
private String nullFilter;

/** Not Null */
private String notNullFilter;

/** If the dataItem is of type date, then date filtering parameters are specified using this. */
private DateFilterPeriod dateFilter;

Expand Down Expand Up @@ -158,6 +164,31 @@ public void setLike(String like) {
this.like = like;
}

@JsonProperty
@JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0)
public String getNull() {
return nullFilter;
}

public void setNull(String nullFilter) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought this is going to be a boolean? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes but didn't we say we only want to store it when it's true?
if it was a boolean, it'd be false by default and stored in the database
if it's a String, we can just set it to null by default, so it'll be excluded by Jackson, unless we explicitly set it to "true"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes but didn't we say we only want to store it when it's true?
I think so but why does that mean we accept/require {"null": ""}? I though we are going for the not perfect but slightly better {"null": true} if users want to filter for null.

@enricocolasante maybe I misremember 😅

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't we agree that we accept any value there? {"null": [ANYTHING_BUT_NULL]}?
Then when saving we remove the value there and use true

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what we said, but just a nuance: if we set it to true (boolean), it means that by default, it will be set to false, and Jackson will serialize both cases.

If we set it to "true" (string), then by default, it's null, and Jackson omits it.

if (nullFilter != null) {
this.nullFilter = "true";
}
}

@JsonProperty("!null")
@JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0)
public String getNotNull() {
return notNullFilter;
}

@JsonProperty("!null")
public void setNotNull(String notNullFilter) {
if (notNullFilter != null) {
this.notNullFilter = "true";
}
}

@JsonProperty
@JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0)
public DateFilterPeriod getDateFilter() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import org.hisp.dhis.common.AssignedUserQueryParam;
import org.hisp.dhis.common.OrganisationUnitSelectionMode;
import org.hisp.dhis.common.QueryFilter;
import org.hisp.dhis.common.QueryOperator;
import org.hisp.dhis.common.SortDirection;
import org.hisp.dhis.common.UID;
import org.hisp.dhis.dataelement.DataElement;
Expand Down Expand Up @@ -473,7 +474,7 @@ public EventQueryParams filterBy(DataElement de, QueryFilter filter) {
}

public EventQueryParams filterBy(DataElement de) {
this.dataElements.putIfAbsent(de, new ArrayList<>());
this.dataElements.putIfAbsent(de, List.of(new QueryFilter(QueryOperator.NNULL)));
return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1287,10 +1287,7 @@ private StringBuilder dataElementAndFiltersSql(
StringBuilder selectBuilder) {
int filterCount = 0;

StringBuilder optionValueJoinBuilder = new StringBuilder();
StringBuilder optionValueConditionBuilder = new StringBuilder();
StringBuilder eventDataValuesWhereSql = new StringBuilder();
Set<String> joinedColumns = new HashSet<>();

for (Entry<DataElement, List<QueryFilter>> item : params.getDataElements().entrySet()) {
++filterCount;
Expand All @@ -1310,105 +1307,57 @@ private StringBuilder dataElementAndFiltersSql(
.append(" as ")
.append(deUid);

String optValueTableAs = "opt_" + filterCount;

if (!joinedColumns.contains(deUid) && de.hasOptionSet() && !filters.isEmpty()) {
String optSetBind = "optset_" + filterCount;

mapSqlParameterSource.addValue(optSetBind, de.getOptionSet().getId());

optionValueJoinBuilder
.append("inner join optionvalue as ")
.append(optValueTableAs)
.append(" on lower(")
.append(optValueTableAs)
.append(".code) = ")
.append("lower(")
.append(dataValueValueSql)
.append(") and ")
.append(optValueTableAs)
.append(".optionsetid = ")
.append(":")
.append(optSetBind)
.append(" ");

joinedColumns.add(deUid);
}

if (!filters.isEmpty()) {
for (QueryFilter filter : filters) {
++filterCount;

final String queryCol =
de.getValueType().isNumeric()
? castToNumber(dataValueValueSql)
: lower(dataValueValueSql);
for (QueryFilter filter : filters) {
++filterCount;

String bindParameter = "parameter_" + filterCount;
int itemType = de.getValueType().isNumeric() ? Types.NUMERIC : Types.VARCHAR;
final String queryCol =
de.getValueType().isNumeric()
? castToNumber(dataValueValueSql)
: lower(dataValueValueSql);

if (!de.hasOptionSet()) {
eventDataValuesWhereSql.append(hlp.whereAnd());
String bindParameter = "parameter_" + filterCount;
int itemType = de.getValueType().isNumeric() ? Types.NUMERIC : Types.VARCHAR;

if (QueryOperator.IN.getValue().equalsIgnoreCase(filter.getSqlOperator())) {
mapSqlParameterSource.addValue(
bindParameter,
QueryFilter.getFilterItems(StringUtils.lowerCase(filter.getFilter())),
itemType);
eventDataValuesWhereSql.append(hlp.whereAnd());

eventDataValuesWhereSql.append(inCondition(filter, bindParameter, queryCol));
} else {
mapSqlParameterSource.addValue(
bindParameter, StringUtils.lowerCase(filter.getSqlBindFilter()), itemType);

eventDataValuesWhereSql
.append(" ")
.append(queryCol)
.append(" ")
.append(filter.getSqlOperator())
.append(" ")
.append(":")
.append(bindParameter)
.append(" ");
}
} else {
if (QueryOperator.IN.getValue().equalsIgnoreCase(filter.getSqlOperator())) {
mapSqlParameterSource.addValue(
bindParameter,
QueryFilter.getFilterItems(StringUtils.lowerCase(filter.getFilter())),
itemType);

optionValueConditionBuilder.append(" and ");
optionValueConditionBuilder.append(inCondition(filter, bindParameter, queryCol));
} else {
mapSqlParameterSource.addValue(
bindParameter, StringUtils.lowerCase(filter.getSqlBindFilter()), itemType);

optionValueConditionBuilder
.append("and lower(")
.append(optValueTableAs)
.append(DOT_NAME)
.append(" ")
.append(filter.getSqlOperator())
.append(" ")
.append(":")
.append(bindParameter)
.append(" ");
}
}
if (filter.getOperator().isUnary()) {
eventDataValuesWhereSql.append(unaryOperatorCondition(filter.getOperator(), deUid));
} else if (QueryOperator.IN.getValue().equalsIgnoreCase(filter.getSqlOperator())) {
mapSqlParameterSource.addValue(
bindParameter,
QueryFilter.getFilterItems(StringUtils.lowerCase(filter.getFilter())),
itemType);

eventDataValuesWhereSql.append(inCondition(filter, bindParameter, queryCol));
} else {
mapSqlParameterSource.addValue(
bindParameter, StringUtils.lowerCase(filter.getSqlBindFilter()), itemType);

eventDataValuesWhereSql
.append(" ")
.append(queryCol)
.append(" ")
.append(filter.getSqlOperator())
.append(" ")
.append(":")
.append(bindParameter)
.append(" ");
}
} else {
eventDataValuesWhereSql.append(hlp.whereAnd());
eventDataValuesWhereSql.append(" (ev.eventdatavalues ?? '");
eventDataValuesWhereSql.append(deUid);
eventDataValuesWhereSql.append("')");
}
}

return optionValueJoinBuilder
.append(optionValueConditionBuilder)
.append(eventDataValuesWhereSql)
.append(" ");
return eventDataValuesWhereSql.append(" ");
}

private String unaryOperatorCondition(QueryOperator queryOperator, String deUid) {
return new StringBuilder()
.append(" ev.eventdatavalues->")
.append("'")
.append(deUid)
.append("' ")
.append(queryOperator.getValue())
.append(" ")
.toString();
}

private String inCondition(QueryFilter filter, String boundParameter, String queryCol) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,9 @@ void shouldMapDataElementFilters() throws BadRequestException, ForbiddenExceptio
.dataElementFilters(
Map.of(
UID.of(DE_1_UID),
List.of(new QueryFilter(QueryOperator.EQ, "2")),
List.of(
new QueryFilter(QueryOperator.EQ, "2"),
new QueryFilter(QueryOperator.NNULL)),
UID.of(DE_2_UID),
List.of(new QueryFilter(QueryOperator.LIKE, "foo"))))
.build();
Expand All @@ -376,7 +378,7 @@ void shouldMapDataElementFilters() throws BadRequestException, ForbiddenExceptio
Map<DataElement, List<QueryFilter>> expected =
Map.of(
de1,
List.of(new QueryFilter(QueryOperator.EQ, "2")),
List.of(new QueryFilter(QueryOperator.EQ, "2"), new QueryFilter(QueryOperator.NNULL)),
de2,
List.of(new QueryFilter(QueryOperator.LIKE, "foo")));
assertEquals(expected, dataElements);
Expand Down
Loading
Loading