TimeBetweenQueryMacroHandler is able to process a query param only once

Hi

Suppose you have a query with the following criteria:

[...]where [...] and (@between(f.lastUpdate, now-:lastdays, now+1, day)  or @between(f.updateTs, now-:lastdays, now+1, day) ) [...]

Entity with alias ‘f’ being an entity with a Date property ‘lastUpdate’ which is a functional date (as opposed to ‘updateTs’ which is technical).

Runningsuch query will produce the following error.

org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
Script1.groovy: 2: unexpected token:  @ line 2, column 2.

This comes from TimeBetweenMacroQueryHandler which does not detect that the parameter ‘lastDays’ in this case is used twice. However the macro handler is able to detect several occurences of his macro.

So this issue can be fixed by extending TimeBetweenMacroQueryHandler this way.

 @Override
    public String replaceQueryParams(String queryString, Map<String, Object> params) {
        Matcher matcher = MACRO_PATTERN.matcher(queryString);
        StringBuffer sb = new StringBuffer();
        Set<String> paramToRemoves = new HashSet<>();
        while (matcher.find()) {
            String macros = matcher.group(0);
            macros = replaceParamsInMacros(macros, params, paramToRemoves);
            matcher.appendReplacement(sb, macros);
        }
        matcher.appendTail(sb);
        for(String paramName : paramToRemoves)
            params.remove(paramName);
        return sb.toString();
    }

    @Override
    protected String replaceParamsInMacros(String macros, Map<String, Object> params) {
        throw new IllegalStateException("do not use this impl any more");
    }

    protected String replaceParamsInMacros(String macros, Map<String, Object> params, Set<String> paramToRemoves) {
        Matcher matcher = QUERY_PARAM_PATTERN.matcher(macros);
        StringBuffer sb = new StringBuffer();
        while (matcher.find()) {
            String paramName = matcher.group(1);
            if (params.containsKey(paramName)) {
                matcher.appendReplacement(sb, params.get(paramName).toString());
                paramToRemoves.add(paramName);
            }
        }
        matcher.appendTail(sb);
        return sb.toString();
    }

Best Regards
Michael

Hi, @michael.renaud!

The problem is that @between macro should not be able to process even one param. As said in manual it only supports now variable with an optional addition or subtraction of an integer number. I’ve tried to reproduce problem and it occurred even with one param usage. Which version of Cuba do you use? And if your fix works, could you please provide sample project?

Regards,
Sergey

Hi @s.fedorov

Attached a sample project using the macro this way : where @between(e.createTs, now, now-:days, day) with sample integration tests.

You’re right it does not work out of the box.

com.haulmont.cuba.core.sys.jpql.JpqlSyntaxException: Errors found for input jpql:[select e from timebetween$Order e where @between(e.createTs, now, now-:days, day)]
CommonErrorNode [<mismatched token: [@24,70:74=':days',<30>,1:70], resync=:days>]
	at com.haulmont.cuba.core.sys.jpql.Parser.checkTreeForExceptions(Parser.java:104)
	at com.haulmont.cuba.core.sys.jpql.Parser.parse(Parser.java:40)
	at com.haulmont.cuba.core.sys.jpql.QueryTree.<init>(QueryTree.java:50)
	at com.haulmont.cuba.core.sys.jpql.QueryTree.<init>(QueryTree.java:40)
	at com.haulmont.cuba.core.global.QueryParserAstBased.getTree(QueryParserAstBased.java:71)
	at com.haulmont.cuba.core.global.QueryParserAstBased.getAnalyzer(QueryParserAstBased.java:85)
	at com.haulmont.cuba.core.global.QueryParserAstBased.getParamNames(QueryParserAstBased.java:92)
	at com.haulmont.cuba.core.app.DataServiceQueryBuilder.getQuery(DataServiceQueryBuilder.java:130)

However I can tell that it was designed so, look at the code of DataServiceQueryBuilder.getQuery

   public Query getQuery(EntityManager em) {
        Query query = em.createQuery(queryString);

        //we have to replace parameter names in macros because for {@link com.haulmont.cuba.core.sys.querymacro.TimeBetweenQueryMacroHandler}
        //we need to replace a parameter with number of days with its value before macros is expanded to JPQL expression
        replaceParamsInMacros(query);

        applyConstraints(query);

        QueryParser parser = QueryTransformerFactory.createParser(queryString);
        Set<String> paramNames = parser.getParamNames(); // <---- exception is thrown here
        [...]

The method replaceParamsInMacros does exactly what is needed

    protected void replaceParamsInMacros(Query query) {
        Collection<QueryMacroHandler> handlers = AppBeans.getAll(QueryMacroHandler.class).values();
        String modifiedQuery = query.getQueryString();
        for (QueryMacroHandler handler : handlers) {
            modifiedQuery = handler.replaceQueryParams(modifiedQuery, queryParams);
        }
        query.setQueryString(modifiedQuery);
    }

…but it modifies directly the queryString of the Query object, while DataServiceQueryBuilder.getQuery is still using the query string before alteration.

[...]
        replaceParamsInMacros(query);
        applyConstraints(query);
[...]
        QueryParser parser = QueryTransformerFactory.createParser(queryString); <-- old query string
        Set<String> paramNames = parser.getParamNames(); // <---- exception is thrown here
        [...]

Fixing the code this way (which is done in the sample project, and what we have done in our project), makes it work gracefully:

[...]
        replaceParamsInMacros(query);
        applyConstraints(query);
[...]
        QueryParser parser = QueryTransformerFactory.createParser(query.getQueryString()); <-- altered query
        Set<String> paramNames = parser.getParamNames(); // <---- no more exception
        [...]

But then you reach another problem which is described in the OP, using the macro twice with a parameter does not work: where @between(e.createTs, now, now-:days, day) or @between(e.date, now, now-:days, day)

image

Then applying the solution in OP fixes the issue.

image

timebetween.zip (98.6 KB)