Skip to content

Commit d39b950

Browse files
committed
Support MemoryExtensions.Contains in LINQ filters
Fixes #12504
1 parent f2a295f commit d39b950

12 files changed

Lines changed: 328 additions & 6 deletions

File tree

dotnet/src/VectorData/AzureAISearch/AzureAISearchFilterTranslator.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,41 @@ private void TranslateMethodCall(MethodCallExpression methodCall)
187187
this.TranslateContains(source, item);
188188
return;
189189

190+
// C# 14 made changes to overload resolution to prefer Span-based overloads when those exist ("first-class spans");
191+
// this makes MemoryExtensions.Contains() be resolved rather than Enumerable.Contains() (see above).
192+
// MemoryExtensions.Contains() also accepts a Span argument for the source, adding an implicit cast we need to remove.
193+
// See https://github.com/dotnet/runtime/issues/109757 for more context.
194+
// Note that MemoryExtensions.Contains has an optional 3rd ComparisonType parameter; we only match when
195+
// it's null.
196+
case { Method.Name: nameof(MemoryExtensions.Contains), Arguments: [var spanArg, var item, ..] } contains
197+
when contains.Method.DeclaringType == typeof(MemoryExtensions)
198+
&& (contains.Arguments.Count is 2
199+
|| (contains.Arguments.Count is 3 && contains.Arguments[2] is ConstantExpression { Value: null }))
200+
&& TryUnwrapSpanImplicitCast(spanArg, out var source):
201+
this.TranslateContains(source, item);
202+
return;
203+
190204
default:
191205
throw new NotSupportedException($"Unsupported method call: {methodCall.Method.DeclaringType?.Name}.{methodCall.Method.Name}");
192206
}
207+
208+
static bool TryUnwrapSpanImplicitCast(Expression expression, [NotNullWhen(true)] out Expression? result)
209+
{
210+
if (expression is MethodCallExpression
211+
{
212+
Method: { Name: "op_Implicit", DeclaringType: { IsGenericType: true } implicitCastDeclaringType },
213+
Arguments: [var unwrapped]
214+
}
215+
&& implicitCastDeclaringType.GetGenericTypeDefinition() is var genericTypeDefinition
216+
&& (genericTypeDefinition == typeof(Span<>) || genericTypeDefinition == typeof(ReadOnlySpan<>)))
217+
{
218+
result = unwrapped;
219+
return true;
220+
}
221+
222+
result = null;
223+
return false;
224+
}
193225
}
194226

195227
private void TranslateContains(Expression source, Expression item)

dotnet/src/VectorData/Common/SqlFilterTranslator.cs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ private void TranslateMethodCall(MethodCallExpression methodCall, bool isSearchC
230230
{
231231
Method:
232232
{
233-
Name: nameof(Enumerable.Contains),
233+
Name: nameof(List<>.Contains),
234234
DeclaringType: { IsGenericType: true } declaringType
235235
},
236236
Object: Expression source,
@@ -239,9 +239,41 @@ private void TranslateMethodCall(MethodCallExpression methodCall, bool isSearchC
239239
this.TranslateContains(source, item);
240240
return;
241241

242+
// C# 14 made changes to overload resolution to prefer Span-based overloads when those exist ("first-class spans");
243+
// this makes MemoryExtensions.Contains() be resolved rather than Enumerable.Contains() (see above).
244+
// MemoryExtensions.Contains() also accepts a Span argument for the source, adding an implicit cast we need to remove.
245+
// See https://github.com/dotnet/runtime/issues/109757 for more context.
246+
// Note that MemoryExtensions.Contains has an optional 3rd ComparisonType parameter; we only match when
247+
// it's null.
248+
case { Method.Name: nameof(MemoryExtensions.Contains), Arguments: [var spanArg, var item, ..] } contains
249+
when contains.Method.DeclaringType == typeof(MemoryExtensions)
250+
&& (contains.Arguments.Count is 2
251+
|| (contains.Arguments.Count is 3 && contains.Arguments[2] is ConstantExpression { Value: null }))
252+
&& TryUnwrapSpanImplicitCast(spanArg, out var source):
253+
this.TranslateContains(source, item);
254+
return;
255+
242256
default:
243257
throw new NotSupportedException($"Unsupported method call: {methodCall.Method.DeclaringType?.Name}.{methodCall.Method.Name}");
244258
}
259+
260+
static bool TryUnwrapSpanImplicitCast(Expression expression, [NotNullWhen(true)] out Expression? result)
261+
{
262+
if (expression is MethodCallExpression
263+
{
264+
Method: { Name: "op_Implicit", DeclaringType: { IsGenericType: true } implicitCastDeclaringType },
265+
Arguments: [var unwrapped]
266+
}
267+
&& implicitCastDeclaringType.GetGenericTypeDefinition() is var genericTypeDefinition
268+
&& (genericTypeDefinition == typeof(Span<>) || genericTypeDefinition == typeof(ReadOnlySpan<>)))
269+
{
270+
result = unwrapped;
271+
return true;
272+
}
273+
274+
result = null;
275+
return false;
276+
}
245277
}
246278

247279
private void TranslateContains(Expression source, Expression item)

dotnet/src/VectorData/CosmosMongoDB/CosmosMongoFilterTranslator.cs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,8 @@ private BsonDocument TranslateNot(UnaryExpression not)
155155
}
156156

157157
private BsonDocument TranslateMethodCall(MethodCallExpression methodCall)
158-
=> methodCall switch
158+
{
159+
return methodCall switch
159160
{
160161
// Enumerable.Contains()
161162
{ Method.Name: nameof(Enumerable.Contains), Arguments: [var source, var item] } contains
@@ -171,11 +172,44 @@ private BsonDocument TranslateMethodCall(MethodCallExpression methodCall)
171172
},
172173
Object: Expression source,
173174
Arguments: [var item]
174-
} when declaringType.GetGenericTypeDefinition() == typeof(List<>) => this.TranslateContains(source, item),
175+
} when declaringType.GetGenericTypeDefinition() == typeof(List<>)
176+
=> this.TranslateContains(source, item),
177+
178+
// C# 14 made changes to overload resolution to prefer Span-based overloads when those exist ("first-class spans");
179+
// this makes MemoryExtensions.Contains() be resolved rather than Enumerable.Contains() (see above).
180+
// MemoryExtensions.Contains() also accepts a Span argument for the source, adding an implicit cast we need to remove.
181+
// See https://github.com/dotnet/runtime/issues/109757 for more context.
182+
// Note that MemoryExtensions.Contains has an optional 3rd ComparisonType parameter; we only match when
183+
// it's null.
184+
{ Method.Name: nameof(MemoryExtensions.Contains), Arguments: [var spanArg, var item, ..] } contains
185+
when contains.Method.DeclaringType == typeof(MemoryExtensions)
186+
&& (contains.Arguments.Count is 2
187+
|| (contains.Arguments.Count is 3 && contains.Arguments[2] is ConstantExpression { Value: null }))
188+
&& TryUnwrapSpanImplicitCast(spanArg, out var source)
189+
=> this.TranslateContains(source, item),
175190

176191
_ => throw new NotSupportedException($"Unsupported method call: {methodCall.Method.DeclaringType?.Name}.{methodCall.Method.Name}")
177192
};
178193

194+
static bool TryUnwrapSpanImplicitCast(Expression expression, [NotNullWhen(true)] out Expression? result)
195+
{
196+
if (expression is MethodCallExpression
197+
{
198+
Method: { Name: "op_Implicit", DeclaringType: { IsGenericType: true } implicitCastDeclaringType },
199+
Arguments: [var unwrapped]
200+
}
201+
&& implicitCastDeclaringType.GetGenericTypeDefinition() is var genericTypeDefinition
202+
&& (genericTypeDefinition == typeof(Span<>) || genericTypeDefinition == typeof(ReadOnlySpan<>)))
203+
{
204+
result = unwrapped;
205+
return true;
206+
}
207+
208+
result = null;
209+
return false;
210+
}
211+
}
212+
179213
private BsonDocument TranslateContains(Expression source, Expression item)
180214
{
181215
switch (source)

dotnet/src/VectorData/CosmosNoSql/CosmosNoSqlFilterTranslator.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,9 +230,41 @@ private void TranslateMethodCall(MethodCallExpression methodCall)
230230
this.TranslateContains(source, item);
231231
return;
232232

233+
// C# 14 made changes to overload resolution to prefer Span-based overloads when those exist ("first-class spans");
234+
// this makes MemoryExtensions.Contains() be resolved rather than Enumerable.Contains() (see above).
235+
// MemoryExtensions.Contains() also accepts a Span argument for the source, adding an implicit cast we need to remove.
236+
// See https://github.com/dotnet/runtime/issues/109757 for more context.
237+
// Note that MemoryExtensions.Contains has an optional 3rd ComparisonType parameter; we only match when
238+
// it's null.
239+
case { Method.Name: nameof(MemoryExtensions.Contains), Arguments: [var spanArg, var item, ..] } contains
240+
when contains.Method.DeclaringType == typeof(MemoryExtensions)
241+
&& (contains.Arguments.Count is 2
242+
|| (contains.Arguments.Count is 3 && contains.Arguments[2] is ConstantExpression { Value: null }))
243+
&& TryUnwrapSpanImplicitCast(spanArg, out var source):
244+
this.TranslateContains(source, item);
245+
return;
246+
233247
default:
234248
throw new NotSupportedException($"Unsupported method call: {methodCall.Method.DeclaringType?.Name}.{methodCall.Method.Name}");
235249
}
250+
251+
static bool TryUnwrapSpanImplicitCast(Expression expression, [NotNullWhen(true)] out Expression? result)
252+
{
253+
if (expression is MethodCallExpression
254+
{
255+
Method: { Name: "op_Implicit", DeclaringType: { IsGenericType: true } implicitCastDeclaringType },
256+
Arguments: [var unwrapped]
257+
}
258+
&& implicitCastDeclaringType.GetGenericTypeDefinition() is var genericTypeDefinition
259+
&& (genericTypeDefinition == typeof(Span<>) || genericTypeDefinition == typeof(ReadOnlySpan<>)))
260+
{
261+
result = unwrapped;
262+
return true;
263+
}
264+
265+
result = null;
266+
return false;
267+
}
236268
}
237269

238270
private void TranslateContains(Expression source, Expression item)

dotnet/src/VectorData/Directory.Build.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
44

55
<PropertyGroup>
6+
<LangVersion>latest</LangVersion>
67
<NoWarn>$(NoWarn);MEVD9000,MEVD9001</NoWarn> <!-- Experimental MEVD connector-facing APIs -->
78
</PropertyGroup>
89

dotnet/src/VectorData/MongoDB/MongoFilterTranslator.cs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,8 @@ private BsonDocument TranslateNot(UnaryExpression not)
161161
}
162162

