Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Microsoft.Azure.Cosmos.Linq
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
Expand Down Expand Up @@ -65,6 +66,10 @@ protected override SqlScalarExpression VisitImplicit(MethodCallExpression method
return null;
}

// In .NET 10+, the searchList may be wrapped in an op_Implicit conversion
// from T[] to ReadOnlySpan<T>. Unwrap it to get the underlying array constant.
searchList = UnwrapSpanImplicitConversion(searchList);

if (searchList.NodeType == ExpressionType.Constant)
{
return this.VisitIN(searchExpression, (ConstantExpression)searchList, context);
Expand All @@ -75,6 +80,29 @@ protected override SqlScalarExpression VisitImplicit(MethodCallExpression method
return SqlFunctionCallScalarExpression.CreateBuiltin("ARRAY_CONTAINS", array, expression);
}

/// <summary>
/// Unwraps an op_Implicit conversion from T[] to ReadOnlySpan&lt;T&gt; or Span&lt;T&gt;,
/// returning the inner array expression. Returns the original expression if not a Span conversion.
/// </summary>
private static Expression UnwrapSpanImplicitConversion(Expression expression)
{
if (expression is MethodCallExpression call
&& call.Method.Name == "op_Implicit"
&& call.Arguments.Count == 1)
{
Type declaringType = call.Method.DeclaringType;
if (declaringType != null
&& declaringType.IsGenericType
&& (declaringType.GetGenericTypeDefinition() == typeof(ReadOnlySpan<>)
|| declaringType.GetGenericTypeDefinition() == typeof(Span<>)))
{
return call.Arguments[0];
}
}

return expression;
}

private SqlScalarExpression VisitIN(Expression expression, ConstantExpression constantExpressionList, TranslationContext context)
{
List<SqlScalarExpression> items = new List<SqlScalarExpression>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@ public static SqlScalarExpression VisitBuiltinFunctionCall(MethodCallExpression
}

// Array functions
if (declaringType.IsEnumerable())
// Note: In .NET 10+, array.Contains() may resolve to MemoryExtensions.Contains(ReadOnlySpan<T>, T)
// ReadOnlySpan<T> does not implement IEnumerable<T>, so we also check for MemoryExtensions
if (declaringType.IsEnumerable() || IsMemoryExtensionsMethod(methodCallExpression))
{
return ArrayBuiltinFunctions.Visit(methodCallExpression, context);
}
Expand All @@ -119,6 +121,15 @@ public static SqlScalarExpression VisitBuiltinFunctionCall(MethodCallExpression
throw new DocumentQueryException(string.Format(CultureInfo.CurrentCulture, ClientResources.MethodNotSupported, methodCallExpression.Method.Name));
}

/// <summary>
/// Checks if the method is MemoryExtensions.Contains (used by .NET 10+ for array.Contains()).
/// </summary>
private static bool IsMemoryExtensionsMethod(MethodCallExpression methodCallExpression)
{
return methodCallExpression.Method.DeclaringType == typeof(MemoryExtensions)
&& methodCallExpression.Method.Name == "Contains";
}

protected abstract SqlScalarExpression VisitExplicit(MethodCallExpression methodCallExpression, TranslationContext context);

protected abstract SqlScalarExpression VisitImplicit(MethodCallExpression methodCallExpression, TranslationContext context);
Expand Down
21 changes: 21 additions & 0 deletions Microsoft.Azure.Cosmos/src/Linq/ConstantEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,27 @@ private static bool CanBeEvaluated(Expression expression)
{
return false;
}

// In .NET 10+, array.Contains() resolves to MemoryExtensions.Contains(ReadOnlySpan<T>, T)
// which involves an op_Implicit conversion from T[] to ReadOnlySpan<T>.
// ReadOnlySpan<T> is a ref struct and cannot be boxed into Expression.Constant,
// so we must prevent evaluation of both the MemoryExtensions.Contains call and its
// op_Implicit argument that converts T[] to ReadOnlySpan<T>.
if (type == typeof(MemoryExtensions) && methodCallExpression.Method.Name == "Contains")
{
return false;
}

// op_Implicit converting T[] to ReadOnlySpan<T>/Span<T> cannot be evaluated
// because ref structs cannot be boxed. This is the argument to MemoryExtensions.Contains.
if (methodCallExpression.Method.Name == "op_Implicit"
&& type != null
&& type.IsGenericType
&& (type.GetGenericTypeDefinition() == typeof(ReadOnlySpan<>)
Comment thread
adityasa marked this conversation as resolved.
|| type.GetGenericTypeDefinition() == typeof(Span<>)))
{
return false;
}
Comment thread
adityasa marked this conversation as resolved.
Comment thread
adityasa marked this conversation as resolved.
}

if (expression.NodeType == ExpressionType.Constant && expression.Type == typeof(object))
Expand Down
Loading
Loading