Hibernate-reactive: Parameter substitution error when limiting results on MS SQL Server and Hibernate Reactive 1.0.0.CR9

Created on 5 Aug 2021  ·  13Comments  ·  Source: hibernate/hibernate-reactive

Hello, I am encountering an issue with MS SQL query parameters in 1.0.0.CR9 when using Paging or Ranges (SQL Exception, message='Incorrect syntax near '?'.'), possibly because the parameter is in the Select clause?

Stack: Quarkus 2.1.0.Final / Panache Reactive / Mutiny 4.1.2 / Hibernate Reactive 1.0.0.CR9 / Hibernate Core 5.5.5.Final

From my rest resource, running this:
[PanacheRepository].find("firstName LIKE ?1").page(Page.of(25)).list();

With SQL logging on in Hibernate, this is the produced SQL:

Hibernate:
    select
        top(?) request0_.id as id1_1_,
        request0_.caseId as caseid2_1_,
        request0_.caseNumber as casenumb3_1_,
        request0_.expires as expires4_1_,
        request0_.addressLine1 as addressl5_1_,
        request0_.addressLine2 as addressl6_1_,
        request0_.agency as agency7_1_,
        request0_.city as city8_1_,
        request0_.email as email9_1_,
        request0_.firstName as firstna10_1_,
        request0_.lastName as lastnam11_1_,
        request0_.phone as phone12_1_,
        request0_.relationship as relatio13_1_,
        request0_.state as state14_1_,
        request0_.zipcode as zipcode15_1_,
        request0_.submitted as submitt16_1_
    from
        Request request0_
    where
        request0_.firstName like @P1

Note the correct parameter substitution in the where clause, but substitution did not occur for the "top(?)" in the select clause.
If I remove the paging and re-run, the query executes successfully.

Trace logging shown below, showing HQL and SQL. I would guess that Panache is using setMaxResults() on the query. Happy to open an issue with Quarkus or Mutiny if that's where the issue is, and thank you in advance!

2021-08-05 12:55:57,342 DEBUG [org.hib.hql.int.ast.QueryTranslatorImpl] (vert.x-eventloop-thread-1) HQL: FROM myproject.entities.Request WHERE requestor.firstName LIKE ?1
2021-08-05 12:55:57,342 DEBUG [org.hib.hql.int.ast.QueryTranslatorImpl] (vert.x-eventloop-thread-1) SQL: select request0_.id as id1_1_, request0_.caseId as caseid2_1_, request0_.caseNumber as casenumb3_1_, request0_.expires as expires4_1_, request0_.addressLine1 as addressl5_1_, request0_.addressLine2 as addressl6_1_, request0_.agency as agency7_1_, request0_.city as city8_1_, request0_.email as email9_1_, request0_.firstName as firstna10_1_, request0_.lastName as lastnam11_1_, request0_.phone as phone12_1_, request0_.relationship as relatio13_1_, request0_.state as state14_1_, request0_.zipcode as zipcode15_1_, request0_.submitted as submitt16_1_ from Request request0_ where request0_.firstName like ?
2021-08-05 12:55:57,342 DEBUG [org.hib.hql.int.ast.ErrorTracker] (vert.x-eventloop-thread-1) throwQueryException() : no errors
2021-08-05 12:55:57,342 TRACE [org.hib.rea.ses.imp.ReactiveHQLQueryPlan] (vert.x-eventloop-thread-1) Find: FROM myproject.entities.Request WHERE requestor.firstName LIKE ?1
2021-08-05 12:55:57,344 TRACE [org.hib.eng.spi.QueryParameters] (vert.x-eventloop-thread-1) Named parameters: {1=%j%}
2021-08-05 12:55:57,345 TRACE [org.hib.typ.des.sql.BasicBinder] (vert.x-eventloop-thread-1) binding parameter [2] as [VARCHAR] - [%j%]
2021-08-05 12:55:57,345 TRACE [org.hib.loa.Loader] (vert.x-eventloop-thread-1) Bound [3] parameters total
2021-08-05 12:55:57,355 DEBUG [org.hib.SQL] (vert.x-eventloop-thread-1)
    select
        top(?) request0_.id as id1_1_,
        request0_.caseId as caseid2_1_,
        request0_.caseNumber as casenumb3_1_,
        request0_.expires as expires4_1_,
        request0_.addressLine1 as addressl5_1_,
        request0_.addressLine2 as addressl6_1_,
        request0_.agency as agency7_1_,
        request0_.city as city8_1_,
        request0_.email as email9_1_,
        request0_.firstName as firstna10_1_,
        request0_.lastName as lastnam11_1_,
        request0_.phone as phone12_1_,
        request0_.relationship as relatio13_1_,
        request0_.state as state14_1_,
        request0_.zipcode as zipcode15_1_,
        request0_.submitted as submitt16_1_
    from
        Request request0_
    where
        request0_.firstName like @P1
