-
Notifications
You must be signed in to change notification settings - Fork 3.2k
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
FunctionPreprocessingExpressionVisitor causes early assignment of default type mapping #19120
Comments
I am closing this as not needed because the visitor is not main cause of the issue. The main cause of the issue is same parameter with multiple different type mappings. Filed #19503 to track it.
|
I must have missed it - can one of you guys quickly explain? This is also a good example of why a sentence or two in the XML docs could help (i.e. #19415).
Remember SQL doesn't guarantee evaluation order unlike C#... Are we sure this is really relevant?
It's true that #19503 remains regardless of this. Another potential fix to this is #17598 which would obviate this (since we could generate different SQL for empty-space parameters). |
I am not referring to short circuiting behavior of SQL, it is of null semantics. So yes, order of terms this visitor adds is highly relevant and necessary otherwise it causes incorrect results. |
I guess I'll wait for the full explanation then. |
Function preprocessor is mainly to add null checks before the translation. The code that compares parameters to empty strings can be moved to translation. Example of the query that gets affected by null semantics: from e1 in ss.Set<Level1>()
where e1.OneToOne_Optional_FK1.Name.StartsWith(e1.OneToOne_Optional_FK1.Name) when preprocessor injects null check we produce the following sql: SELECT [l].[Id], [l].[Date], [l].[Name], [l].[OneToMany_Optional_Self_Inverse1Id], [l].[OneToMany_Required_Self_Inverse1Id], [l].[OneToOne_Optional_Self1Id]
FROM [LevelOne] AS [l]
LEFT JOIN [LevelTwo] AS [l0] ON [l].[Id] = [l0].[Level1_Optional_Id]
WHERE ([l0].[Name] = N'') OR ([l0].[Name] IS NOT NULL AND (LEFT([l0].[Name], LEN([l0].[Name])) = [l0].[Name])) and when it doesnt (and null semantics runs): SELECT [l].[Id], [l].[Date], [l].[Name], [l].[OneToMany_Optional_Self_Inverse1Id], [l].[OneToMany_Required_Self_Inverse1Id], [l].[OneToOne_Optional_Self1Id]
FROM [LevelOne] AS [l]
LEFT JOIN [LevelTwo] AS [l0] ON [l].[Id] = [l0].[Level1_Optional_Id]
WHERE ([l0].[Name] = N'') OR ((LEFT([l0].[Name], LEN([l0].[Name])) = [l0].[Name]) OR (LEFT([l0].[Name], LEN([l0].[Name])) IS NULL AND [l0].[Name] IS NULL)) The predicate in second query returns true when both sides are null (e.g. when the navigation is null, so we propagate null value to it's Name property). Similar thing happens (and we have similar mechanism of adding null check) for collection navigations
would get translated to: entity.Select(e => (from ce in CollectionElements
where ce.FK == e.Navigation.PK
select ce) if so happens that ce.FK is null and the e.Navigation is null (therefore the null value propagates to PK) - null semantics would rewrite the query in such a way that those elements would get returned. This is not what we want (only want to create collections for navigations that are actually not null) - so we add a similar null check: entity.Select(e => (from ce in CollectionElements
where e.Navigation.PK != null && ce.FK == e.Navigation.PK
select ce) Now, the reason why we can't add those null checks as part of the function translation is (as always, when it comes to null semantics) the negated case. For negated case we add the null checks just the same: normal case: a.StartsWith(b) -> a != null && b != null && a.StartsWith(b) negated case: !a.StartsWith(b) -> a != null && b != null && !a.StartsWith(b) That's why we override VisitUnary and handle the case separately. !a.StartsWith(b) -> !(a != null && b != null && a.StartsWith(b)) --de morgan-> a == null || b == null || !a.StartsWith(b) Which is not what we want :( |
While investigating an issue with case-insensitive StartsWith in Npgsql (npgsql/efcore.pg#388), I ran into some problematic behavior.
In PostgreSQL, case-sensitive strings are represented as a special store type (citext). In order to have proper case-insensitive comparisons, both sides of a comparison expression must be properly typed as citext (
SELECT CAST('HELLO' AS CITEXT) = CAST('hello' AS TEXT)
is false). Now, when the pattern given to StartsWith is a parameter, my string method translator does proper type inference and applies the citext type mapping.However, FunctionPreprocessingExpressionVisitor detects StartsWith/EndsWith, and introduces a check for whether the pattern is an empty string. Since there is no type inference here, the parameter expression will get whatever the default is for the CLR type (so it's text and not citext). And since this is the first occurrence of the parameter expression encountered by QuerySqlGenerator, the parameter gets configured as a text parameter instead of a citext parameter.
An easy fix for this would be to simply switch the order in the inserted conjunection (i.e. add the empty string check on the right side instead of on the left side). However, I'm wondering why we need this very special casing of StartsWith/EndsWith in a preprocessor rather than handling this logic inside the method translator, as we always do... I can see @maumar introduced this in dbfa82a for null semantics, but maybe we can clean/simplify this.
In the meantime I'll work around the issue by adding SQL casts back to citext on the parameter.
The text was updated successfully, but these errors were encountered: