-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
CONCAT
of LIKE
with percentage (%
) literal fails for null
values
#3041
Comments
Can you please attach the SQL that is generated from the query? |
A little bit cropped SQL: select profileent0_."id" as id1_64_0_,
profiledev1_."id" as id1_68_1_,
profileent0_."blocked" as blocked5_64_0_,
profileent0_."coordinates_timezone" as coordina6_64_0_,
profileent0_."coordinates_updated" as coordina7_64_0_,
profileent0_."login" as dess_log9_64_0_,
profileent0_."first_name" as first_n11_64_0_,
profileent0_."gps_timezone" as gps_tim14_64_0_,
profileent0_."last_name" as last_na15_64_0_,
profileent0_."last_seen" as last_se16_64_0_,
profileent0_."latitude" as latitud17_64_0_,
profileent0_."longitude" as longitu18_64_0_,
profileent0_."preset" as preset20_64_0_,
profiledev1_."application" as applicat2_68_1_,
profiledev1_."version" as version3_68_1_,
profiledev1_."auth_domain_type" as auth_dom4_68_1_,
profiledev1_."created" as created5_68_1_,
profiledev1_."installment_id" as installm6_68_1_,
profiledev1_."last_device" as last_dev7_68_1_,
profiledev1_."last_seen" as last_see8_68_1_,
profiledev1_."mobile_platform" as mobile_p9_68_1_,
profiledev1_."platform_version" as platfor10_68_1_,
profiledev1_."profile_id" as profile11_68_1_
from "e2e_public"."profiles" profileent0_
left outer join "e2e_public"."profiles_devices" profiledev1_ on (profiledev1_."profile_id" = profileent0_."id")
where (profileent0_."last_seen">=? and profileent0_."last_seen"<=? or profileent0_."last_seen" is null)
and (? is null or profileent0_."id"=?)
and (? is null or profileent0_."login" like (?||'%'))
and (? is null or profileent0_."preset" like (?||'%'))
and (? is null or profileent0_."last_name"=?)
and (? is null or profileent0_."first_name"=?)
and (? is null or profiledev1_."application"=?)
and (? is null or profiledev1_."mobile_platform"=?)
and (profiledev1_."last_seen" is null or profiledev1_."last_device" = true)
order by profileent0_."id" DESC
limit ? so this lines are under question: and (? is null or profileent0_."login" like (?||'%'))
and (? is null or profileent0_."preset" like (?||'%')) |
Thanks a lot. |
Just to be clear, we used to take the wildcard and alter the input parameter by applying it. Thus, #2939 fixes the risk you face if you had Now we do I don't know if you can share you complete entity type as well as the arguments fed to that query. And I see you're using a projection, so that would be useful to know as well. |
Something else that could be at play is usage of Cyrillic alphabet as I see in the screenshot. I don't know if that's an issue at all or not, but if the data doesn't quite fit strings, the database could be trying to "help" by migrating to raw binary strings, and throwing everything for a loop. That would also be fitting in its warning to use a cast operation. |
Nothing interesting here. Entity: @Entity
@Table(name = "profiles")
@Getter
@Setter
@ToString
public class ProfileEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false, unique = true)
private Long id;
@Column(name = "login")
private String login;
@Column(name = "phone")
private String phone;
@Column(name = "email")
private String email;
@Column(name = "last_name")
private String lastName;
@Column(name = "first_name")
private String firstName;
@Column(name = "preset")
private String preset;
@Column(name = "last_seen")
private Instant lastSeen;
@Column(name = "latitude")
private double latitude;
@Column(name = "longitude")
private double longitude;
@Column(name = "coordinates_updated")
private Instant coordinatesUpdated;
@Column(name = "fis_timezone")
private Integer fisTimezone;
@Column(name = "coordinates_timezone")
private Integer coordinatesTimezone;
@Column(name = "gps_timezone")
private Integer gpsTimezone;
@Column(name = "blocked", nullable = false)
private boolean blocked;
} Filtering fields DTO: @lombok.Value
@lombok.Builder
@lombok.Jacksonized
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ProfileJournalFilter {
Long id; // nullable...
String login; // nullable!
String preset; // nullable!
@NotNull
@Builder.Default
Instant dateStart = TimeUtils.FILTER_DATE_TIME_START;
@NotNull
@Builder.Default
Instant dateEnd = TimeUtils.FILTER_DATE_TIME_END;
String lastName; // nullable!
String firstName; // nullable!
MobileApplication application; // it's enum, nullable
MobilePlatform platform; // // it's enum, nullable
} Projection: public interface AdminProfileProjection {
ProfileEntity getProfile();
ProfileDeviceEntity getProfileDevice();
} THe root of a problem is nullability of string fields in DTO. Before #2939 if ... AND NULL IS NULL OR presetr LIKE NULL ... but now it is: ... AND NULL IS NULL OR preset LIKE NULL || '%' so PostgreSQL cannot concatenate NULL with '%' because interprets it as bytea.
No, it is not the case. All strings are utf-8 so no problems with ascii or cyrillic chars or even multi-byte emojies. |
Thanks for the insight. That raises the general question how |
No any other logic on my side. I think implementation before #2939 just did that (unexpectedly) :) |
Our code previously made an assumption regarding representing null values in the context of I wonder whether we could isolate |
CONCAT
of LIKE
with percentage (%
) literal fails for null
values
I can look into this. I'm not quite certain where this happens, but it's worth a shot. |
To handle potential NULL values when LIKE is combined with wildcards, insert a COALESCE function. See #3041
Looks like transforming a LIKE with wildcards into a CONCAT function happens at a different point in time then when the arguments are fed in. Essentially, we can't tell that in advance if there will be However, JPA has the To illustrate:
This query, when a partial name is provided, will wrap it in wildcards. If null, the COALESCE function will collapse to wdyt @mp911de ? |
@gregturn I found this question on SO and it makes me doubt that simple Could it be solution: |
For my query I've found a solution: AND ( :#{#filter.preset} IS NULL OR pe.preset LIKE CONCAT(COALESCE(CAST(:#{#filter.preset} AS string), ''), '%')) or even AND (pe.preset LIKE CONCAT(COALESCE(CAST(:#{#filter.preset} AS string), ''), '%')) because right part began to support nullish values in filter (and in case |
I'll try the CAST operation. But it's true that if you have a |
To handle potential NULL values when LIKE is combined with wildcards, insert a COALESCE function. See #3041
To handle potential NULL values when LIKE is combined with wildcards, insert a COALESCE function. See #3041
Okay, it looks like |
It is a hard question =) My opinion is that it should be consistent with all implementations of the JPA and RDBMSes and work the same and documented. In fact, two options of concatenating null with '%' — casting to NULL and casting to '%' — are acceptable. But the fall of the request with an exception is undesirable... |
Hm, |
This implies we need a provider-oriented function that decides WHICH variant to apply. Basically, Hibernate ->
If you have If we had a data set of @Query("select e from Employee e where e.name LIKE '%:name%'")
List<Employee> findMatch(String name); that sort of rule should yield:
Does that sound fitting? |
For me it's an open question what is the most reasonable result for First variant looks like backward-compatible, second is a little bit more intuitive. I hope for your informed decision. |
To handle potential NULL values when LIKE is combined with wildcards, insert a COALESCE function. See #3041
To handle potential NULL values when LIKE is combined with wildcards, insert a COALESCE function. Also, apply a provider-specific CAST to parameters to ensure it doesn't transform into binary formats. See #3041
We now replace LIKE expressions according to their type with individual bindings if an existing binding cannot be used because of how the bound value is being transformed. WHERE foo like %:name or bar like :name becomes WHERE foo like :name (i.e. '%' + :name) or bar like :name_1 (i.e. :name) See #3041
We now distinguish between the binding parameter target and its origin. The parameter target represents how the binding is bound to the query, the origin points to where the binding parameter comes from (method invocation argument or an expression). The revised design removes the assumption that binding parameters must match their indices/names of the method call to introduce synthetic parameters for different binding variants while using the same underlying invocation parameters. See #3041
We now verify all bindings to ensure that a like-parameter doesn't mix up with plain bindings (e.g. firstname = %:myparam or lastname = :myparam). We also create unique synthetic parameters for positional bindings. Also, fix Regex to properly detect named, anonymous and positional bind markers. See #3041
Resolved. Merged to |
We now replace LIKE expressions according to their type with individual bindings if an existing binding cannot be used because of how the bound value is being transformed. WHERE foo like %:name or bar like :name becomes WHERE foo like :name (i.e. '%' + :name) or bar like :name_1 (i.e. :name) See #3041
We now distinguish between the binding parameter target and its origin. The parameter target represents how the binding is bound to the query, the origin points to where the binding parameter comes from (method invocation argument or an expression). The revised design removes the assumption that binding parameters must match their indices/names of the method call to introduce synthetic parameters for different binding variants while using the same underlying invocation parameters. See #3041
We now verify all bindings to ensure that a like-parameter doesn't mix up with plain bindings (e.g. firstname = %:myparam or lastname = :myparam). We also create unique synthetic parameters for positional bindings. Also, fix Regex to properly detect named, anonymous and positional bind markers. See #3041
We now replace LIKE expressions according to their type with individual bindings if an existing binding cannot be used because of how the bound value is being transformed. WHERE foo like %:name or bar like :name becomes WHERE foo like :name (i.e. '%' + :name) or bar like :name_1 (i.e. :name) See #3041
We now distinguish between the binding parameter target and its origin. The parameter target represents how the binding is bound to the query, the origin points to where the binding parameter comes from (method invocation argument or an expression). The revised design removes the assumption that binding parameters must match their indices/names of the method call to introduce synthetic parameters for different binding variants while using the same underlying invocation parameters. See #3041
We now verify all bindings to ensure that a like-parameter doesn't mix up with plain bindings (e.g. firstname = %:myparam or lastname = :myparam). We also create unique synthetic parameters for positional bindings. Also, fix Regex to properly detect named, anonymous and positional bind markers. See #3041
Backported to |
@gregturn hi, will it be backported to 2.7.15? |
@SimSonic I'm afraid not. There are simply too many differences between Spring Data JPA 2.7.x and 3.x to backport it that far. |
@gregturn how about us rolling back the conflicting change in Spring Data JPA 2.7.x? There are workarounds to omit named parameter reuse and judging from the impact of |
Hello! I have such repository method (simplified):
Columns are defined simply:
After I upgraded Spring Boot from 2.7.12 to 2.7.13 I've received failing test which calls this method. The error:
I've discovered that this is caused by this feature: #2939
How can I fix query? Or this is implementation bug?
The text was updated successfully, but these errors were encountered: