diff --git a/Mindbox.Data.Linq.Tests/SqlGeneration/ArrayContainsTranslationTests.cs b/Mindbox.Data.Linq.Tests/SqlGeneration/ArrayContainsTranslationTests.cs deleted file mode 100644 index 74d4c85..0000000 --- a/Mindbox.Data.Linq.Tests/SqlGeneration/ArrayContainsTranslationTests.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System; -using System.Data.Linq; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Mindbox.Data.Linq.Tests -{ - /// - /// Tests that array.Contains(value) in LINQ queries is correctly translated to SQL IN clause. - /// In .NET 10, the C# compiler generates MemoryExtensions.Contains(ReadOnlySpan, value) - /// for this pattern instead of Enumerable.Contains — this must produce the same SQL. - /// Two compiler patterns exist: - /// 1. MethodCallExpression(op_Implicit, array) — simple closure capture - /// 2. InvocationExpression(ConstantExpression(delegate), []) — pre-compiled closure - /// Both must be handled. - /// - [TestClass] - public class ArrayContainsTranslationTests - { - [TestMethod] - public void ArrayContains_TranslatesToSqlIn() - { - var ids = new[] { 1, 2, 3 }; - - using var connection = new DbConnectionStub(); - using var context = new DataContext(connection); - - var query = context.GetTable().Where(t => ids.Contains(t.Id)); - - using var command = context.GetCommand(query); - - Assert.AreEqual( - "SELECT [t0].[Id], [t0].[Discriminator], [t0].[X]" + Environment.NewLine + - "FROM [SimpleTable] AS [t0]" + Environment.NewLine + - "WHERE [t0].[Id] IN (@p0, @p1, @p2)", - command.CommandText); - } - - /// - /// Directly constructs the InvocationExpression(ConstantExpression(delegate), []) pattern - /// using Expression API — guaranteed to exercise Pattern 2 regardless of compiler version. - /// The MemoryExtensions.Contains call is built by hand with a pre-compiled zero-arg delegate - /// as the span argument, matching exactly what .NET 10 generates in nested closure contexts. - /// - [TestMethod] - public void ArrayContains_InvocationExpressionPattern_TranslatesToSqlIn() - { - var ids = new[] { 1, 2, 3 }; - - // Build MemoryExtensions.Contains(ReadOnlySpan, int) method - var containsMethod = typeof(MemoryExtensions) - .GetMethods(BindingFlags.Public | BindingFlags.Static) - .Single(m => m.Name == "Contains" - && m.IsGenericMethod - && m.GetParameters() is [{ ParameterType: var p0 }, { ParameterType: { IsGenericParameter: true } }] - && p0.IsGenericType - && p0.GetGenericTypeDefinition() == typeof(ReadOnlySpan<>)) - .MakeGenericMethod(typeof(int)); - - // Build op_Implicit: int[] → ReadOnlySpan - var opImplicit = typeof(ReadOnlySpan) - .GetMethod("op_Implicit", new[] { typeof(int[]) }); - - // Pattern 2: the compiler pre-compiles the implicit conversion into a zero-arg delegate - // InvocationExpression(ConstantExpression(Func>), []) - // Build: Func> spanFactory = () => op_Implicit(ids) - var spanFactoryBody = Expression.Call(opImplicit, Expression.Constant(ids)); - var spanFactory = Expression.Lambda(spanFactoryBody).Compile(); - var spanExpr = Expression.Invoke(Expression.Constant(spanFactory)); - - // Build: WHERE t.Id IN (ids) via MemoryExtensions.Contains(spanExpr, t.Id) - var tParam = Expression.Parameter(typeof(SimpleEntity), "t"); - var idMember = Expression.Property(tParam, nameof(SimpleEntity.Id)); - var containsCall = Expression.Call(containsMethod, spanExpr, idMember); - var predicate = Expression.Lambda>(containsCall, tParam); - - using var connection = new DbConnectionStub(); - using var context = new DataContext(connection); - - var query = context.GetTable().Where(predicate); - - using var command = context.GetCommand(query); - - Assert.AreEqual( - "SELECT [t0].[Id], [t0].[Discriminator], [t0].[X]" + Environment.NewLine + - "FROM [SimpleTable] AS [t0]" + Environment.NewLine + - "WHERE [t0].[Id] IN (@p0, @p1, @p2)", - command.CommandText); - } - - [TestMethod] - public void ArrayContains_EmptyArray_TranslatesToFalse() - { - var ids = Array.Empty(); - - using var connection = new DbConnectionStub(); - using var context = new DataContext(connection); - - var query = context.GetTable().Where(t => ids.Contains(t.Id)); - - using var command = context.GetCommand(query); - - Assert.AreEqual( - "SELECT [t0].[Id], [t0].[Discriminator], [t0].[X]" + Environment.NewLine + - "FROM [SimpleTable] AS [t0]" + Environment.NewLine + - "WHERE 0 = 1", - command.CommandText); - } - } -} diff --git a/Mindbox.Data.Linq/Mindbox.Data.Linq.csproj b/Mindbox.Data.Linq/Mindbox.Data.Linq.csproj index fc5788e..df1aeb4 100644 --- a/Mindbox.Data.Linq/Mindbox.Data.Linq.csproj +++ b/Mindbox.Data.Linq/Mindbox.Data.Linq.csproj @@ -8,7 +8,7 @@ true true snupkg - 10.8.1$(VersionTag) + 10.8.2$(VersionTag) SYSLIB0003;SYSLIB0011 diff --git a/Mindbox.Data.Linq/SqlClient/Query/QueryConverter.cs b/Mindbox.Data.Linq/SqlClient/Query/QueryConverter.cs index 557a3a2..bf2f79d 100644 --- a/Mindbox.Data.Linq/SqlClient/Query/QueryConverter.cs +++ b/Mindbox.Data.Linq/SqlClient/Query/QueryConverter.cs @@ -1774,54 +1774,6 @@ private SqlExpression GetAggregate(SqlNodeType aggType, Type clrType, SqlExpress return new SqlUnary(aggType, clrType, sqlType, exp, this.dominatingExpression); } - /// - /// Searches for an array of the given type in a delegate's closure. - /// Handles direct T[] fields (display class) and Object[] fields (runtime Closure), - /// including delegates nested inside Object[] fields (up to depth 3). - /// - private static object FindArrayInClosure(object target, Type arrayType, int depth = 0) { - if (target == null || depth > 3) { - return null; - } - foreach (var f in target.GetType().GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) { - var val = f.GetValue(target); - if (val?.GetType() == arrayType) { - return val; - } - if (val is object[] items) { - foreach (var item in items) { - if (item?.GetType() == arrayType) { - return item; - } - if (item is Delegate nested && nested.Target != null) { - var found = FindArrayInClosure(nested.Target, arrayType, depth + 1); - if (found != null) { - return found; - } - } - } - } - } - return null; - } - - private static Expression TryExtractArrayFromSpanExpression(Expression spanExpr) { - if (spanExpr is MethodCallExpression { Arguments: [var arrayExpr] }) { - return arrayExpr; - } - - if (spanExpr is InvocationExpression { Arguments.Count: 0, Expression: ConstantExpression { Value: Delegate { Target: { } target } } } - && spanExpr.Type.GetGenericArguments() is [var elementType]) { - var arrayType = elementType.MakeArrayType(); - var array = FindArrayInClosure(target, arrayType); - if (array != null) { - return Expression.Constant(array, arrayType); - } - } - - return null; - } - private SqlNode VisitContains(Expression sequence, Expression value) { Type elemType = TypeSystem.GetElementType(sequence.Type); SqlNode seqNode = this.Visit(sequence); @@ -1927,21 +1879,6 @@ private SqlNode VisitMethodCall(MethodCallExpression mc) { if (this.IsSequenceOperatorCall(mc)) { return this.VisitSequenceOperatorCall(mc); } - // In .NET 10, array.Contains(value) in expression trees compiles to - // MemoryExtensions.Contains(ReadOnlySpan span, value). - // Two compiler patterns exist for the span argument: - // 1. MethodCallExpression(op_Implicit, [array]) — simple local capture - // 2. InvocationExpression(ConstantExpression(pre_compiled_delegate), []) — nested closure - // Both must be unwrapped to extract the underlying array for SQL IN translation. - else if (mc.Method.DeclaringType == typeof(MemoryExtensions) - && mc.Method.Name == "Contains" - && mc.Arguments[0].Type is { IsGenericType: true } spanType - && spanType.GetGenericTypeDefinition() == typeof(ReadOnlySpan<>)) { - var arrayExpr = TryExtractArrayFromSpanExpression(mc.Arguments[0]); - if (arrayExpr != null) { - return this.VisitContains(arrayExpr, mc.Arguments[1]); - } - } else if (IsDataManipulationCall(mc)) { return this.VisitDataManipulationCall(mc); }