Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added basic support for HStore operations query translation #240

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/EFCore.PG/NpgsqlDbFunctionsExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,5 +136,15 @@ var regexPattern
RegexOptions.Singleline,
_regexTimeout);
}

public static string[] HStoreKeys([CanBeNull] this DbFunctions _, [NotNull] IDictionary<string, string> hstore)
{
return hstore.Keys.ToArray();
}

public static string[] HStoreValues([CanBeNull] this DbFunctions _, [NotNull] IDictionary<string, string> hstore)
{
return hstore.Values.ToArray();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ public class NpgsqlCompositeMethodCallTranslator : RelationalCompositeMethodCall
new NpgsqlStringTrimTranslator(),
new NpgsqlStringTrimEndTranslator(),
new NpgsqlStringTrimStartTranslator(),
new NpgsqlRegexIsMatchTranslator()
new NpgsqlRegexIsMatchTranslator(),
new NpgsqlDictionaryIndexTranslator(),
new NpgsqlHStoreKeysTranslator()
};

public NpgsqlCompositeMethodCallTranslator(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Text;
using System.Linq;
using System.Reflection;

namespace Microsoft.EntityFrameworkCore.Query.ExpressionTranslators.Internal
{
public class NpgsqlDictionaryIndexTranslator : IMethodCallTranslator
{
static readonly PropertyInfo DictionaryPropertyIndexAccessor =
typeof(IDictionary<string, string>).GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Single(p => p.Name == "Item");

public Expression Translate(MethodCallExpression methodCallExpression)
{
if (methodCallExpression.NodeType == ExpressionType.Call

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MethodCallExpression is always ET.Call? Redundant check.

&& methodCallExpression.Method.Name == "get_Item"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be easy to compare with actual MethodInfo rather than string name.

&& typeof(IDictionary<string, string>).IsAssignableFrom(methodCallExpression.Method.DeclaringType))
{
return Expression.MakeIndex(methodCallExpression.Object, DictionaryPropertyIndexAccessor, methodCallExpression.Arguments);
}
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Microsoft.EntityFrameworkCore.Query.Expressions;
using Microsoft.EntityFrameworkCore.Query.Expressions.Internal;
using System;
using System.Linq;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;

namespace Microsoft.EntityFrameworkCore.Query.ExpressionTranslators.Internal
{
public class NpgsqlHStoreKeysTranslator : IMethodCallTranslator
{
public Expression Translate(MethodCallExpression methodCallExpression)
{
if(methodCallExpression.Method ==
typeof(NpgsqlDbFunctionsExtensions)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

store in static field to avoid doing reflection for every translation.
Same for the other If

.GetRuntimeMethod(
nameof(NpgsqlDbFunctionsExtensions.HStoreKeys), new Type[] { typeof(DbFunctions), typeof(IDictionary<string, string>) }))
{
return new SqlFunctionExpression("akeys", typeof(string[]), new Expression[] { methodCallExpression.Arguments[1] });
}

if (methodCallExpression.Method ==
typeof(NpgsqlDbFunctionsExtensions)
.GetRuntimeMethod(
nameof(NpgsqlDbFunctionsExtensions.HStoreValues), new Type[] { typeof(DbFunctions), typeof(IDictionary<string, string>) }))
{
return new SqlFunctionExpression("avals", typeof(string[]), new Expression[] { methodCallExpression.Arguments[1] });
}

if(typeof(IDictionary<string, string>).IsAssignableFrom(methodCallExpression.Method.DeclaringType)
&& methodCallExpression.Method.Name == nameof(IDictionary<string, string>.ContainsKey))
{
return new DictionaryContainsKeyExpression(methodCallExpression.Arguments.Single(), methodCallExpression.Object);
}

return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Text;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Utilities;
using System.Diagnostics;
using Microsoft.EntityFrameworkCore.Query.Sql.Internal;

namespace Microsoft.EntityFrameworkCore.Query.Expressions.Internal
{
public class DictionaryContainsKeyExpression : Expression
{
/// <summary>
/// Creates a new instance of expression that is used by EF translator to fetch
/// HStore keys from database.
/// </summary>
/// <param name="key"> The key. </param>
/// <param name="dictionary"> The dictionary. </param>
public DictionaryContainsKeyExpression(
[NotNull] Expression key,
[NotNull] Expression dictionary)
{
Check.NotNull(dictionary, nameof(dictionary));
Check.NotNull(key, nameof(key));
Debug.Assert(typeof(IDictionary<string, string>).IsAssignableFrom(dictionary.Type));

Dictionary = dictionary;
Key = key;
}

/// <summary>
/// Gets the dictionary.
/// </summary>
/// <value>
/// The dictionary.
/// </value>
public virtual Expression Dictionary { get; }

/// <summary>
/// Gets the key.
/// </summary>
/// <value>
/// The key.
/// </value>
public virtual Expression Key { get; }

/// <summary>
/// Returns the node type of this <see cref="Expression" />. (Inherited from <see cref="Expression" />.)
/// </summary>
/// <returns>The <see cref="ExpressionType" /> that represents this expression.</returns>
public override ExpressionType NodeType => ExpressionType.Extension;

/// <summary>
/// Gets the static type of the expression that this <see cref="Expression" /> represents. (Inherited from <see cref="Expression" />.)
/// </summary>
/// <returns>The <see cref="Type" /> that represents the static type of the expression.</returns>
public override Type Type => typeof(bool);

/// <summary>
/// Dispatches to the specific visit method for this node type.
/// </summary>
protected override Expression Accept(ExpressionVisitor visitor)
{
Check.NotNull(visitor, nameof(visitor));

return visitor is NpgsqlQuerySqlGenerator npsgqlGenerator
? npsgqlGenerator.VisitDictionaryContainsKey(this)
: base.Accept(visitor);
}

/// <summary>
/// Reduces the node and then calls the <see cref="ExpressionVisitor.Visit(System.Linq.Expressions.Expression)" /> method passing the
/// reduced expression.
/// Throws an exception if the node isn't reducible.
/// </summary>
/// <param name="visitor"> An instance of <see cref="ExpressionVisitor" />. </param>
/// <returns> The expression being visited, or an expression which should replace it in the tree. </returns>
/// <remarks>
/// Override this method to provide logic to walk the node's children.
/// A typical implementation will call visitor.Visit on each of its
/// children, and if any of them change, should return a new copy of
/// itself with the modified children.
/// </remarks>
///
protected override Expression VisitChildren(ExpressionVisitor visitor)
{
var newDictionary = visitor.Visit(Dictionary);
var newKey = visitor.Visit(Key);

return newKey != Key || newDictionary != Dictionary
? new DictionaryContainsKeyExpression(newKey, newDictionary)
: this;
}

/// <summary>
/// Tests if this object is considered equal to another.
/// </summary>
/// <param name="obj"> The object to compare with the current object. </param>
/// <returns>
/// true if the objects are considered equal, false if they are not.
/// </returns>
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj))
{
return false;
}

if (ReferenceEquals(this, obj))
{
return true;
}

return obj.GetType() == GetType() && Equals((DictionaryContainsKeyExpression)obj);
}

bool Equals(DictionaryContainsKeyExpression other)
=> Dictionary.Equals(other.Dictionary) && Key.Equals(other.Key);

/// <summary>
/// Returns a hash code for this object.
/// </summary>
/// <returns>
/// A hash code for this object.
/// </returns>
public override int GetHashCode()
{
unchecked
{
return (Key.GetHashCode() * 397) ^ Dictionary.GetHashCode();
}
}

/// <summary>
/// Creates a <see cref="string" /> representation of the Expression.
/// </summary>
/// <returns>A <see cref="string" /> representation of the Expression.</returns>
public override string ToString() => $"{Dictionary} ? {Key}";
}
}
25 changes: 25 additions & 0 deletions src/EFCore.PG/Query/Sql/Internal/NpgsqlQuerySqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
using Microsoft.EntityFrameworkCore.Query.Expressions.Internal;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Utilities;
using System.Collections.Generic;

namespace Microsoft.EntityFrameworkCore.Query.Sql.Internal
{
Expand Down Expand Up @@ -138,6 +139,21 @@ protected override Expression VisitUnary(UnaryExpression expression)
return base.VisitUnary(expression);
}

protected override Expression VisitIndex(IndexExpression expression)
{
if (expression.Indexer.Name == "Item"
&& typeof(IDictionary<string, string>).IsAssignableFrom(expression.Object.Type))
{
Debug.Assert(expression.Arguments.Count == 1);
var expr = Visit(expression.Object);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the other. Can be SqlFunctionExpression.

Sql.Append(" -> ");
Visit(expression.Arguments[0]);
return null;
}

return base.VisitIndex(expression);
}

void GenerateArrayIndex([NotNull] BinaryExpression expression)
{
Debug.Assert(expression.NodeType == ExpressionType.ArrayIndex);
Expand Down Expand Up @@ -176,6 +192,15 @@ public Expression VisitArrayAny(ArrayAnyExpression arrayAnyExpression)
return arrayAnyExpression;
}

public Expression VisitDictionaryContainsKey(DictionaryContainsKeyExpression expr)
{
Visit(expr.Dictionary);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be written using SqlFunctionExpression in 2.1 passing in dictionary as instance. That would avoid creating new expression type

Sql.Append(" ? ");
Visit(expr.Key);

return expr;
}

// PostgreSQL array indexing is 1-based. If the index happens to be a constant,
// just increment it. Otherwise, append a +1 in the SQL.
Expression GenerateOneBasedIndexExpression(Expression expression)
Expand Down
Loading