Skip to content

QueryEnhancerFactory with JSqlParser on classpath prevent using an {h-schema} placeholder in native queries #2564

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

Closed
valkuc opened this issue Jun 7, 2022 · 22 comments
Assignees
Labels
status: declined A suggestion or change that we don't feel we should currently apply

Comments

@valkuc
Copy link

valkuc commented Jun 7, 2022

Hello.
Using Spring Data 2.7.0 (from spring-data-bom version 2021.2.0) and having com.github.jsqlparser in the classpath prevent application to start if repositories use native queries with hibernate placeholders like h-schema. It failing early with net.sf.jsqlparser.JSQLParserException: Encountered unexpected token: "{" "{".
I did not find ability to turn off that feature. The decision to use JSqlParserQueryEnhancer instead of DefaultQueryEnhancer is made in static way in QueryEnhancerFactory.

My project is based on Spring Boot 2.6.8 and in order to mitigate this issue I had switched from shipped spring-data-bom version 2021.1.4 to 2021.2.0. This solved mentioned critical issue but introduced new one :(

Here is test project to help reproduce that issue. Just run CountryRepositoryTest.
Thanks in advance.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Jun 7, 2022
@DiegoKrupitza
Copy link
Contributor

Ok as far as I can see the main problem is that com.github.jsqlparser does not allow to use hibernate placeholders such as h-schema because this is not part of the SQL standard.

I dug a bit deeper to find a solution to that problem and it turns out your query SELECT c.* FROM {h-schema}countries c will not even work correctly with QueryUtils which is basically what DefaultQueryEnhancer uses.
For example the QueryUtils.createCountQueryFor("SELECT c.* FROM {h-schema}countries c",null);, which will be performed if you would call DefaultQueryEnhancer#createCountQueryFor(), does not return a correct count query (see screenshot 1).

image

If I replace {h-schema} with, let's say, public or delete it, the count query creation works perfectly fine (see screenshot 2&3).

image

image

As far as I can see using DefaultQueryEnhancer here can lead to other issues (because wrong count query is generated).

I identified two things that need to be changed/fixed:

  1. QueryUtils should support hibernate placeholders (if we even support this? @gregturn )
  2. Add some kind of configuration (maybe a property) in case the user wants to use DefaultQueryEnhancer over JSQLParserQueryEnhancer for native queries in case they have JSQLParser in classpath for other tasks.

I think Point 2 needs a decision from the members.

@valkuc
Copy link
Author

valkuc commented Jun 8, 2022

Thanks for advanced research! Regarding p.1 - yes it's definitely supported at least by sprint-data-jpa supplied with Spring Boot 2.5.x. Actually this issue happened when I decided to perform migration from SB 2.5 to 2.6

@DiegoKrupitza
Copy link
Contributor

DiegoKrupitza commented Jun 8, 2022

Thanks for advanced research! Regarding p.1 - yes it's definitely supported at least by sprint-data-jpa supplied with Spring Boot 2.5.x.

Yeah as far as I can see it does not fail because the count query is not used anywhere (yet).

When I checkout spring-projects/spring-data-jpa/2.5.x and add the following test case:

@Test
void hibernatePlaceHolderStringCountQuery() {
   assertCountQuery("SELECT c.* FROM {h-schema}countries c", "SELECT count(c.*) FROM {h-schema}countries c");
}

the count query is wrong. (Test case fails). So I think this should be fixed.

@vyemialyanchyk
Copy link

vyemialyanchyk commented Aug 25, 2022

I have the same issue and want to use DefaultQueryEnhancer without dig publicity into unnecessary details....

this is resolution for Kotlin, expect you'll fix the issue to avoid such workarounds ASAP:

    ByteBuddyAgent.install();
    ByteBuddy()
        .redefine(QueryEnhancerFactory::class.java)
        .method(named("isJSqlParserInClassPath"))
        .intercept(FixedValue.value(false))
        .make()
        .load(
            QueryEnhancerFactory::class.java.getClassLoader(),
            ClassReloadingStrategy.fromInstalledAgent()
        )

@valkuc
Copy link
Author

valkuc commented Aug 31, 2022

@DiegoKrupitza Hi. The issue is still reproducing in SB 2.7.3. Any ETA to fix it? It would be nice to have some configuration property to tell application either to use JSQL parser for query enhancer, or no.

@vyemialyanchyk Thanks for the snippet. Currently I solved it by placing my own implementation of QueryEnhancerFactory in the same package which return false in isJSqlParserInClassPath method.

@DiegoKrupitza
Copy link
Contributor

@DiegoKrupitza Hi. The issue is still reproducing in SB 2.7.3. Any ETA to fix it? It would be nice to have some configuration property to tell application either to use JSQL parser for query enhancer, or no.

Hey @valkuc,
in #2578 (comment) @gregturn mentioned:

But if this is going to involve "options", then I'm inclined to say this moves beyond the scope of anything we want to maintain directly inside Spring Data JPA.

I understood this as "a flag in the applications.properties is a no-go and if you can detect this without this its ok". The easiest implementation for this would be to introduce a flag but this is not the path to got.

In my opinion there are two ways on how to deal with this problem without introducing a flag:

  1. parse the string for special chars and fall back to DefaultImpl.
  2. pass the string to every "suitable and realistic" implementation and choose the best result.

(1.) is of course not a maintainable solutions and is obviously a bad decision.
(2.) is a better solution but the problem here is to decided which "suitable and realistic" to choose. If there are too many options, the performance will suffer, if there are not enough options we still have the problem that we do not find a good result.

Right now I can't say when I have time to work on this because I am really busy with uni and other stuff... But maybe you could try to come up with a solution based on my thoughts and create a PR ( one more contributor 🤩). I would be happy to look over it :)

@valkuc
Copy link
Author

valkuc commented Aug 31, 2022

@DiegoKrupitza Let me disagree with you :) I don't see anything bad in allowing developer to choose which query parser he want to use independently of what is located in the classpath. Let's take spring.jpa.database-platform for example. If this property is not set the actual value will be detected automatically. Why just not introduce something like spring.jpa.query-enhancer - if developer will not set it - then enhancer will be detected automatically (as it currently works). In my opinion this is much faster, performant and error-prone then doing analysis of every query for some specific chars/placeholders. I can work in this direction if you don't mind.