163163
private BsonDocument TranslateMethodCall(MethodCallExpression methodCall)
164-
=> methodCall switch
164+
{
165+
return methodCall switch
165166
{
166167
// Enumerable.Contains()
167168
{ Method.Name: nameof(Enumerable.Contains), Arguments: [var source, var item] } contains
@@ -179,9 +180,41 @@ private BsonDocument TranslateMethodCall(MethodCallExpression methodCall)
179180
Arguments: [var item]
180181
} when declaringType.GetGenericTypeDefinition() == typeof(List<>) => this.TranslateContains(source, item),
181182

183+
// C# 14 made changes to overload resolution to prefer Span-based overloads when those exist ("first-class spans");
184+
// this makes MemoryExtensions.Contains() be resolved rather than Enumerable.Contains() (see above).
185+
// MemoryExtensions.Contains() also accepts a Span argument for the source, adding an implicit cast we need to remove.
186+
// See https://github.com/dotnet/runtime/issues/109757 for more context.
187+
// Note that MemoryExtensions.Contains has an optional 3rd ComparisonType parameter; we only match when
188+
// it's null.
189+
{ Method.Name: nameof(MemoryExtensions.Contains), Arguments: [var spanArg, var item, ..] } contains
190+
when contains.Method.DeclaringType == typeof(MemoryExtensions)
191+
&& (contains.Arguments.Count is 2
192+
|| (contains.Arguments.Count is 3 && contains.Arguments[2] is ConstantExpression { Value: null }))
193+
&& TryUnwrapSpanImplicitCast(spanArg, out var source)
194+
=> this.TranslateContains(source, item),
195+
182196
_ => throw new NotSupportedException($"Unsupported method call: {methodCall.Method.DeclaringType?.Name}.{methodCall.Method.Name}")
183197
};
184198

199+
static bool TryUnwrapSpanImplicitCast(Expression expression, [NotNullWhen(true)] out Expression? result)
200+
{
201+
if (expression is MethodCallExpression
202+
{
203+
Method: { Name: "op_Implicit", DeclaringType: { IsGenericType: true } implicitCastDeclaringType },
204+
Arguments: [var unwrapped]
205+
}
206+
&& implicitCastDeclaringType.GetGenericTypeDefinition() is var genericTypeDefinition
207+
&& (genericTypeDefinition == typeof(Span<>) || genericTypeDefinition == typeof(ReadOnlySpan<>)))
208+
{
209+
result = unwrapped;
210+
return true;
211+
}
212+
213+
result = null;
214+
return false;
215+
}
216+
}
217+
185218
private BsonDocument TranslateContains(Expression source, Expression item)
186219
{
187220
switch (source)

dotnet/src/VectorData/Pinecone/PineconeFilterTranslator.cs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,8 @@ private Metadata TranslateNot(UnaryExpression not)
158158
}
159159

160160
private Metadata TranslateMethodCall(MethodCallExpression methodCall)
161-
=> methodCall switch
161+
{
162+
return methodCall switch
162163
{
163164
// Enumerable.Contains()
164165
{ Method.Name: nameof(Enumerable.Contains), Arguments: [var source, var item] } contains
@@ -176,9 +177,41 @@ private Metadata TranslateMethodCall(MethodCallExpression methodCall)
176177
Arguments: [var item]
177178
} when declaringType.GetGenericTypeDefinition() == typeof(List<>) => this.TranslateContains(source, item),
178179

180+
// C# 14 made changes to overload resolution to prefer Span-based overloads when those exist ("first-class spans");
181+
// this makes MemoryExtensions.Contains() be resolved rather than Enumerable.Contains() (see above).
182+
// MemoryExtensions.Contains() also accepts a Span argument for the source, adding an implicit cast we need to remove.
183+
// See https://github.com/dotnet/runtime/issues/109757 for more context.
184+
// Note that MemoryExtensions.Contains has an optional 3rd ComparisonType parameter; we only match when
185+
// it's null.
186+
{ Method.Name: nameof(MemoryExtensions.Contains), Arguments: [var spanArg, var item, ..] } contains
187+
when contains.Method.DeclaringType == typeof(MemoryExtensions)
188+
&& (contains.Arguments.Count is 2
189+
|| (contains.Arguments.Count is 3 && contains.Arguments[2] is ConstantExpression { Value: null }))
190+
&& TryUnwrapSpanImplicitCast(spanArg, out var source)
191+
=> this.TranslateContains(source, item),
192+
179193
_ => throw new NotSupportedException($"Unsupported method call: {methodCall.Method.DeclaringType?.Name}.{methodCall.Method.Name}")
180194
};
181195

