diff --git a/Microsoft.Azure.Cosmos/src/Linq/BuiltinFunctions/ArrayBuiltinFunctions.cs b/Microsoft.Azure.Cosmos/src/Linq/BuiltinFunctions/ArrayBuiltinFunctions.cs index d96ac56a22..cb4fd58a7b 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/BuiltinFunctions/ArrayBuiltinFunctions.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/BuiltinFunctions/ArrayBuiltinFunctions.cs @@ -35,7 +35,7 @@ protected override SqlScalarExpression VisitImplicit(MethodCallExpression method } } - private class ArrayContainsVisitor : SqlBuiltinFunctionVisitor + internal class ArrayContainsVisitor : SqlBuiltinFunctionVisitor { public ArrayContainsVisitor() : base("ARRAY_CONTAINS", true, null) @@ -75,7 +75,7 @@ protected override SqlScalarExpression VisitImplicit(MethodCallExpression method return SqlFunctionCallScalarExpression.CreateBuiltin("ARRAY_CONTAINS", array, expression); } - private SqlScalarExpression VisitIN(Expression expression, ConstantExpression constantExpressionList, TranslationContext context) + internal SqlScalarExpression VisitIN(Expression expression, ConstantExpression constantExpressionList, TranslationContext context) { List items = new List(); foreach (object item in (IEnumerable)constantExpressionList.Value) diff --git a/Microsoft.Azure.Cosmos/src/Linq/BuiltinFunctions/BuiltinFunctionVisitor.cs b/Microsoft.Azure.Cosmos/src/Linq/BuiltinFunctions/BuiltinFunctionVisitor.cs index 3008b3eac9..8f36452840 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/BuiltinFunctions/BuiltinFunctionVisitor.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/BuiltinFunctions/BuiltinFunctionVisitor.cs @@ -11,6 +11,8 @@ namespace Microsoft.Azure.Cosmos.Linq using Microsoft.Azure.Cosmos.Spatial; using Microsoft.Azure.Cosmos.SqlObjects; using Microsoft.Azure.Documents; + using static Microsoft.Azure.Cosmos.Linq.ArrayBuiltinFunctions; + using static Microsoft.Azure.Cosmos.Linq.StringBuiltinFunctions; internal abstract class BuiltinFunctionVisitor { @@ -93,6 +95,34 @@ public static SqlScalarExpression VisitBuiltinFunctionCall(MethodCallExpression return ExpressionToSql.VisitNonSubqueryScalarExpression(methodCallExpression.Object, context); } + // Handle MemoryExtension implicit cast to Span/ReadOnlySpan -- introduced in C#14 + // Try to unwrap and translate the Span.Contains expression into IN + if (methodCallExpression.Method.DeclaringType == typeof(MemoryExtensions)) + { + bool canUnwrap = Utilities.TryUnwrapSpanImplicitCast(methodCallExpression.Arguments[0], out Expression unwrappedExpression); + if (canUnwrap) + { + // Make a new MethodCallExpression with the unwrapped expression + Expression searchList = null; + Expression searchExpression = null; + + // If non static Contains + if (methodCallExpression.Arguments.Count == 2) + { + searchList = unwrappedExpression; + searchExpression = methodCallExpression.Arguments[1]; + } + + if (searchList == null || searchExpression == null) + { + return null; + } + + ArrayContainsVisitor visitor = new ArrayContainsVisitor(); + return visitor.VisitIN(searchExpression, (ConstantExpression)searchList, context); + } + } + // String functions or ToString with Objects that are not strings and guids if ((declaringType == typeof(string)) || (methodCallExpression.Method.Name == "ToString" && diff --git a/Microsoft.Azure.Cosmos/src/Linq/ConstantEvaluator.cs b/Microsoft.Azure.Cosmos/src/Linq/ConstantEvaluator.cs index 50b22bb15d..b8fe1470dd 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/ConstantEvaluator.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/ConstantEvaluator.cs @@ -49,7 +49,9 @@ private static bool CanBeEvaluated(Expression expression) if (methodCallExpression != null) { Type type = methodCallExpression.Method.DeclaringType; - if (type == typeof(Enumerable) || type == typeof(Queryable) || type == typeof(CosmosLinq)) + + // Aside from the known types, we also need to avoid partial eval for op_implicit methods, which are the implicit conversions of enum to memoryextension span types (introduced in c#13) + if (type == typeof(Enumerable) || type == typeof(Queryable) || type == typeof(CosmosLinq) || methodCallExpression.Method.Name == "op_Implicit") { return false; } diff --git a/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs b/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs index 85840d493b..28348513e3 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/ExpressionToSQL.cs @@ -1155,8 +1155,7 @@ private static Collection VisitMethodCall(MethodCallExpression inputExpression, context.PushMethod(inputExpression); Type declaringType = inputExpression.Method.DeclaringType; - - if ((declaringType != typeof(Queryable) + if ((declaringType != typeof(Queryable) && declaringType != typeof(Enumerable) /*LINQ Methods*/ && declaringType != typeof(CosmosLinqExtensions) /*OrderByRank*/) || !inputExpression.Method.IsStatic /*Other extansion method*/) diff --git a/Microsoft.Azure.Cosmos/src/Linq/Utilities.cs b/Microsoft.Azure.Cosmos/src/Linq/Utilities.cs index ca39671f74..1fbbf7e76f 100644 --- a/Microsoft.Azure.Cosmos/src/Linq/Utilities.cs +++ b/Microsoft.Azure.Cosmos/src/Linq/Utilities.cs @@ -62,6 +62,22 @@ public static ParameterExpression NewParameter(string prefix, Type type, HashSet suffix++; } } + + public static bool TryUnwrapSpanImplicitCast(Expression expression, out Expression result) + { + if (expression is MethodCallExpression methodCallExpression + && methodCallExpression.Method.Name == "op_Implicit" + && methodCallExpression.Method.DeclaringType is { IsGenericType: true } implicitCastDeclaringType + && implicitCastDeclaringType.GetGenericTypeDefinition() is var genericTypeDefinition + && (genericTypeDefinition == typeof(Span<>) || genericTypeDefinition == typeof(ReadOnlySpan<>))) + { + result = methodCallExpression.Arguments[0]; + return true; + } + + result = null; + return false; + } } internal abstract class ExpressionSimplifier diff --git a/Microsoft.Azure.Cosmos/tests/CosmosDbDemo/CosmosDbDemo.csproj b/Microsoft.Azure.Cosmos/tests/CosmosDbDemo/CosmosDbDemo.csproj new file mode 100644 index 0000000000..7ece2522d3 --- /dev/null +++ b/Microsoft.Azure.Cosmos/tests/CosmosDbDemo/CosmosDbDemo.csproj @@ -0,0 +1,24 @@ + + + + + true + net9.0 + enable + enable + preview + AnyCPU + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos/tests/CosmosDbDemo/CosmosDbDemo.sln b/Microsoft.Azure.Cosmos/tests/CosmosDbDemo/CosmosDbDemo.sln new file mode 100644 index 0000000000..3b6d23f9e1 --- /dev/null +++ b/Microsoft.Azure.Cosmos/tests/CosmosDbDemo/CosmosDbDemo.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Azure.Cosmos", "..\..\src\Microsoft.Azure.Cosmos.csproj", "{1E7BA935-5548-458C-84E7-2F4CEFC620EC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CosmosDbDemo", "CosmosDbDemo.csproj", "{93FEBE02-3010-4367-8163-BC2FBE31E48C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1E7BA935-5548-458C-84E7-2F4CEFC620EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1E7BA935-5548-458C-84E7-2F4CEFC620EC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1E7BA935-5548-458C-84E7-2F4CEFC620EC}.Debug|x64.ActiveCfg = Debug|Any CPU + {1E7BA935-5548-458C-84E7-2F4CEFC620EC}.Debug|x64.Build.0 = Debug|Any CPU + {1E7BA935-5548-458C-84E7-2F4CEFC620EC}.Debug|x86.ActiveCfg = Debug|Any CPU + {1E7BA935-5548-458C-84E7-2F4CEFC620EC}.Debug|x86.Build.0 = Debug|Any CPU + {1E7BA935-5548-458C-84E7-2F4CEFC620EC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1E7BA935-5548-458C-84E7-2F4CEFC620EC}.Release|Any CPU.Build.0 = Release|Any CPU + {1E7BA935-5548-458C-84E7-2F4CEFC620EC}.Release|x64.ActiveCfg = Release|Any CPU + {1E7BA935-5548-458C-84E7-2F4CEFC620EC}.Release|x64.Build.0 = Release|Any CPU + {1E7BA935-5548-458C-84E7-2F4CEFC620EC}.Release|x86.ActiveCfg = Release|Any CPU + {1E7BA935-5548-458C-84E7-2F4CEFC620EC}.Release|x86.Build.0 = Release|Any CPU + {93FEBE02-3010-4367-8163-BC2FBE31E48C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {93FEBE02-3010-4367-8163-BC2FBE31E48C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {93FEBE02-3010-4367-8163-BC2FBE31E48C}.Debug|x64.ActiveCfg = Debug|Any CPU + {93FEBE02-3010-4367-8163-BC2FBE31E48C}.Debug|x64.Build.0 = Debug|Any CPU + {93FEBE02-3010-4367-8163-BC2FBE31E48C}.Debug|x86.ActiveCfg = Debug|Any CPU + {93FEBE02-3010-4367-8163-BC2FBE31E48C}.Debug|x86.Build.0 = Debug|Any CPU + {93FEBE02-3010-4367-8163-BC2FBE31E48C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {93FEBE02-3010-4367-8163-BC2FBE31E48C}.Release|Any CPU.Build.0 = Release|Any CPU + {93FEBE02-3010-4367-8163-BC2FBE31E48C}.Release|x64.ActiveCfg = Release|Any CPU + {93FEBE02-3010-4367-8163-BC2FBE31E48C}.Release|x64.Build.0 = Release|Any CPU + {93FEBE02-3010-4367-8163-BC2FBE31E48C}.Release|x86.ActiveCfg = Release|Any CPU + {93FEBE02-3010-4367-8163-BC2FBE31E48C}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/Microsoft.Azure.Cosmos/tests/CosmosDbDemo/Program.cs b/Microsoft.Azure.Cosmos/tests/CosmosDbDemo/Program.cs new file mode 100644 index 0000000000..3deccace3c --- /dev/null +++ b/Microsoft.Azure.Cosmos/tests/CosmosDbDemo/Program.cs @@ -0,0 +1,85 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Azure.Cosmos; +using Microsoft.Azure.Cosmos.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class Program +{ + // Cosmos DB Emulator values + private static readonly string EndpointUrl = "https://localhost:8081"; + private static readonly string PrimaryKey = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="; + private static readonly string DatabaseId = "ToDoList"; + private static readonly string ContainerId = "Items"; + + [TestMethod] + public async Task Main() + { + try + { + Console.WriteLine("Beginning operations..."); + + using CosmosClient client = new( + accountEndpoint: EndpointUrl, + authKeyOrResourceToken: PrimaryKey, + new CosmosClientOptions { ConnectionMode = ConnectionMode.Gateway } + ); + + // Create database if it doesn't exist + Database database = await client.CreateDatabaseIfNotExistsAsync(DatabaseId); + Console.WriteLine($"Created database: {database.Id}"); + + // Create container if it doesn't exist + Container container = await database.CreateContainerIfNotExistsAsync(ContainerId, "/id"); + Console.WriteLine($"Created container: {container.Id}"); + + // Create a sample item + TodoItem todoItem = new TodoItem + { + id = Guid.NewGuid().ToString(), + Title = "Learn Cosmos DB", + IsComplete = false + }; + + // Add the item + await container.CreateItemAsync(todoItem); + Console.WriteLine($"Created item: {todoItem.id}"); + + string[] someStringArray = ["Learn Cosmos DB"]; + IOrderedQueryable queryable = container.GetItemLinqQueryable(); + IQueryable query = queryable.Where(item => someStringArray.Contains(item.Title)); + + string querytest = query.ToQueryDefinition().QueryText; + Console.WriteLine($"Generated SQL Query: {querytest}"); + + using FeedIterator feed = query.ToFeedIterator(); + + while (feed.HasMoreResults) + { + foreach(TodoItem item in await feed.ReadNextAsync()) + { + Console.WriteLine($"Item: {item.id}"); + } + } + } + catch (CosmosException cosmosEx) + { + Console.WriteLine($"Cosmos DB Error: {cosmosEx.Message}"); + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex}"); + } + } + + private class TodoItem + { +#pragma warning disable IDE1006 // Naming Styles + public string id { get; set; } +#pragma warning restore IDE1006 // Naming Styles + public string Title { get; set; } + public bool IsComplete { get; set; } + public string[] ArrayField { get; set; } + } +} \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqTranslationBaselineTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqTranslationBaselineTests.cs index 0cfd83e2af..8f424a9a88 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqTranslationBaselineTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Linq/LinqTranslationBaselineTests.cs @@ -1286,7 +1286,10 @@ public void TestStringFunctions() Func> getQuery = this.CreateDataTestStringFunctions(); List inputs = new List - { + { + //// Memory Span Conversion + //new LinqTestInput("Contains in constant list", b => getQuery(b).Select(doc => constantList.Contains(doc.StringField))), + //new LinqTestInput("Contains in constant array", b => getQuery(b).Select(doc => constantArray.Contains(doc.StringField))), // Concat new LinqTestInput("Concat 2", b => getQuery(b).Select(doc => string.Concat(doc.StringField, "str"))), new LinqTestInput("Concat 3", b => getQuery(b).Select(doc => string.Concat(doc.StringField, "str1", "str2"))), diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Microsoft.Azure.Cosmos.EmulatorTests.csproj b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Microsoft.Azure.Cosmos.EmulatorTests.csproj index efc6c67076..78a5eabeb6 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Microsoft.Azure.Cosmos.EmulatorTests.csproj +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Microsoft.Azure.Cosmos.EmulatorTests.csproj @@ -3,7 +3,7 @@ true true AnyCPU - net6.0 + net9.0 false false Microsoft.Azure.Cosmos @@ -11,7 +11,7 @@ true master True - $(LangVersion) + preview