2021-08-05 12:55:57,358 ERROR [org.hib.rea.errors] (vert.x-eventloop-thread-1) HR000057: Failed to execute statement [$1select request0_.id as id1_1_, request0_.caseId as caseid2_1_, request0_.caseNumber as casenumb3_1_, request0_.expires as expires4_1_, request0_.addressLine1 as addressl5_1_, request0_.addressLine2 as addressl6_1_, request0_.agency as agency7_1_, request0_.city as city8_1_, request0_.email as email9_1_, request0_.firstName as firstna10_1_, request0_.lastName as lastnam11_1_, request0_.phone as phone12_1_, request0_.relationship as relatio13_1_, request0_.state as state14_1_, request0_.zipcode as zipcode15_1_, request0_.submitted as submitt16_1_ from Request request0_ where request0_.firstName like @P1]: $2could not execute query: java.util.concurrent.CompletionException: io.vertx.mssqlclient.MSSQLException: {number=102, state=1, severity=15, message='Incorrect syntax near '?'.', serverName='aa3ae4a0fd3c', lineNumber=1, additional=[io.vertx.mssqlclient.MSSQLException: {number=8180, state=1, severity=16, message='Statement(s) could not be prepared.', serverName='aa3ae4a0fd3c', lineNumber=1}]}
bug

All 13 comments

There's something weird about that SQL.

Why are we even using top n instead of offset ... fetch .... And why the redundant parentheses?

I would guess that Panache is using setMaxResults() on the query.

Well it certainly should be. Panache has no business going and doing its own SQL generation, right @FroMage?

Well I tried this in plain HR, and I get:

select hqlquerypa0_.id as id1_0_, hqlquerypa0_.description as descript2_0_, hqlquerypa0_.name as name3_0_, hqlquerypa0_.type as type4_0_ from Flour hqlquerypa0_ order by hqlquerypa0_.id offset @P1 rows fetch next @P2 rows only

So it seems to me that this must be a Panache-related problem.

Thank you very much for reviewing and I'll reach-out to some of the projects in the stack. Since the only HQL shown in the trace logs was FROM myproject.entities.Request WHERE requestor.firstName LIKE ?1, I assumed the addition of the top(?) was coming from the MS SQL implementation for setMaxResults(), however the SQL in your post shows it is using completely different syntax.

Still encountering the issue, I tried adding an order by, to have this HQL: FROM myproject.entities.Request WHERE requestor.firstName LIKE ?1 ORDER BY submitted

2021-08-05 16:20:37,992 DEBUG [org.hib.hql.int.ast.QueryTranslatorImpl] (vert.x-eventloop-thread-19) HQL: FROM myproject.entities.Request WHERE requestor.firstName LIKE ?1 ORDER BY submitted
2021-08-05 16:20:37,992 DEBUG [org.hib.hql.int.ast.QueryTranslatorImpl] (vert.x-eventloop-thread-19) SQL: select request0_.id as id1_1_, request0_.caseId as caseid2_1_, request0_.caseNumber as casenumb3_1_, request0_.expires as expires4_1_, request0_.addressLine1 as addressl5_1_, request0_.addressLine2 as addressl6_1_, request0_.agency as agency7_1_, request0_.city as city8_1_, request0_.email as email9_1_, request0_.firstName as firstna10_1_, request0_.lastName as lastnam11_1_, request0_.phone as phone12_1_, request0_.relationship as relatio13_1_, request0_.state as state14_1_, request0_.zipcode as zipcode15_1_, request0_.submitted as submitt16_1_ from Request request0_ where request0_.firstName like ? order by request0_.submitted

This attempted to execute the following SQL:

2021-08-05 16:20:38,022 DEBUG [org.hib.SQL] (vert.x-eventloop-thread-19)
    select
        request0_.id as id1_1_,
        request0_.caseId as caseid2_1_,
        request0_.caseNumber as casenumb3_1_,
        request0_.expires as expires4_1_,
        request0_.addressLine1 as addressl5_1_,
        request0_.addressLine2 as addressl6_1_,
        request0_.agency as agency7_1_,
        request0_.city as city8_1_,
        request0_.email as email9_1_,
        request0_.firstName as firstna10_1_,
        request0_.lastName as lastnam11_1_,
        request0_.phone as phone12_1_,
        request0_.relationship as relatio13_1_,
        request0_.state as state14_1_,
        request0_.zipcode as zipcode15_1_,
        request0_.submitted as submitt16_1_
    from
        Request request0_
    where
        request0_.firstName like @P1
    order by
        request0_.submitted offset 0 rows fetch next ? rows only

