Skip to content

Strip no-op SQL CASTs#38156

Open
roji wants to merge 2 commits intomainfrom
roji/roji-fix-like-cast-value-converter
Open

Strip no-op SQL CASTs#38156
roji wants to merge 2 commits intomainfrom
roji/roji-fix-like-cast-value-converter

Conversation

@roji
Copy link
Copy Markdown
Member

@roji roji commented Apr 22, 2026

REVIEW 2ND COMMIT ONLY, THE 1ST IS PURE CLEANUP

Fixes #36247

When a value-converted type has an implicit conversion operator to its provider type (e.g. FullName -> string), the C# compiler inserts a Convert node that gets translated to an unnecessary CAST(column AS nvarchar(max)) even though the column already stores nvarchar(max). This produces suboptimal SQL, e.g.:

-- Before
SELECT ... WHERE CAST([u].[Name] AS nvarchar(max)) LIKE N'Name%'

-- After
SELECT ... WHERE [u].[Name] LIKE N'Name%'

Approach

The fix adds a post-processing step in SqlExpressionSimplifyingExpressionVisitor that strips SqlUnaryExpression(Convert) nodes where the Convert's store type matches the operand's store type — i.e. the CAST would be a no-op in SQL. This also catches other same-store-type no-op CASTs (e.g. CAST(AVG(float_column) AS REAL) on SQLite where both float and double map to REAL).

Alternative considered: fixing type inference in ApplyTypeMapping

We also explored fixing this earlier in the pipeline, by eliminating the Convert node during translation (in VisitUnary) and fixing type inference centrally in SqlExpressionFactory.ApplyTypeMapping. The idea was: when an inferred RelationalTypeMapping has a value converter whose model CLR type doesn't match the target expression's CLR type, the converter is inapplicable — so resolve a mapping for the expression's own CLR type instead.

This would avoid the unnecessary CAST node entirely (rather than creating it and stripping it in post-processing), and is arguably more principled. However, it requires constructing a new type mapping that preserves all the facets and provider-specific state of the original (store type, size, precision, scale, unicode, fixed-length, plus any custom state in provider subclasses). RelationalTypeMapping.Clone can't clear a converter (null means "keep existing"), and FindMapping(clrType, storeType) may not reproduce custom provider state. Since type mappings can be arbitrary provider-defined subclasses with arbitrary state, there's a risk of losing information. We chose not to go with this approach for now, as the post-processing approach is safe and correct.

@roji roji requested a review from a team as a code owner April 22, 2026 16:39
Copilot AI review requested due to automatic review settings April 22, 2026 16:39
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR removes redundant SQL CAST generation introduced by C# implicit conversions on value-converted properties by stripping no-op SqlUnaryExpression(Convert) nodes when the Convert’s store type matches the operand’s store type. This improves generated SQL (e.g., LIKE predicates) and also simplifies some aggregate SQL on SQLite.

Changes:

  • Add a simplification in SqlExpressionSimplifyingExpressionVisitor to remove no-op SQL CASTs for same-store-type Convert unary expressions.
  • Update SQLite AVG SQL baselines to reflect CAST removal where it is a no-op.
  • Add relational + provider-specific functional tests asserting EF.Functions.Like over a value-converted type doesn’t generate a CAST.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
src/EFCore.Relational/Query/Internal/SqlExpressionSimplifyingExpressionVisitor.cs Adds post-processing to strip no-op CAST/Convert SQL unary nodes when store types match.
test/EFCore.Relational.Specification.Tests/Query/AdHocMiscellaneousQueryRelationalTestBase.cs Adds a relational spec test covering Like on a value-converted type with implicit conversion to provider type.
test/EFCore.Sqlite.FunctionalTests/Query/AdHocMiscellaneousQuerySqliteTest.cs Updates AVG SQL baselines and adds provider override asserting no CAST for Like.
test/EFCore.SqlServer.FunctionalTests/Query/AdHocMiscellaneousQuerySqlServerTest.cs Adds provider override asserting no CAST for Like on SQL Server.

Comment thread src/EFCore.Relational/Query/Internal/SqlExpressionSimplifyingExpressionVisitor.cs Outdated
Comment thread src/EFCore.Relational/Query/Internal/SqlExpressionSimplifyingExpressionVisitor.cs Outdated
@roji roji force-pushed the roji/roji-fix-like-cast-value-converter branch 2 times, most recently from 0cb4f3c to 2ac5d4c Compare April 22, 2026 18:33
Copilot AI review requested due to automatic review settings April 22, 2026 18:33
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 22 changed files in this pull request and generated no new comments.

When a SqlUnaryExpression(Convert) has the same store type as its operand,
the CAST is a no-op in SQL. This occurs e.g. when a value-converted type has
an implicit conversion operator to its provider type (e.g. FullName -> string),
causing the C# compiler to insert a Convert node that gets translated to
CAST(column AS nvarchar(max)) even though the column already stores nvarchar(max).

Strip such no-op CASTs in post-processing, producing cleaner SQL.

Fixes #36247

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@roji roji force-pushed the roji/roji-fix-like-cast-value-converter branch from 2ac5d4c to e5e4ee1 Compare April 22, 2026 19:10
@roji roji changed the title Strip no-op SQL CASTs for value-converted types Strip no-op SQL CASTs Apr 22, 2026
Copy link
Copy Markdown
Member

@AndriySvyryd AndriySvyryd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like something with regression potential. Perhaps around JSON or primitive collections, but I don't have specific test cases in mind.

@roji
Copy link
Copy Markdown
Member Author

roji commented Apr 25, 2026

This seems like something with regression potential. Perhaps around JSON or primitive collections, but I don't have specific test cases in mind.

Yeah, I agree the change has a bit of risk associated with it; though if it causes a problem, I'd expect that to be more through the exposing of a bug elsewhere that was worked around by the extra CAST (like how the ISDATE function was incorrectly typed as returning bit(1), and this needed to be corrected as part of this PR).

Do you think it's worth preemptively doing an appcontext switch for opting out of the change? We don't usually do that, but it may be appropriate here. Or if you have anything else in mind which could help reduce the risk.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unnecessary SQL CAST when using EF.Functions.Like with strongly-typed value-converted types

3 participants