Just for info: we added JSQL parser in our project just to perform syntax analysis of sql queries entered by user in UI and we definitely do not want for it to be used in spring internals, but... there is no way to turn that off.

@valkuc
Copy link
Author

valkuc commented Sep 12, 2022

@DiegoKrupitza So, what is you opinion? Just want to be sure that if I will start working on my approach there will be a chance that I will not waste time :)

@DiegoKrupitza
Copy link
Contributor

DiegoKrupitza commented Sep 13, 2022

@DiegoKrupitza So, what is you opinion? Just want to be sure that if I will start working on my approach there will be a chance that I will not waste time :)

@valkuc I see your point and It makes sense but unfortunately I am not the one who decides this. I am just a normal contributer as you. Members of spring data jpa have to decide this.

Regarding disabling certain parsers with properties.. I have a counter example. Let's say we have multiple QueryEnhancer implementations (JSQLParser for SQL, Hibernate Parser for JPQL, EclipseLink for JPQL and the DefaultImpl using QueryUtils). Your approach must have configuration for all of them and ideally should not be hardcoded. Because maybe I want to use for JPQL the recommended Impl which is decided based on whats in classpath and only the DefaultImpl for native SQL queries. There has to be 2 configuration properties: One for SQL and one for JPQL. But now, what if I want to use EclipseLink over Hibernate for JPQL (and have both in classpath) how do you configure this? I think with the comment from @gregturn it was meant to avoid such complicated configuration.

@valkuc
Copy link
Author

valkuc commented Sep 13, 2022

@DiegoKrupitza Interesting... I got your point. Well, maybe this is a bit of "off-topic" but could you please guide me where I can find information about why JSQLParser support was introduced. Just want to understand, because for me current situation looks like: "hey, that functionality worked for years and now it's broken" (I'm about {h-schema} placeholder).

@schauder
Copy link
Contributor

@valkuc The reason to introduce the JSQLParser was that Spring Data JPA struggles with tons of problems resulting from failing to parse SQL properly.

That Hibernate supports stuff that looks almost like SQL, but isn't was surprise to everyone involved I guess.

@valkuc
Copy link
Author

valkuc commented Sep 13, 2022

Looks looks like the problem is even more wider. Having JSQLParser on the classpath makes it not possible to use SpEL in native queries. For example:

@Query(value = "SELECT c.* FROM countries c WHERE c.name = :#{#example.name}", nativeQuery = true)
List<Country> findCountries(Country example);

will fail with Caused by: net.sf.jsqlparser.JSQLParserException: Encountered unexpected token: "{" "{".

I guess that if JSQLParser was introduced to validate query syntax, maybe it should be invoked as a last processor, just before query will be sent to DB? But... I'm not sure this is possible in scope of Spring Data JPA as this means that query should be processed after it passed to underlying persistence provider (Hibernate for example).

DiegoKrupitza added a commit to DiegoKrupitza/spring-data-jpa that referenced this issue Sep 13, 2022
I tried to reproduce the error mentioned in spring-projects#2564 but I can't.
@DiegoKrupitza
Copy link
Contributor

DiegoKrupitza commented Sep 13, 2022

Looks looks like the problem is even more wider. Having JSQLParser on the classpath makes it not possible to use SpEL in native queries. For example:

@Query(value = "SELECT c.* FROM countries c WHERE c.name = :#{#example.name}", nativeQuery = true)
List<Country> findCountries(Country example);

will fail with Caused by: net.sf.jsqlparser.JSQLParserException: Encountered unexpected token: "{" "{".

I guess that if JSQLParser was introduced to validate query syntax, maybe it should be invoked as a last processor, just before query will be sent to DB? But... I'm not sure this is possible in scope of Spring Data JPA as this means that query should be processed after it passed to underlying persistence provider (Hibernate for example).

@valkuc
Ok so I tried to reproduce this error here DiegoKrupitza@98b53f9 but I works (See screenshots).

image

I hooked into the debugger to check whether it is really detected as a native query and saw that the query is rewritten before feeding it to the JSQLParserQueryEnhancer. Apparently JSQLParser accepts this placeholder, because it is in the format that is also used for prepared statements.

image

@valkuc
Copy link
Author

valkuc commented Sep 13, 2022

@DiegoKrupitza Looks like my fault. I did not removed original query which uses {h-schema} placeholder.
изображение
After removing first query, second query successfully completed.

DiegoKrupitza added a commit to DiegoKrupitza/spring-data-jpa that referenced this issue Sep 20, 2022
…ons fail.

In spring-projects#2564 we discovered that sometimes queries contain content that cannot be parsed by special implementation such as `JSqlParserQueryEnhancer`. Therefore, we implement a fallback mechanism that tries to use the first possible suitable implementation for a given query, that does not fail with an exception on creation. If the first one fails we fallback to the next one until we have reached the `DefaultQueryEnhancer`.

Closes spring-projects#2564
DiegoKrupitza added a commit to DiegoKrupitza/spring-data-jpa that referenced this issue Sep 20, 2022
In spring-projects#2564 we discovered that sometimes queries contain content that cannot be parsed by special implementation such as `JSqlParserQueryEnhancer`. Therefore, we implement a fallback mechanism that tries to use the first possible suitable implementation for a given query, that does not fail with an exception on creation. If the first one fails we fallback to the next one until we have reached the `DefaultQueryEnhancer`.

Closes spring-projects#2564
@DiegoKrupitza
Copy link
Contributor

I created a proposal for this in #2639 . It is now up to the members to decide whether this should be considers or not.

@kender1314
Copy link

Keywords also have this problem, such as sql contains 'binary'

@DiegoKrupitza
Copy link
Contributor

Keywords also have this problem, such as sql contains 'binary'

@kender1314 could you please provide an example query that has this problem?

@gregturn
Copy link
Contributor

What about my idea over at #2639 (comment) in response to @DiegoKrupitza 's proposal.

DiegoKrupitza added a commit to DiegoKrupitza/spring-data-jpa that referenced this issue Nov 8, 2022
With this commit it is possible to manually select which `@QueryEnhancer` should be used for a given query. Selection can be performed on method or type level.

Closes spring-projects#2564
@koszta5
Copy link

koszta5 commented Nov 18, 2022

I can confirm that this bug is indeed present in spring-data-jpa 2.7.3
If you use #{#entityName} instead of actual table name in your nativeQuery you will encounter this problem

@DiegoKrupitza
Copy link
Contributor

I can confirm that this bug is indeed present in spring-data-jpa 2.7.3 If you use #{#entityName} instead of actual table name in your nativeQuery you will encounter this problem

@koszta5 Could you provide a code snippet (method + all annotations + how you call it) and the stack trace you get, because I can not really reproduce the error.

gregturn pushed a commit that referenced this issue Jan 9, 2023
…ically selected `QueryEnhancer`.

Sometimes queryies contain content that cannot be pasrsed by special implementations such as `JSqlParserQueryEnhancer` (or other potential solutions). We implemented a means for developers to deliberately override the automatic choice with their own via annotation.

Resolves #2564.
gregturn pushed a commit that referenced this issue Jan 9, 2023
…ically selected `QueryEnhancer`.

Sometimes queryies contain content that cannot be pasrsed by special implementations such as `JSqlParserQueryEnhancer` (or other potential solutions). We implemented a means for developers to deliberately override the automatic choice with their own via annotation.

Resolves #2564.
gregturn pushed a commit that referenced this issue Jan 9, 2023
Sometimes the automatically chosen `QueryEnhancer` (e.g. `JSqlParserQueryEnhancer`) isn't the right one. Go through a series of choices, finding the first one that doesn't fail on query creation.

Also provide an override through `@QueryEnhancerOverride` that bypasses the selection process, supporting per-method as well as on the entire repository.

Resolves #2564.
gregturn pushed a commit that referenced this issue Jan 9, 2023
Sometimes the automatically chosen `QueryEnhancer` (e.g. `JSqlParserQueryEnhancer`) isn't the right one. Go through a series of choices, finding the first one that doesn't fail on query creation.

Also provide an override through `@QueryEnhancerOverride` that bypasses the selection process, supporting per-method as well as on the entire repository.

Resolves #2564.
gregturn pushed a commit that referenced this issue Jan 9, 2023
Sometimes the automatically chosen `QueryEnhancer` (e.g. `JSqlParserQueryEnhancer`) isn't the right one. Go through a series of choices, finding the first one that doesn't fail on query creation.

Also provide an override through `@QueryEnhancerOverride` that bypasses the selection process, supporting per-method as well as on the entire repository.

Resolves #2564.
gregturn pushed a commit that referenced this issue Jan 10, 2023
Sometimes the automatically chosen `QueryEnhancer` (e.g. `JSqlParserQueryEnhancer`) isn't the right one. Go through a series of choices, finding the first one that doesn't fail on query creation.

Also provide an override through `@QueryEnhancerOverride` that bypasses the selection process, supporting per-method as well as on the entire repository.

Resolves #2564.
gregturn pushed a commit that referenced this issue Jan 10, 2023
Sometimes the automatically chosen `QueryEnhancer` (e.g. `JSqlParserQueryEnhancer`) isn't the right one. Go through a series of choices, finding the first one that doesn't fail on query creation.

Also provide an override through `@QueryEnhancerOverride` that bypasses the selection process, supporting per-method as well as on the entire repository.

Resolves #2564.
@gregturn gregturn reopened this Jan 12, 2023
@gregturn
Copy link
Contributor

@DiegoKrupitza Sorry for pulling this change out. We appreciate your efforts. But further review has raised various questions, some of which are captured here.

For now, I've pulled these efforts aside to https://github.com/spring-projects/spring-data-jpa/tree/issue/gh-2564 so we can further review the scope of the problem (handling failing custom queries) and ensure the solution gives end-users a practical means to reach that solution.

In the mean time, the main branch has been reverted to its previous state.

gregturn pushed a commit that referenced this issue Jan 12, 2023
Sometimes the automatically chosen `QueryEnhancer` (e.g. `JSqlParserQueryEnhancer`) isn't the right one. Go through a series of choices, finding the first one that doesn't fail on query creation.

Also provide an override through `@QueryEnhancerOverride` that bypasses the selection process, available at the repository level and on a per-method basis.

Resolves #2564.
@gregturn gregturn changed the title QueryEnhancerFactory with JSqlParser on classpath prevent using hibernate placeholders in native queries QueryEnhancerFactory with JSqlParser on classpath prevent using an {h-schema} placeholder in native queries Apr 28, 2023
@gregturn
Copy link
Contributor

After further review, this {h-schema} is a feature we are not ready to support at this point in time.

If you are interested in something analogous, you can always implement it out-of-band to what Spring Data JPA offers by using currently supported bindings and variable names. You simply have to track the variable yourself and pass it along to the operations you have it implemented.

@gregturn gregturn closed this as not planned Won't fix, can't repro, duplicate, stale Apr 28, 2023
@gregturn gregturn added status: declined A suggestion or change that we don't feel we should currently apply and removed status: waiting-for-triage An issue we've not yet triaged labels Apr 28, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: declined A suggestion or change that we don't feel we should currently apply
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants