Skip to content

Commit 26d84d9

Browse files
committed
Entity framework tests [1/?]
1 parent 0d9d6bd commit 26d84d9

32 files changed

+12775
-141
lines changed

src/EfCore.Ydb/src/Query/Internal/YdbSqlTranslatingExpressionVisitor.cs

Lines changed: 296 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
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;
18
using Microsoft.EntityFrameworkCore.Query;
9+
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
10+
using Microsoft.EntityFrameworkCore.Storage;
11+
using ArgumentOutOfRangeException = System.ArgumentOutOfRangeException;
212

313
namespace 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+
}

src/EfCore.Ydb/src/Storage/Internal/YdbDatabaseCreator.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ private async Task<bool> ExistsInternal(CancellationToken cancellationToken = de
3131
}
3232
finally
3333
{
34-
await connection1.CloseAsync().ConfigureAwait(false);
3534
await connection1.DisposeAsync().ConfigureAwait(false);
3635
}
3736
}

0 commit comments

Comments
 (0)