So... the same problem still exists in that it did not do the parameter substitution properly (now for the fetch next ? rows only), but that did change the generated SQL syntax to match your example.

Since Quarkus doesn't yet include hibernate-reactive-core 1.0.0.CR9, I specifically overrode the version in my pom for this and hibernate-core 5.5.5.Final. Could I be missing a dependency?

I looked at where I believe the Hibernate query is created in Panache io.quarkus.hibernate.reactive.panache.common.runtime.CommonPanacheQueryImpl:L254, it appears to use only setFirstResult and setMaxResults on the query. I looked at the query object in the debugger and at the end of that createQuery method, the underlying ReactiveQueryImpl has queryOptions containing firstRow=0 and maxRows=25.

Appreciate the help! (Understand I'm using a preview)

I think I found a bug, likely in SQLServerParameters.processLimit()

Panache is creating a hibernate query with firstRow=0.

Inside org.hibernate.reactive.loader.ReactiveLoader.executeReactiveQueryStatement, SQLServer2012LimitHandler.getOffsetFetch() gets called:

if ( !LimitHelper.hasFirstRow( selection ) ) {
    return " offset 0 rows fetch next ? rows only";
}
return " offset ? rows fetch next ? rows only";

Therefore, the SQL at this point ends with offset 0 rows fetch next ? rows only. Note the 0 instead of the ?.
From SQLServerParameters.processLimit():

        if (isProcessingNotRequired(sql)) {
            return sql;
        }

        // Replace 'offset ? fetch next ? rows only' with the @P style parameters for Sql Server
        int index = hasOffset ? parameterArray.length - 1 : parameterArray.length;
        int pos = sql.indexOf( " offset ?" );
        if ( pos > -1 ) {
            String sqlProcessed = sql.substring( 0, pos ) + " offset @P" + index++ + " rows";
            if ( sql.indexOf( " fetch next ?" ) > -1 ) {
                sqlProcessed += " fetch next @P" + index + " rows only ";
            }
            return sqlProcessed;
        }

        return sql;

The sql.indexOf( " offset ?") is the problem: Because the LimitHandler already hard-coded the offset to 0, the substring offset ? is not found, so it does not attempt to process any further parameters.
Should the nested string match looking for the fetch next ? be moved outside of the match for offset?

@bharward If you could create a test case that uses only Hibernate Reactive that would be much appreciated

I guess we will also have a look at one that uses Panache, but this repository is not the right place for those kind of issue

I suspect we are missing a test which sets only max results and not first result.

@gavinking and @DavideD - Thank you. It appears another test, one that would do _both_ setFirstResult(0) and setMaxResults(n) (n being any value > 0), would reproduce the error I'm seeing.

SQLServer2012LimitHandler.getOffsetFetch() creates different SQL when firstResult==0 ("offset 0 rows...") compared to when firstResult >0 ("offset ? rows");

If it would still be helpful, in a few days I could spend some time to either create a small HR-only example to reproduce, or modify HQLQueryParameterNamedLimitTest to add a test as I described.

Ah, you are right, this test will fail in HQLQueryParameterNamedLimitTest:

    @Test
    public void testFirstResultZeroAndMaxResults(TestContext context) {
        test(
                context,
                openSession()
                        .thenCompose( s ->
                                s.createQuery( "from Flour order by id" )
                                        .setFirstResult( 0 )
                                        .setMaxResults( 10 )
                                        .getResultList()
                                        .thenAccept( result -> {
                                            context.assertEquals( 3, result.size() );
                                        } )
                        )
        );
    }

I will have a look

@DavideD _in addition_ to the test you proposed, may I also suggest an additional test like this:

       @Test
    public void testFirstResultZeroAndMaxResultsWithoutOrderBy(TestContext context) {
        test(
                context,
                openSession()
                        .thenCompose( s ->
                                s.createQuery( "from Flour" )
                                        .setFirstResult( 0 )
                                        .setMaxResults( 10 )
                                        .getResultList()
                                        .thenAccept( result -> {
                                            context.assertEquals( 3, result.size() );
                                        } )
                        )
        );
    }

The difference is in removing the "order by". The reason is that SQLServer2005LimitHandler writes the query as select top(?) ... whenever the query does not include an "order by", but when it does it uses the offset ? fetch next ? syntax. So I think 2 test cases are needed.

Much appreciated!!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Xset-s picture Xset-s  ·  3Comments

DavideD picture DavideD  ·  31Comments

pqab picture pqab  ·  21Comments

arifpratama398 picture arifpratama398  ·  10Comments

gavinking picture gavinking  ·  6Comments