Skip to content

Commit

Permalink
Adding an extension mapping for string_to_array
Browse files Browse the repository at this point in the history
  • Loading branch information
austindrenski committed May 29, 2018
1 parent 2870f7b commit e2b7b1b
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 53 deletions.
72 changes: 50 additions & 22 deletions src/EFCore.PG/Extensions/NpgsqlArrayExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@

#endregion

using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities;

// ReSharper disable once CheckNamespace
namespace Microsoft.EntityFrameworkCore
Expand All @@ -36,57 +36,85 @@ namespace Microsoft.EntityFrameworkCore
public static class NpgsqlArrayExtensions
{
/// <summary>
/// Determines whether a range contains a specified value.
/// Concatenates elements using the supplied delimiter.
/// </summary>
/// <param name="_">The DbFunctions instance.</param>
/// <param name="array">The list to conver to a string in which to locate the value.</param>
/// <param name="array">The list to convert to a string in which to locate the value.</param>
/// <param name="delimiter">The value used to delimit the elements.</param>
/// <typeparam name="T">The type of the elements of <paramref name="array"/>.</typeparam>
/// <returns>
/// <value>true</value> if the range contains the specified value; otherwise, <value>false</value>.
/// The string concatenation of the elements with the supplied delimiter.
/// </returns>
public static string ArrayToString<T>([CanBeNull] this DbFunctions _, [NotNull] T[] array, [CanBeNull] string delimiter)
=> throw new NotSupportedException();
=> throw new ClientEvaluationNotSupportedException();

/// <summary>
/// Determines whether a range contains a specified value.
/// Concatenates elements using the supplied delimiter.
/// </summary>
/// <param name="_">The DbFunctions instance.</param>
/// <param name="array">The list to conver to a string in which to locate the value.</param>
/// <param name="list">The list to convert to a string in which to locate the value.</param>
/// <param name="delimiter">The value used to delimit the elements.</param>
/// <param name="nullString">The value used to represent a null value.</param>
/// <typeparam name="T">The type of the elements of <paramref name="array"/>.</typeparam>
/// <typeparam name="T">The type of the elements of <paramref name="list"/>.</typeparam>
/// <returns>
/// <value>true</value> if the range contains the specified value; otherwise, <value>false</value>.
/// The string concatenation of the elements with the supplied delimiter.
/// </returns>
public static string ArrayToString<T>([CanBeNull] this DbFunctions _, [NotNull] T[] array, [CanBeNull] string delimiter, [CanBeNull] string nullString)
=> throw new NotSupportedException();
public static string ArrayToString<T>([CanBeNull] this DbFunctions _, [NotNull] List<T> list, [CanBeNull] string delimiter)
=> throw new ClientEvaluationNotSupportedException();

/// <summary>
/// Determines whether a range contains a specified value.
/// Concatenates elements using the supplied delimiter and the string representation for null elements.
/// </summary>
/// <param name="_">The DbFunctions instance.</param>
/// <param name="list">The list to conver to a string in which to locate the value.</param>
/// <param name="array">The list to convert to a string in which to locate the value.</param>
/// <param name="delimiter">The value used to delimit the elements.</param>
/// <typeparam name="T">The type of the elements of <paramref name="list"/>.</typeparam>
/// <param name="nullString">The value used to represent a null value.</param>
/// <typeparam name="T">The type of the elements of <paramref name="array"/>.</typeparam>
/// <returns>
/// <value>true</value> if the range contains the specified value; otherwise, <value>false</value>.
/// The string concatenation of the elements with the supplied delimiter and null string.
/// </returns>
public static string ArrayToString<T>([CanBeNull] this DbFunctions _, [NotNull] List<T> list, [CanBeNull] string delimiter)
=> throw new NotSupportedException();
public static string ArrayToString<T>([CanBeNull] this DbFunctions _, [NotNull] T[] array, [CanBeNull] string delimiter, [CanBeNull] string nullString)
=> throw new ClientEvaluationNotSupportedException();

/// <summary>
/// Determines whether a range contains a specified value.
/// Concatenates elements using the supplied delimiter and the string representation for null elements.
/// </summary>
/// <param name="_">The DbFunctions instance.</param>
/// <param name="list">The list to conver to a string in which to locate the value.</param>
/// <param name="list">The list to convert to a string in which to locate the value.</param>
/// <param name="delimiter">The value used to delimit the elements.</param>
/// <param name="nullString">The value used to represent a null value.</param>
/// <typeparam name="T">The type of the elements of <paramref name="list"/>.</typeparam>
/// <returns>
/// <value>true</value> if the range contains the specified value; otherwise, <value>false</value>.
/// The string concatenation of the elements with the supplied delimiter and null string.
/// </returns>
public static string ArrayToString<T>([CanBeNull] this DbFunctions _, [NotNull] List<T> list, [CanBeNull] string delimiter, [CanBeNull] string nullString)
=> throw new NotSupportedException();
=> throw new ClientEvaluationNotSupportedException();

/// <summary>
/// Converts the input string into an array using the supplied delimiter and the string representation for null elements.
/// </summary>
/// <param name="_">The DbFunctions instance.</param>
/// <param name="input">The input string of delimited values.</param>
/// <param name="delimiter">The value that delimits the elements.</param>
/// <param name="nullString">The value that represents a null value.</param>
/// <typeparam name="T">The type of the elements in the resulting array.</typeparam>
/// <returns>
/// The array resulting from splitting the input string based on the supplied delimiter and null string.
/// </returns>
public static T[] StringToArray<T>([CanBeNull] this DbFunctions _, [NotNull] string input, [CanBeNull] string delimiter, [CanBeNull] string nullString)
=> throw new ClientEvaluationNotSupportedException();

/// <summary>
/// Converts the input string into a <see cref="List{T}"/> using the supplied delimiter and the string representation for null elements.
/// </summary>
/// <param name="_">The DbFunctions instance.</param>
/// <param name="input">The input string of delimited values.</param>
/// <param name="delimiter">The value that delimits the elements.</param>
/// <param name="nullString">The value that represents a null value.</param>
/// <typeparam name="T">The type of the elements in the resulting array.</typeparam>
/// <returns>
/// The list resulting from splitting the input string based on the supplied delimiter and null string.
/// </returns>
public static List<T> StringToList<T>([CanBeNull] this DbFunctions _, [NotNull] string input, [CanBeNull] string delimiter, [CanBeNull] string nullString)
=> throw new ClientEvaluationNotSupportedException();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,22 +53,12 @@ public Expression Translate(MethodCallExpression expression)
if (!IsTypeSupported(expression))
return null;

if (!IsMethodSupported(expression.Method))
return null;

// TODO: use #430 to map @> to source.All(x => other.Contains(x));
// TODO: use #430 to map && to soucre.Any(x => other.Contains(x));

switch (expression.Method.Name)
{
#region Instance

case "get_Item" when expression.Object is Expression instance:
return Expression.MakeIndex(instance, instance.Type.GetRuntimeProperty("Item"), expression.Arguments);

#endregion

#region Enumerable
#region EnumerableStaticMethods

case nameof(Enumerable.ElementAt):
return Expression.MakeIndex(expression.Arguments[0], expression.Arguments[0].Type.GetRuntimeProperty("Item"), new[] { expression.Arguments[1] });
Expand All @@ -94,9 +84,15 @@ public Expression Translate(MethodCallExpression expression)
case nameof(NpgsqlArrayExtensions.ArrayToString):
return new SqlFunctionExpression("array_to_string", typeof(string), expression.Arguments.Skip(1));

case nameof(NpgsqlArrayExtensions.StringToArray):
return new SqlFunctionExpression("string_to_array", expression.Method.ReturnType, expression.Arguments.Skip(1));

case nameof(NpgsqlArrayExtensions.StringToList):
return new SqlFunctionExpression("string_to_array", expression.Method.ReturnType, expression.Arguments.Skip(1));

#endregion

#region ArrayStatic
#region ArrayStaticMethods

case nameof(Array.IndexOf) when expression.Method.DeclaringType == typeof(Array):
return
Expand All @@ -111,7 +107,10 @@ public Expression Translate(MethodCallExpression expression)

#endregion

#region ListInstance
#region ListInstanceMethods

case "get_Item" when expression.Object is Expression instance:
return Expression.MakeIndex(instance, instance.Type.GetRuntimeProperty("Item"), expression.Arguments);

case nameof(IList.IndexOf) when IsArrayOrList(expression.Method.DeclaringType):
return
Expand Down Expand Up @@ -144,21 +143,34 @@ public Expression Translate(MethodCallExpression expression)
/// </returns>
static bool IsTypeSupported([NotNull] MethodCallExpression expression)
{
Type declaringType = expression.Method.DeclaringType;

// Methods declared here are always translated.
if (declaringType == typeof(NpgsqlArrayExtensions))
return true;

// Methods not declared here are never translated.
if (!IsArrayOrList(declaringType) &&
declaringType != typeof(Array) &&
declaringType != typeof(Enumerable))
return false;

// Instance methods are only translated for T[] and List<T>.
if (expression.Object is Expression instance)
return IsArrayOrList(instance.Type);

// Extension methods may only be translated when a parameter is T[] or List<T>
if (expression.Object is null)
{
// Static method with no parameters? Skip.
if (expression.Arguments.Count == 0)
return false;

if (expression.Arguments.Count > 1 &&
expression.Arguments[0].Type == typeof(DbFunctions))
return IsArrayOrList(expression.Arguments[1].Type);

// Is this an extension method on T[] or List<T>?
return IsArrayOrList(expression.Arguments[0].Type);
}

// Something else? Skip.
return false;
}

Expand All @@ -171,20 +183,7 @@ static bool IsTypeSupported([NotNull] MethodCallExpression expression)
/// <returns>
/// True if <paramref name="type"/> is an array or a <see cref="List{T}"/>; otherwise, false.
/// </returns>
static bool IsArrayOrList(Type type)
=> type.IsArray || type.IsGenericType && typeof(List<>) == type.GetGenericTypeDefinition();

/// <summary>
/// Tests if the method is declared on an array, a <see cref="List{T}"/>, or <see cref="Enumerable"/>.
/// </summary>
/// <param name="method">
/// The method to test.
/// </param>
/// <returns>
/// True if <paramref name="method"/> is declared on an array, a <see cref="List{T}"/>, or <see cref="Enumerable"/>; otherwise, false.
/// </returns>
static bool IsMethodSupported([NotNull] MethodInfo method)
=> method.DeclaringType is Type t && (IsArrayOrList(t) || t == typeof(Array) || t == typeof(Enumerable) || t == typeof(NpgsqlArrayExtensions));
static bool IsArrayOrList(Type type) => type.IsArray || type.IsGenericType && typeof(List<>) == type.GetGenericTypeDefinition();

#endregion
}
Expand Down
48 changes: 48 additions & 0 deletions src/EFCore.PG/Utilities/ClientEvaluationNotSupported.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#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
{
/// <summary>
/// The exception that is thrown when a method intended for SQL translation is evaluated by the client.
/// </summary>
public class ClientEvaluationNotSupportedException : NotSupportedException
{
readonly string _callerMemberName;

/// <inheritdoc />
public override string Message
=> $"{_callerMemberName} is only intended for use via SQL translation as part of an EF Core LINQ query.";

/// <inheritdoc />
public ClientEvaluationNotSupportedException([CallerMemberName] string method = default)
{
_callerMemberName = method;
}
}
}
28 changes: 28 additions & 0 deletions test/EFCore.PG.FunctionalTests/Query/ArrayQueryTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,34 @@ public void List_ArrayToString_with_null()
}
}

[Fact]
public void Array_StringToArray()
{
using (var ctx = CreateContext())
{
var _ =
ctx.SomeEntities
.Select(e => EF.Functions.ArrayToString(e.SomeArray, ",", "*"))
.Select(e => EF.Functions.StringToArray<string>(e, ",", "*")).ToList();

AssertContainsInSql(@"SELECT string_to_array(array_to_string(e.""SomeArray"", ',', '*'), ',', '*')");
}
}

[Fact]
public void List_StringToList()
{
using (var ctx = CreateContext())
{
var _ =
ctx.SomeEntities
.Select(e => EF.Functions.ArrayToString(e.SomeList, ",", "*"))
.Select(e => EF.Functions.StringToList<string>(e, ",", "*")).ToList();

AssertContainsInSql(@"SELECT string_to_array(array_to_string(e.""SomeList"", ',', '*'), ',', '*')");
}
}

#if NETCOREAPP2_1
[Fact]
public void Array_Append_constant()
Expand Down

0 comments on commit e2b7b1b

Please sign in to comment.