diff --git a/QueryBaseline.cs b/QueryBaseline.cs index 6d9d51beb..c0803e86f 100644 --- a/QueryBaseline.cs +++ b/QueryBaseline.cs @@ -4014,53 +4014,3 @@ -System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor, Boolean wrapExceptions) : - AssertSql( - @"SELECT (o.""OrderDate"" + MAKE_INTERVAL(secs => 1)) AS ""OrderDate"" -FROM ""Orders"" AS o -WHERE ((o.""OrderDate"" <> NULL) OR (o.""OrderDate"" IS NULL OR NULL IS NULL)) AND (o.""OrderDate"" IS NOT NULL OR NULL IS NOT NULL)"); - - - -System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor, Boolean wrapExceptions) : - AssertSql( - @"SELECT (o.""OrderDate"" + MAKE_INTERVAL(years => 1)) AS ""OrderDate"" -FROM ""Orders"" AS o -WHERE ((o.""OrderDate"" <> NULL) OR (o.""OrderDate"" IS NULL OR NULL IS NULL)) AND (o.""OrderDate"" IS NOT NULL OR NULL IS NOT NULL)"); - - - -System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor, Boolean wrapExceptions) : - AssertSql( - @"SELECT (o.""OrderDate"" + MAKE_INTERVAL(mins => 1)) AS ""OrderDate"" -FROM ""Orders"" AS o -WHERE ((o.""OrderDate"" <> NULL) OR (o.""OrderDate"" IS NULL OR NULL IS NULL)) AND (o.""OrderDate"" IS NOT NULL OR NULL IS NOT NULL)"); - - - -System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor, Boolean wrapExceptions) : - AssertSql( - @"@__years_0='2' - -SELECT (o.""OrderDate"" + MAKE_INTERVAL(years => @__years_0)) AS ""OrderDate"" -FROM ""Orders"" AS o -WHERE ((o.""OrderDate"" <> NULL) OR (o.""OrderDate"" IS NULL OR NULL IS NULL)) AND (o.""OrderDate"" IS NOT NULL OR NULL IS NOT NULL)"); - - - -System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor, Boolean wrapExceptions) : - AssertSql( - @"SELECT (o.""OrderDate"" + MAKE_INTERVAL(months => 1)) AS ""OrderDate"" -FROM ""Orders"" AS o -WHERE ((o.""OrderDate"" <> NULL) OR (o.""OrderDate"" IS NULL OR NULL IS NULL)) AND (o.""OrderDate"" IS NOT NULL OR NULL IS NOT NULL)"); - - - -System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor, Boolean wrapExceptions) : - AssertSql( - @"SELECT (o.""OrderDate"" + MAKE_INTERVAL(hours => 1)) AS ""OrderDate"" -FROM ""Orders"" AS o -WHERE ((o.""OrderDate"" <> NULL) OR (o.""OrderDate"" IS NULL OR NULL IS NULL)) AND (o.""OrderDate"" IS NOT NULL OR NULL IS NOT NULL)"); - - - diff --git a/src/EFCore.PG/Extensions/NpgsqlArrayExtensions.cs b/src/EFCore.PG/Extensions/NpgsqlArrayExtensions.cs index afbe423c6..674152580 100644 --- a/src/EFCore.PG/Extensions/NpgsqlArrayExtensions.cs +++ b/src/EFCore.PG/Extensions/NpgsqlArrayExtensions.cs @@ -23,9 +23,10 @@ #endregion +using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using JetBrains.Annotations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; // ReSharper disable once CheckNamespace namespace Microsoft.EntityFrameworkCore @@ -49,7 +50,7 @@ public static class NpgsqlArrayExtensions /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. /// public static string ArrayToString([CanBeNull] this DbFunctions _, [NotNull] T[] array, [CanBeNull] string delimiter) - => throw new ClientEvaluationNotSupportedException(); + => throw ClientEvaluationNotSupportedException(); /// /// Concatenates elements using the supplied delimiter. @@ -65,7 +66,7 @@ public static string ArrayToString([CanBeNull] this DbFunctions _, [NotNull] /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. /// public static string ArrayToString([CanBeNull] this DbFunctions _, [NotNull] List list, [CanBeNull] string delimiter) - => throw new ClientEvaluationNotSupportedException(); + => throw ClientEvaluationNotSupportedException(); /// /// Concatenates elements using the supplied delimiter and the string representation for null elements. @@ -82,7 +83,7 @@ public static string ArrayToString([CanBeNull] this DbFunctions _, [NotNull] /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. /// public static string ArrayToString([CanBeNull] this DbFunctions _, [NotNull] T[] array, [CanBeNull] string delimiter, [CanBeNull] string nullString) - => throw new ClientEvaluationNotSupportedException(); + => throw ClientEvaluationNotSupportedException(); /// /// Concatenates elements using the supplied delimiter and the string representation for null elements. @@ -99,7 +100,7 @@ public static string ArrayToString([CanBeNull] this DbFunctions _, [NotNull] /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. /// public static string ArrayToString([CanBeNull] this DbFunctions _, [NotNull] List list, [CanBeNull] string delimiter, [CanBeNull] string nullString) - => throw new ClientEvaluationNotSupportedException(); + => throw ClientEvaluationNotSupportedException(); /// /// Converts the input string into an array using the supplied delimiter and the string representation for null elements. @@ -116,7 +117,7 @@ public static string ArrayToString([CanBeNull] this DbFunctions _, [NotNull] /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. /// public static T[] StringToArray([CanBeNull] this DbFunctions _, [NotNull] string input, [CanBeNull] string delimiter, [CanBeNull] string nullString) - => throw new ClientEvaluationNotSupportedException(); + => throw ClientEvaluationNotSupportedException(); /// /// Converts the input string into a using the supplied delimiter and the string representation for null elements. @@ -133,6 +134,21 @@ public static T[] StringToArray([CanBeNull] this DbFunctions _, [NotNull] str /// This method is only intended for use via SQL translation as part of an EF Core LINQ query. /// public static List StringToList([CanBeNull] this DbFunctions _, [NotNull] string input, [CanBeNull] string delimiter, [CanBeNull] string nullString) - => throw new ClientEvaluationNotSupportedException(); + => throw ClientEvaluationNotSupportedException(); + + #region Utilities + + /// + /// Helper method to throw a with the name of the throwing method. + /// + /// The method that throws the exception. + /// + /// A . + /// + [NotNull] + static NotSupportedException ClientEvaluationNotSupportedException([CallerMemberName] string method = default) + => new NotSupportedException($"{method} is only intended for use via SQL translation as part of an EF Core LINQ query."); + + #endregion } } diff --git a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs index b0edb5b79..23b1f215b 100644 --- a/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.PG/Query/ExpressionVisitors/NpgsqlSqlTranslatingExpressionVisitor.cs @@ -34,6 +34,7 @@ using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query.Expressions; using Microsoft.EntityFrameworkCore.Query.ExpressionVisitors; +using Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; using Remotion.Linq.Clauses; using Remotion.Linq.Clauses.Expressions; @@ -80,6 +81,17 @@ public class NpgsqlSqlTranslatingExpressionVisitor : SqlTranslatingExpressionVis #endregion + /// + /// The current query model visitor. + /// + [NotNull] readonly RelationalQueryModelVisitor _queryModelVisitor; + + /// + /// The current query compilation context. + /// + [NotNull] + RelationalQueryCompilationContext Context => _queryModelVisitor.QueryCompilationContext; + /// public NpgsqlSqlTranslatingExpressionVisitor( [NotNull] SqlTranslatingExpressionVisitorDependencies dependencies, @@ -87,17 +99,18 @@ public NpgsqlSqlTranslatingExpressionVisitor( [CanBeNull] SelectExpression targetSelectExpression = null, [CanBeNull] Expression topLevelPredicate = null, bool inProjection = false) - : base(dependencies, queryModelVisitor, targetSelectExpression, topLevelPredicate, inProjection) {} + : base(dependencies, queryModelVisitor, targetSelectExpression, topLevelPredicate, inProjection) + => _queryModelVisitor = queryModelVisitor; #region Overrides /// protected override Expression VisitBinary(BinaryExpression expression) - => expression.NodeType is ExpressionType.ArrayIndex - ? Expression.MakeIndex( + => expression.NodeType is ExpressionType.ArrayIndex && + IsSafeToVisit(expression, Context) + ? Expression.ArrayAccess( Visit(expression.Left) ?? expression.Left, - indexer: null, - new[] { Visit(expression.Right) ?? expression.Right, }) + Visit(expression.Right) ?? expression.Right) : base.VisitBinary(expression); /// @@ -312,6 +325,17 @@ protected virtual Expression VisitArrayContains([NotNull] Expression array, [Not /// static bool IsArrayOrList([NotNull] Type type) => type.IsArray || type.IsGenericType && typeof(List<>) == type.GetGenericTypeDefinition(); + /// + /// True if the expression is safe to visitat this stage. + /// + /// The expression to check + /// The context to use. + /// + /// True to visit this expression; otherwise false. + /// + static bool IsSafeToVisit(BinaryExpression expression, RelationalQueryCompilationContext context) + => MemberAccessBindingExpressionVisitor.GetPropertyPath(expression.Left, context, out _).Count != 0; + #endregion } } diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgFunctionExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgFunctionExpression.cs index 9b2c179c6..ccb1fc548 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/PgFunctionExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/PgFunctionExpression.cs @@ -29,6 +29,7 @@ using System.Diagnostics; using System.Linq; using System.Linq.Expressions; +using System.Text; using JetBrains.Annotations; using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Sql.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; diff --git a/src/EFCore.PG/Utilities/ClientEvaluationNotSupported.cs b/src/EFCore.PG/Utilities/ClientEvaluationNotSupported.cs deleted file mode 100644 index 8fce1fdbb..000000000 --- a/src/EFCore.PG/Utilities/ClientEvaluationNotSupported.cs +++ /dev/null @@ -1,48 +0,0 @@ -#region License - -// The PostgreSQL License -// -// Copyright (C) 2016 The Npgsql Development Team -// -// Permission to use, copy, modify, and distribute this software and its -// documentation for any purpose, without fee, and without a written -// agreement is hereby granted, provided that the above copyright notice -// and this paragraph and the following two paragraphs appear in all copies. -// -// IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY -// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, -// INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS -// DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF -// THE POSSIBILITY OF SUCH DAMAGE. -// -// THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS -// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS -// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. - -#endregion - -using System; -using System.Runtime.CompilerServices; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Utilities -{ - /// - /// The exception that is thrown when a method intended for SQL translation is evaluated by the client. - /// - public class ClientEvaluationNotSupportedException : NotSupportedException - { - readonly string _callerMemberName; - - /// - public override string Message - => $"{_callerMemberName} is only intended for use via SQL translation as part of an EF Core LINQ query."; - - /// - public ClientEvaluationNotSupportedException([CallerMemberName] string method = default) - { - _callerMemberName = method; - } - } -} diff --git a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs index a1e79318c..8c37eb4e5 100644 --- a/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs @@ -947,50 +947,22 @@ public void List_Exists_equals_with_column_array_element_flipped() #region Support - /// - /// Provides resources for unit tests. - /// ArrayFixture Fixture { get; } - /// - /// Initializes resources for unit tests. - /// - /// The fixture of resources for testing. public ArrayQueryTest(ArrayFixture fixture) { Fixture = fixture; Fixture.TestSqlLoggerFactory.Clear(); } - /// - /// Creates a new . - /// - /// - /// An for testing. - /// ArrayContext CreateContext() => Fixture.CreateContext(); - /// - /// Asserts that the SQL fragment appears in the logs. - /// - /// The SQL statement or fragment to search for in the logs. void AssertContainsInSql(string expected) => Assert.Contains(expected, Fixture.TestSqlLoggerFactory.Sql); - /// - /// Asserts that the SQL fragment does not appear in the logs. - /// - /// The SQL statement or fragment to search for in the logs. void AssertDoesNotContainInSql(string expected) => Assert.DoesNotContain(expected, Fixture.TestSqlLoggerFactory.Sql); - #endregion Support - - #region Fixtures - - /// - /// Represents a database suitable for testing operations with PostgreSQL arrays. - /// public class ArrayContext : DbContext { public DbSet SomeEntities { get; set; } @@ -998,22 +970,18 @@ public ArrayContext(DbContextOptions options) : base(options) {} protected override void OnModelCreating(ModelBuilder builder) {} } - /// - /// Represents an entity suitable for testing operations with PostgreSQL arrays. - /// public class SomeArrayEntity { public int Id { get; set; } public int[] SomeArray { get; set; } - public List SomeList { get; set; } public int[,] SomeMatrix { get; set; } + public List SomeList { get; set; } public byte[] SomeBytea { get; set; } + + // ReSharper disable once UnusedMember.Global public string SomeString { get; set; } } - /// - /// Represents a fixture suitable for testing operations with PostgreSQL arrays. - /// public class ArrayFixture : IDisposable { readonly DbContextOptions _options; @@ -1039,18 +1007,16 @@ public ArrayFixture() Id = 1, SomeArray = new[] { 3, 4 }, SomeBytea = new byte[] { 3, 4 }, - SomeList = new List { 3, 4 }, SomeMatrix = new[,] { { 5, 6 }, { 7, 8 } }, - SomeString = "This_is_a_test" + SomeList = new List { 3, 4 } }); ctx.SomeEntities.Add(new SomeArrayEntity { Id = 2, SomeArray = new[] { 5, 6, 7 }, SomeBytea = new byte[] { 5, 6, 7 }, - SomeList = new List { 5, 6, 7 }, SomeMatrix = new[,] { { 10, 11 }, { 12, 13 } }, - SomeString = "this_is_a_test" + SomeList = new List { 3, 4 } }); ctx.SaveChanges(); } diff --git a/test/EFCore.PG.FunctionalTests/Query/CompiledQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/CompiledQueryNpgsqlTest.cs index b00d841a1..0e49db804 100644 --- a/test/EFCore.PG.FunctionalTests/Query/CompiledQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/CompiledQueryNpgsqlTest.cs @@ -1,55 +1,15 @@ -using System.Linq; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Query; -using Microsoft.EntityFrameworkCore.TestModels.Northwind; +using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.TestUtilities; -using Microsoft.EntityFrameworkCore.TestUtilities.Xunit; -using Xunit; using Xunit.Abstractions; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query { public class CompiledQueryNpgsqlTest : CompiledQueryTestBase> { - // ReSharper disable once UnusedParameter.Local public CompiledQueryNpgsqlTest(NorthwindQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) : base(fixture) { fixture.TestSqlLoggerFactory.Clear(); } - - [ConditionalFact(Skip = "Throws: Can't write CLR type System.String[] with handler type TextHandler")] - public override void Query_with_array_parameter() - { - var query = EF.CompileQuery( - (NorthwindContext context, string[] args) - => context.Customers.Where(c => c.CustomerID == args[0])); - - using (var context = CreateContext()) - { - var args = new[] { "ALFKI" }; - - // BUG: this passes - var _ = context.Customers.Where(c => c.CustomerID == args[0]).ToList(); - - // BUG: this throws - // System.InvalidCastException : Can't write CLR type System.String[] with handler type TextHandler - var result = query(context, args).First().CustomerID; - - Assert.Equal("ALFKI", result); - } - - using (var context = CreateContext()) - { - Assert.Equal("ANATR", query(context, new[] { "ANATR" }).First().CustomerID); - } - } - - [ConditionalFact(Skip = "Throws: Can't write CLR type System.String[] with handler type TextHandler")] - public override async Task Query_with_array_parameter_async() - { - await base.Query_with_array_parameter_async(); - } } }