From 8a34cd94bf9ece4e8550324b2bb636366169b949 Mon Sep 17 00:00:00 2001 From: Sergei Pavlov Date: Thu, 9 Jan 2025 02:36:34 -0800 Subject: [PATCH 1/6] Process `ReadOnlySpan<>.Contains()` case in LINQ espression --- Orm/Xtensive.Orm.Tests/Linq/InTest.cs | 13 ++++++++++++ .../Orm/Linq/ExpressionExtensions.cs | 5 +++-- Orm/Xtensive.Orm/Orm/Linq/QueryHelper.cs | 3 ++- .../Orm/Linq/Translator.Expressions.cs | 21 ++++++++++++------- .../Orm/Linq/Translator.Queryable.cs | 12 +++++++++-- Orm/Xtensive.Orm/Reflection/WellKnownTypes.cs | 4 +++- 6 files changed, 44 insertions(+), 14 deletions(-) diff --git a/Orm/Xtensive.Orm.Tests/Linq/InTest.cs b/Orm/Xtensive.Orm.Tests/Linq/InTest.cs index c96709a252..3b9e03b92e 100644 --- a/Orm/Xtensive.Orm.Tests/Linq/InTest.cs +++ b/Orm/Xtensive.Orm.Tests/Linq/InTest.cs @@ -657,5 +657,18 @@ where track.Name.In(IncludeAlgorithm.TableValuedParameter, names) select track ).ToList(); } + + // Related to https://github.com/DataObjects-NET/dataobjects-net/issues/402 + [Test] + public void ReadOnlySpanContains_Test() + { + var result = ReadOnlySpanContains_GetCustomers(["Michelle", "Jack"]); + Assert.AreEqual(2, result.Count); + Assert.IsTrue(result.Contains("Michelle")); + Assert.IsTrue(result.Contains("Jack")); + } + + private List ReadOnlySpanContains_GetCustomers(string[] customerNames) => + (from c in Session.Query.All() where MemoryExtensions.Contains(customerNames, c.FirstName) select c.FirstName).ToList(); } } diff --git a/Orm/Xtensive.Orm/Orm/Linq/ExpressionExtensions.cs b/Orm/Xtensive.Orm/Orm/Linq/ExpressionExtensions.cs index 7e563615c3..cd5e8b2792 100644 --- a/Orm/Xtensive.Orm/Orm/Linq/ExpressionExtensions.cs +++ b/Orm/Xtensive.Orm/Orm/Linq/ExpressionExtensions.cs @@ -77,7 +77,8 @@ public static bool IsLocalCollection(this Expression expression, TranslatorConte var expressionType => !IsEntitySet(expressionType) && !expression.IsSubqueryExpression() && expressionType != WellKnownTypes.String - && expressionType.IsOfGenericInterface(WellKnownInterfaces.EnumerableOfT) + && (expressionType.IsOfGenericInterface(WellKnownInterfaces.EnumerableOfT) + || expressionType.IsOfGenericType(WellKnownTypes.ReadOnlySpanOfT)) && (IsEvaluableCollection(context, expression, expressionType) || IsForeignQuery(expression)) }; @@ -203,4 +204,4 @@ public static ParameterInfo[] GetConstructorParameters(this NewExpression expres return expression.Constructor.GetParameters(); } } -} \ No newline at end of file +} diff --git a/Orm/Xtensive.Orm/Orm/Linq/QueryHelper.cs b/Orm/Xtensive.Orm/Orm/Linq/QueryHelper.cs index 1214f57ddc..f8357609e7 100644 --- a/Orm/Xtensive.Orm/Orm/Linq/QueryHelper.cs +++ b/Orm/Xtensive.Orm/Orm/Linq/QueryHelper.cs @@ -202,7 +202,8 @@ public static void TryAddConvarianceCast(ref Expression source, Type baseType) public static Type GetSequenceElementType(Type type) { - var sequenceType = type.GetGenericInterface(WellKnownInterfaces.EnumerableOfT); + var sequenceType = type.GetGenericInterface(WellKnownInterfaces.EnumerableOfT) + ?? (type.IsOfGenericType(WellKnownTypes.ReadOnlySpanOfT) ? type : null); return sequenceType?.GetGenericArguments()[0]; } diff --git a/Orm/Xtensive.Orm/Orm/Linq/Translator.Expressions.cs b/Orm/Xtensive.Orm/Orm/Linq/Translator.Expressions.cs index 65b6a5688a..c7e1c58462 100644 --- a/Orm/Xtensive.Orm/Orm/Linq/Translator.Expressions.cs +++ b/Orm/Xtensive.Orm/Orm/Linq/Translator.Expressions.cs @@ -541,15 +541,20 @@ protected override Expression VisitMethodCall(MethodCallExpression mc) } } - // Process local collections - if (mc.Object.IsLocalCollection(context)) { - // IList.Contains - // List.Contains - // Array.Contains - var parameters = method.GetParameters(); - if (methodName == nameof(ICollection.Contains) && parameters.Length == 1) - return VisitContains(mc.Object, mc.Arguments[0], false); + if (methodName == nameof(ICollection.Contains)) { + if (mc.Object.IsLocalCollection(context)) { + // IList.Contains + // List.Contains + // Array.Contains + var parameters = method.GetParameters(); + if (parameters.Length == 1) + return VisitContains(mc.Object, mc.Arguments[0], false); + } + else if (methodDeclaringType == typeof(MemoryExtensions)) { + // ReadOnlySpan<>.Contains + return VisitContains(mc.Arguments[0], mc.Arguments[1], false); + } } var result = base.VisitMethodCall(mc); diff --git a/Orm/Xtensive.Orm/Orm/Linq/Translator.Queryable.cs b/Orm/Xtensive.Orm/Orm/Linq/Translator.Queryable.cs index 8e1c3c6366..e7834b331e 100644 --- a/Orm/Xtensive.Orm/Orm/Linq/Translator.Queryable.cs +++ b/Orm/Xtensive.Orm/Orm/Linq/Translator.Queryable.cs @@ -1767,9 +1767,17 @@ private ProjectionExpression VisitSequence(Expression sequenceExpression, Expres string.Format(Strings.ExExpressionXIsNotASequence, expressionPart.ToString(true))); } - private ProjectionExpression VisitLocalCollectionSequence(Expression sequence) => - CreateLocalCollectionProjectionExpression(typeof(TItem), ParameterAccessorFactory.CreateAccessorExpression>( + private ProjectionExpression VisitLocalCollectionSequence(Expression sequence) + { + var type = sequence.Type; + if (type.IsOfGenericType(WellKnownTypes.ReadOnlySpanOfT)) { + var methodToArray = type.GetMethod(nameof(ReadOnlySpan<>.ToArray)); + sequence = Expression.Call(sequence, methodToArray); + } + + return CreateLocalCollectionProjectionExpression(typeof(TItem), ParameterAccessorFactory.CreateAccessorExpression>( compiledQueryScope is not null ? compiledQueryScope.QueryParameterReplacer.Replace(sequence) : sequence).CachingCompile(), this, sequence); + } private Expression VisitContainsAny(Expression setA, Expression setB, bool isRoot, Type elementType) { diff --git a/Orm/Xtensive.Orm/Reflection/WellKnownTypes.cs b/Orm/Xtensive.Orm/Reflection/WellKnownTypes.cs index 8a2c1244f7..05cf26748d 100644 --- a/Orm/Xtensive.Orm/Reflection/WellKnownTypes.cs +++ b/Orm/Xtensive.Orm/Reflection/WellKnownTypes.cs @@ -81,5 +81,7 @@ internal static class WellKnownTypes public static readonly Type ObjectArray = typeof(object[]); public static readonly Type DefaultMemberAttribute = typeof(DefaultMemberAttribute); + + public static readonly Type ReadOnlySpanOfT = typeof(ReadOnlySpan<>); } -} \ No newline at end of file +} From c3a8fc2ce6b1aca239d661340d49a5311df85f78 Mon Sep 17 00:00:00 2001 From: Sergei Pavlov Date: Thu, 9 Jan 2025 02:45:49 -0800 Subject: [PATCH 2/6] Simplify --- Orm/Xtensive.Orm/Orm/Linq/Translator.Queryable.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Orm/Xtensive.Orm/Orm/Linq/Translator.Queryable.cs b/Orm/Xtensive.Orm/Orm/Linq/Translator.Queryable.cs index e7834b331e..ace68d5bb0 100644 --- a/Orm/Xtensive.Orm/Orm/Linq/Translator.Queryable.cs +++ b/Orm/Xtensive.Orm/Orm/Linq/Translator.Queryable.cs @@ -1771,8 +1771,7 @@ private ProjectionExpression VisitLocalCollectionSequence(Expression sequ { var type = sequence.Type; if (type.IsOfGenericType(WellKnownTypes.ReadOnlySpanOfT)) { - var methodToArray = type.GetMethod(nameof(ReadOnlySpan<>.ToArray)); - sequence = Expression.Call(sequence, methodToArray); + sequence = Expression.Call(sequence, type.GetMethod(nameof(ReadOnlySpan<>.ToArray))); } return CreateLocalCollectionProjectionExpression(typeof(TItem), ParameterAccessorFactory.CreateAccessorExpression>( From 809cd0fae49139c67f39eabe70b31b65cebff440 Mon Sep 17 00:00:00 2001 From: Sergei Pavlov Date: Thu, 9 Jan 2025 02:50:20 -0800 Subject: [PATCH 3/6] Fix .NET8 build --- Orm/Xtensive.Orm/Orm/Linq/Translator.Queryable.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Orm/Xtensive.Orm/Orm/Linq/Translator.Queryable.cs b/Orm/Xtensive.Orm/Orm/Linq/Translator.Queryable.cs index ace68d5bb0..611d7108e7 100644 --- a/Orm/Xtensive.Orm/Orm/Linq/Translator.Queryable.cs +++ b/Orm/Xtensive.Orm/Orm/Linq/Translator.Queryable.cs @@ -1771,7 +1771,7 @@ private ProjectionExpression VisitLocalCollectionSequence(Expression sequ { var type = sequence.Type; if (type.IsOfGenericType(WellKnownTypes.ReadOnlySpanOfT)) { - sequence = Expression.Call(sequence, type.GetMethod(nameof(ReadOnlySpan<>.ToArray))); + sequence = Expression.Call(sequence, type.GetMethod("ToArray")); } return CreateLocalCollectionProjectionExpression(typeof(TItem), ParameterAccessorFactory.CreateAccessorExpression>( From 4a2bba6b80ee01b182a47b3ea6d35bde87d6c934 Mon Sep 17 00:00:00 2001 From: Sergei Pavlov Date: Thu, 9 Jan 2025 03:40:09 -0800 Subject: [PATCH 4/6] Process Span<> case --- Orm/Xtensive.Orm/Orm/Linq/ExpressionExtensions.cs | 3 ++- Orm/Xtensive.Orm/Orm/Linq/QueryHelper.cs | 2 +- Orm/Xtensive.Orm/Orm/Linq/Translator.Queryable.cs | 2 +- Orm/Xtensive.Orm/Reflection/WellKnownTypes.cs | 1 + 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Orm/Xtensive.Orm/Orm/Linq/ExpressionExtensions.cs b/Orm/Xtensive.Orm/Orm/Linq/ExpressionExtensions.cs index cd5e8b2792..e286272103 100644 --- a/Orm/Xtensive.Orm/Orm/Linq/ExpressionExtensions.cs +++ b/Orm/Xtensive.Orm/Orm/Linq/ExpressionExtensions.cs @@ -78,7 +78,8 @@ public static bool IsLocalCollection(this Expression expression, TranslatorConte && !expression.IsSubqueryExpression() && expressionType != WellKnownTypes.String && (expressionType.IsOfGenericInterface(WellKnownInterfaces.EnumerableOfT) - || expressionType.IsOfGenericType(WellKnownTypes.ReadOnlySpanOfT)) + || expressionType.IsOfGenericType(WellKnownTypes.ReadOnlySpanOfT) + || expressionType.IsOfGenericType(WellKnownTypes.SpanOfT)) && (IsEvaluableCollection(context, expression, expressionType) || IsForeignQuery(expression)) }; diff --git a/Orm/Xtensive.Orm/Orm/Linq/QueryHelper.cs b/Orm/Xtensive.Orm/Orm/Linq/QueryHelper.cs index f8357609e7..e03e336d32 100644 --- a/Orm/Xtensive.Orm/Orm/Linq/QueryHelper.cs +++ b/Orm/Xtensive.Orm/Orm/Linq/QueryHelper.cs @@ -203,7 +203,7 @@ public static void TryAddConvarianceCast(ref Expression source, Type baseType) public static Type GetSequenceElementType(Type type) { var sequenceType = type.GetGenericInterface(WellKnownInterfaces.EnumerableOfT) - ?? (type.IsOfGenericType(WellKnownTypes.ReadOnlySpanOfT) ? type : null); + ?? (type.IsOfGenericType(WellKnownTypes.ReadOnlySpanOfT) || type.IsOfGenericType(WellKnownTypes.SpanOfT) ? type : null); return sequenceType?.GetGenericArguments()[0]; } diff --git a/Orm/Xtensive.Orm/Orm/Linq/Translator.Queryable.cs b/Orm/Xtensive.Orm/Orm/Linq/Translator.Queryable.cs index 611d7108e7..22517a5e49 100644 --- a/Orm/Xtensive.Orm/Orm/Linq/Translator.Queryable.cs +++ b/Orm/Xtensive.Orm/Orm/Linq/Translator.Queryable.cs @@ -1770,7 +1770,7 @@ private ProjectionExpression VisitSequence(Expression sequenceExpression, Expres private ProjectionExpression VisitLocalCollectionSequence(Expression sequence) { var type = sequence.Type; - if (type.IsOfGenericType(WellKnownTypes.ReadOnlySpanOfT)) { + if (type.IsOfGenericType(WellKnownTypes.ReadOnlySpanOfT) || type.IsOfGenericType(WellKnownTypes.SpanOfT)) { sequence = Expression.Call(sequence, type.GetMethod("ToArray")); } diff --git a/Orm/Xtensive.Orm/Reflection/WellKnownTypes.cs b/Orm/Xtensive.Orm/Reflection/WellKnownTypes.cs index 05cf26748d..82ad10f096 100644 --- a/Orm/Xtensive.Orm/Reflection/WellKnownTypes.cs +++ b/Orm/Xtensive.Orm/Reflection/WellKnownTypes.cs @@ -83,5 +83,6 @@ internal static class WellKnownTypes public static readonly Type DefaultMemberAttribute = typeof(DefaultMemberAttribute); public static readonly Type ReadOnlySpanOfT = typeof(ReadOnlySpan<>); + public static readonly Type SpanOfT = typeof(Span<>); } } From 8fcee172dc6736e001fcadc156d92ed8d8b70610 Mon Sep 17 00:00:00 2001 From: Sergei Pavlov Date: Thu, 9 Jan 2025 03:46:37 -0800 Subject: [PATCH 5/6] Correct test-sql.yaml --- .github/workflows/test-sql.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-sql.yaml b/.github/workflows/test-sql.yaml index 96221fc53b..0999baed07 100644 --- a/.github/workflows/test-sql.yaml +++ b/.github/workflows/test-sql.yaml @@ -19,7 +19,8 @@ jobs: timezoneLinux: "UTC" - name: Setup .NET uses: actions/setup-dotnet@v4 - with: { dotnet-version: 9 } + with: + dotnet-version: 9.x - name: Setup MSSQL Tools uses: rails-sqlserver/setup-mssql@v1 with: From 5ddda5b79bc575d170b09c43db533d305f061ddd Mon Sep 17 00:00:00 2001 From: Sergei Pavlov Date: Thu, 9 Jan 2025 10:47:52 -0800 Subject: [PATCH 6/6] Bump Version --- Version.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Version.props b/Version.props index a24dbafa6b..af84675e24 100644 --- a/Version.props +++ b/Version.props @@ -2,8 +2,8 @@ - 7.2.160 - servicetitan + 7.2.161 + servicetitan-span