196+
static bool TryUnwrapSpanImplicitCast(Expression expression, [NotNullWhen(true)] out Expression? result)
197+
{
198+
if (expression is MethodCallExpression
199+
{
200+
Method: { Name: "op_Implicit", DeclaringType: { IsGenericType: true } implicitCastDeclaringType },
201+
Arguments: [var unwrapped]
202+
}
203+
&& implicitCastDeclaringType.GetGenericTypeDefinition() is var genericTypeDefinition
204+
&& (genericTypeDefinition == typeof(Span<>) || genericTypeDefinition == typeof(ReadOnlySpan<>)))
205+
{
206+
result = unwrapped;
207+
return true;
208+
}
209+
210+
result = null;
211+
return false;
212+
}
213+
}
214+
182215
private Metadata TranslateContains(Expression source, Expression item)
183216
{
184217
switch (source)

dotnet/src/VectorData/Qdrant/QdrantFilterTranslator.cs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,8 @@ private Filter TranslateNot(Expression expression)
267267
#endregion Logical operators
268268

269269
private Filter TranslateMethodCall(MethodCallExpression methodCall)
270-
=> methodCall switch
270+
{
271+
return methodCall switch
271272
{
272273
// Enumerable.Contains()
273274
{ Method.Name: nameof(Enumerable.Contains), Arguments: [var source, var item] } contains
@@ -286,9 +287,41 @@ private Filter TranslateMethodCall(MethodCallExpression methodCall)
286287
} when declaringType.GetGenericTypeDefinition() == typeof(List<>)
287288
=> this.TranslateContains(source, item),
288289

290+
// C# 14 made changes to overload resolution to prefer Span-based overloads when those exist ("first-class spans");
291+
// this makes MemoryExtensions.Contains() be resolved rather than Enumerable.Contains() (see above).
292+
// MemoryExtensions.Contains() also accepts a Span argument for the source, adding an implicit cast we need to remove.
293+
// See https://github.com/dotnet/runtime/issues/109757 for more context.
294+
// Note that MemoryExtensions.Contains has an optional 3rd ComparisonType parameter; we only match when
295+
// it's null.
296+
{ Method.Name: nameof(MemoryExtensions.Contains), Arguments: [var spanArg, var item, ..] } contains
297+
when contains.Method.DeclaringType == typeof(MemoryExtensions)
298+
&& (contains.Arguments.Count is 2
299+
|| (contains.Arguments.Count is 3 && contains.Arguments[2] is ConstantExpression { Value: null }))
300+
&& TryUnwrapSpanImplicitCast(spanArg, out var source)
301+
=> this.TranslateContains(source, item),
302+
289303
_ => throw new NotSupportedException($"Unsupported method call: {methodCall.Method.DeclaringType?.Name}.{methodCall.Method.Name}")
290304
};
291305

306+
static bool TryUnwrapSpanImplicitCast(Expression expression, [NotNullWhen(true)] out Expression? result)
307+
{
308+
if (expression is MethodCallExpression
309+
{
310+
Method: { Name: "op_Implicit", DeclaringType: { IsGenericType: true } implicitCastDeclaringType },
311+
Arguments: [var unwrapped]
312+
}
313+
&& implicitCastDeclaringType.GetGenericTypeDefinition() is var genericTypeDefinition
314+
&& (genericTypeDefinition == typeof(Span<>) || genericTypeDefinition == typeof(ReadOnlySpan<>)))
315+
{
316+
result = unwrapped;
317+
return true;
318+
}
319+
320+
result = null;
321+
return false;
322+
}
323+
}
324+
292325
private Filter TranslateContains(Expression source, Expression item)
293326
{
294327
switch (source)

0 commit comments

Comments
 (0)