1+ using System . Data ;
2+ using System . Diagnostics ;
3+ using System . Diagnostics . CodeAnalysis ;
4+ using System . Globalization ;
5+ using System . Linq . Expressions ;
6+ using System . Reflection ;
7+ using System . Text ;
18using Microsoft . EntityFrameworkCore . Query ;
9+ using Microsoft . EntityFrameworkCore . Query . SqlExpressions ;
10+ using Microsoft . EntityFrameworkCore . Storage ;
11+ using ArgumentOutOfRangeException = System . ArgumentOutOfRangeException ;
212
313namespace EfCore . Ydb . Query . Internal ;
414
@@ -9,5 +19,289 @@ QueryableMethodTranslatingExpressionVisitor queryableMethodTranslatingExpression
919) : RelationalSqlTranslatingExpressionVisitor (
1020 dependencies ,
1121 queryCompilationContext ,
12- queryableMethodTranslatingExpressionVisitor
13- ) ;
22+ queryableMethodTranslatingExpressionVisitor )
23+ {
24+ private readonly QueryCompilationContext _queryCompilationContext = queryCompilationContext ;
25+
26+ private readonly YdbSqlExpressionFactory _sqlExpressionFactory =
27+ ( YdbSqlExpressionFactory ) dependencies . SqlExpressionFactory ;
28+
29+ private static readonly MethodInfo StringStartsWithMethod
30+ = typeof ( string ) . GetRuntimeMethod ( nameof ( string . StartsWith ) , [ typeof ( string ) ] ) ! ;
31+
32+ private static readonly MethodInfo StringEndsWithMethod
33+ = typeof ( string ) . GetRuntimeMethod ( nameof ( string . EndsWith ) , [ typeof ( string ) ] ) ! ;
34+
35+ private static readonly MethodInfo StringContainsMethod
36+ = typeof ( string ) . GetRuntimeMethod ( nameof ( string . Contains ) , [ typeof ( string ) ] ) ! ;
37+
38+ private static readonly MethodInfo EscapeLikePatternParameterMethod =
39+ typeof ( YdbSqlTranslatingExpressionVisitor ) . GetTypeInfo ( )
40+ . GetDeclaredMethod ( nameof ( ConstructLikePatternParameter ) ) ! ;
41+
42+
43+ protected override Expression VisitMethodCall ( MethodCallExpression methodCallExpression )
44+ {
45+ var method = methodCallExpression . Method ;
46+
47+ if ( method == StringStartsWithMethod
48+ && TryTranslateStartsEndsWithContains (
49+ methodCallExpression . Object ! ,
50+ methodCallExpression . Arguments [ 0 ] ,
51+ StartsEndsWithContains . StartsWith ,
52+ out var translation1 )
53+ )
54+ {
55+ return translation1 ;
56+ }
57+
58+ if ( method == StringEndsWithMethod
59+ && TryTranslateStartsEndsWithContains (
60+ methodCallExpression . Object ! ,
61+ methodCallExpression . Arguments [ 0 ] ,
62+ StartsEndsWithContains . EndsWith ,
63+ out var translation2 )
64+ )
65+ {
66+ return translation2 ;
67+ }
68+
69+ if ( method == StringContainsMethod
70+ && TryTranslateStartsEndsWithContains (
71+ methodCallExpression . Object ! ,
72+ methodCallExpression . Arguments [ 0 ] ,
73+ StartsEndsWithContains . Contains ,
74+ out var translation3 )
75+ )
76+ {
77+ return translation3 ;
78+ }
79+
80+ return base . VisitMethodCall ( methodCallExpression ) ;
81+ }
82+
83+ private bool TryTranslateStartsEndsWithContains (
84+ Expression instance ,
85+ Expression pattern ,
86+ StartsEndsWithContains methodType ,
87+ [ NotNullWhen ( true ) ] out SqlExpression ? translation
88+ )
89+ {
90+ if ( Visit ( instance ) is not SqlExpression translatedInstance
91+ || Visit ( pattern ) is not SqlExpression translatedPattern )
92+ {
93+ translation = null ;
94+ return false ;
95+ }
96+
97+ var stringTypeMapping = ExpressionExtensions . InferTypeMapping ( translatedInstance , translatedPattern ) ;
98+
99+ // UTF8 is DbType.String whereas STRING is DbType.Binary
100+ var isUtf8 = stringTypeMapping ? . DbType == DbType . String ;
101+
102+ translatedInstance = _sqlExpressionFactory . ApplyTypeMapping ( translatedInstance , stringTypeMapping ) ;
103+ translatedPattern = _sqlExpressionFactory . ApplyTypeMapping ( translatedPattern , stringTypeMapping ) ;
104+
105+ switch ( translatedPattern )
106+ {
107+ case SqlConstantExpression patternConstant :
108+ {
109+ translation = patternConstant . Value switch
110+ {
111+ null => _sqlExpressionFactory . Like (
112+ translatedInstance ,
113+ _sqlExpressionFactory . Constant ( null , typeof ( string ) , stringTypeMapping )
114+ ) ,
115+ "" => _sqlExpressionFactory . Like ( translatedInstance , _sqlExpressionFactory . Constant ( "%" ) ) ,
116+ string s => _sqlExpressionFactory . Like (
117+ translatedInstance ,
118+ _sqlExpressionFactory . Constant (
119+ methodType switch
120+ {
121+ StartsEndsWithContains . StartsWith => EscapeLikePattern ( s ) + '%' ,
122+ StartsEndsWithContains . EndsWith => '%' + EscapeLikePattern ( s ) ,
123+ StartsEndsWithContains . Contains => $ "%{ EscapeLikePattern ( s ) } %",
124+
125+ _ => throw new ArgumentOutOfRangeException ( nameof ( methodType ) , methodType , null )
126+ } ) ) ,
127+
128+ _ => throw new UnreachableException ( )
129+ } ;
130+
131+ return true ;
132+ }
133+
134+ case SqlParameterExpression patternParameter :
135+ {
136+ var lambda = Expression . Lambda (
137+ Expression . Call (
138+ EscapeLikePatternParameterMethod ,
139+ QueryCompilationContext . QueryContextParameter ,
140+ Expression . Constant ( patternParameter . Name ) ,
141+ Expression . Constant ( methodType ) ) ,
142+ QueryCompilationContext . QueryContextParameter ) ;
143+
144+ var escapedPatternParameter =
145+ _queryCompilationContext . RegisterRuntimeParameter (
146+ $ "{ patternParameter . Name } _{ methodType . ToString ( ) . ToLower ( CultureInfo . InvariantCulture ) } ",
147+ lambda ) ;
148+
149+ translation = _sqlExpressionFactory . Like (
150+ translatedInstance ,
151+ new SqlParameterExpression ( escapedPatternParameter . Name ! , escapedPatternParameter . Type ,
152+ stringTypeMapping ) ) ;
153+
154+ return true ;
155+ }
156+
157+ default :
158+ switch ( methodType )
159+ {
160+ case StartsEndsWithContains . StartsWith or StartsEndsWithContains . EndsWith :
161+ var substringArguments = new SqlExpression [ 3 ] ;
162+ substringArguments [ 0 ] = translatedInstance ;
163+ substringArguments [ 2 ] = _sqlExpressionFactory . Function (
164+ "len" ,
165+ [ translatedPattern ] ,
166+ nullable : true ,
167+ argumentsPropagateNullability : [ true ] ,
168+ typeof ( int )
169+ ) ;
170+
171+ if ( methodType == StartsEndsWithContains . StartsWith )
172+ {
173+ substringArguments [ 1 ] = _sqlExpressionFactory . Constant ( 1 ) ;
174+ }
175+ else
176+ {
177+ substringArguments [ 1 ] = _sqlExpressionFactory . Subtract (
178+ _sqlExpressionFactory . Function (
179+ "len" ,
180+ [ translatedInstance ] ,
181+ nullable : true ,
182+ argumentsPropagateNullability : [ true ] ,
183+ typeof ( int )
184+ ) ,
185+ _sqlExpressionFactory . Function (
186+ "len" ,
187+ [ translatedPattern ] ,
188+ nullable : true ,
189+ argumentsPropagateNullability : [ true ] ,
190+ typeof ( int )
191+ )
192+ ) ;
193+ }
194+
195+ var substringFunction = _sqlExpressionFactory . Function (
196+ "substring" ,
197+ substringArguments ,
198+ nullable : true ,
199+ argumentsPropagateNullability : [ true , false , false ] ,
200+ typeof ( string ) ,
201+ stringTypeMapping
202+ ) ;
203+
204+ translation = _sqlExpressionFactory . AndAlso (
205+ _sqlExpressionFactory . IsNotNull ( translatedInstance ) ,
206+ _sqlExpressionFactory . AndAlso (
207+ _sqlExpressionFactory . IsNotNull ( translatedPattern ) ,
208+ _sqlExpressionFactory . OrElse (
209+ _sqlExpressionFactory . Equal (
210+ isUtf8
211+ ? _sqlExpressionFactory . Function (
212+ "unwrap" ,
213+ [
214+ _sqlExpressionFactory . Convert (
215+ substringFunction ,
216+ typeof ( string ) ,
217+ typeMapping : StringTypeMapping . Default
218+ )
219+ ] ,
220+ nullable : false ,
221+ argumentsPropagateNullability : [ true ] ,
222+ typeof ( string )
223+ )
224+ : substringFunction ,
225+ translatedPattern
226+ ) ,
227+ _sqlExpressionFactory . Equal ( translatedPattern ,
228+ _sqlExpressionFactory . Constant ( string . Empty )
229+ )
230+ )
231+ )
232+ ) ;
233+ break ;
234+ case StartsEndsWithContains . Contains :
235+ translation =
236+ _sqlExpressionFactory . AndAlso (
237+ _sqlExpressionFactory . IsNotNull ( translatedInstance ) ,
238+ _sqlExpressionFactory . AndAlso (
239+ _sqlExpressionFactory . IsNotNull ( translatedPattern ) ,
240+ _sqlExpressionFactory . GreaterThan (
241+ _sqlExpressionFactory . Function (
242+ "strpos" , [ translatedInstance , translatedPattern ] , nullable : true ,
243+ argumentsPropagateNullability : [ true , true ] , typeof ( int ) ) ,
244+ _sqlExpressionFactory . Constant ( 0 ) ) ) ) ;
245+ break ;
246+
247+ default :
248+ throw new UnreachableException ( ) ;
249+ }
250+
251+ return true ;
252+ }
253+ }
254+
255+
256+ public enum StartsEndsWithContains
257+ {
258+ StartsWith ,
259+ EndsWith ,
260+ Contains
261+ }
262+
263+ public static string ? ConstructLikePatternParameter (
264+ QueryContext queryContext ,
265+ string baseParameterName ,
266+ StartsEndsWithContains methodType
267+ )
268+ => queryContext . ParameterValues [ baseParameterName ] switch
269+ {
270+ null => null ,
271+
272+ // In .NET, all strings start/end with the empty string, but SQL LIKE return false for empty patterns.
273+ // Return % which always matches instead.
274+ "" => "%" ,
275+
276+ string s => methodType switch
277+ {
278+ StartsEndsWithContains . StartsWith => EscapeLikePattern ( s ) + '%' ,
279+ StartsEndsWithContains . EndsWith => '%' + EscapeLikePattern ( s ) ,
280+ StartsEndsWithContains . Contains => $ "%{ EscapeLikePattern ( s ) } %",
281+ _ => throw new ArgumentOutOfRangeException ( nameof ( methodType ) , methodType , null )
282+ } ,
283+
284+ _ => throw new UnreachableException ( )
285+ } ;
286+
287+ private const char LikeEscapeChar = '\\ ' ;
288+
289+ private static bool IsLikeWildChar ( char c )
290+ => c is '%' or '_' ;
291+
292+ private static string EscapeLikePattern ( string pattern )
293+ {
294+ var builder = new StringBuilder ( ) ;
295+ foreach ( var c in pattern )
296+ {
297+ if ( IsLikeWildChar ( c ) || c == LikeEscapeChar )
298+ {
299+ builder . Append ( LikeEscapeChar ) ;
300+ }
301+
302+ builder . Append ( c ) ;
303+ }
304+
305+ return builder . ToString ( ) ;
306+ }
307+ }
0 commit comments