Skip to content

Commit

Permalink
Hstore query support
Browse files Browse the repository at this point in the history
  • Loading branch information
yinzara committed Oct 4, 2024
1 parent 30cebf0 commit 8bb6452
Show file tree
Hide file tree
Showing 9 changed files with 593 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
using System.Collections.Immutable;
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions;
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;
using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics;

namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public class NpgsqlHstoreTranslator : IMethodCallTranslator, IMemberTranslator
{
private static readonly Type DictionaryType = typeof(Dictionary<string, string>);
private static readonly Type ImmutableDictionaryType = typeof(ImmutableDictionary<string, string>);

private static readonly MethodInfo Dictionary_ContainsKey =
DictionaryType.GetMethod(nameof(Dictionary<string, string>.ContainsKey))!;

private static readonly MethodInfo ImmutableDictionary_ContainsKey =
ImmutableDictionaryType.GetMethod(nameof(ImmutableDictionary<string, string>.ContainsKey))!;

private static readonly MethodInfo Dictionary_ContainsValue =
DictionaryType.GetMethod(nameof(Dictionary<string, string>.ContainsValue))!;

private static readonly MethodInfo ImmutableDictionary_ContainsValue =
ImmutableDictionaryType.GetMethod(nameof(ImmutableDictionary<string, string>.ContainsValue))!;

private static readonly MethodInfo Dictionary_Item_Getter =
DictionaryType.FindIndexerProperty()!.GetMethod!;

private static readonly MethodInfo ImmutableDictionary_Item_Getter =
ImmutableDictionaryType.FindIndexerProperty()!.GetMethod!;

private static readonly MethodInfo Enumerable_Any =
typeof(Enumerable).GetMethod(
nameof(Enumerable.Any), BindingFlags.Public | BindingFlags.Static,
[typeof(IEnumerable<>).MakeGenericType(Type.MakeGenericMethodParameter(0))])!
.MakeGenericMethod(typeof(KeyValuePair<string, string>));

private static readonly MethodInfo Enumerable_ToList =
typeof(Enumerable).GetMethod(
nameof(Enumerable.ToList), BindingFlags.Public | BindingFlags.Static,
[typeof(IEnumerable<>).MakeGenericType(Type.MakeGenericMethodParameter(0))])!
.MakeGenericMethod(typeof(string));

private static readonly PropertyInfo Dictionary_Count = DictionaryType.GetProperty(nameof(Dictionary<string, string>.Count))!;

private static readonly PropertyInfo ImmutableDictionary_Count =
ImmutableDictionaryType.GetProperty(nameof(ImmutableDictionary<string, string>.Count))!;

private static readonly PropertyInfo ImmutableDictionary_IsEmpty =
ImmutableDictionaryType.GetProperty(nameof(ImmutableDictionary<string, string>.IsEmpty))!;

private static readonly PropertyInfo Dictionary_Keys = DictionaryType.GetProperty(nameof(Dictionary<string, string>.Keys))!;

private static readonly PropertyInfo ImmutableDictionary_Keys =
ImmutableDictionaryType.GetProperty(nameof(ImmutableDictionary<string, string>.Keys))!;

private static readonly PropertyInfo Dictionary_Values = DictionaryType.GetProperty(nameof(Dictionary<string, string>.Values))!;

private static readonly PropertyInfo ImmutableDictionary_Values =
ImmutableDictionaryType.GetProperty(nameof(ImmutableDictionary<string, string>.Values))!;

private readonly RelationalTypeMapping _stringListTypeMapping;
private readonly RelationalTypeMapping _stringTypeMapping;
private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory;
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public NpgsqlHstoreTranslator(IRelationalTypeMappingSource typeMappingSource, NpgsqlSqlExpressionFactory sqlExpressionFactory)
{
_sqlExpressionFactory = sqlExpressionFactory;
_stringListTypeMapping = typeMappingSource.FindMapping(typeof(List<string>))!;
_stringTypeMapping = typeMappingSource.FindMapping(typeof(string))!;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public SqlExpression? Translate(
SqlExpression? instance,
MethodInfo method,
IReadOnlyList<SqlExpression> arguments,
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
{
if (method == Enumerable_Any && arguments[0].TypeMapping?.StoreType == "hstore")
{
return _sqlExpressionFactory.NotEqual(
Translate(arguments[0], Dictionary_Count, typeof(int), logger)!,
_sqlExpressionFactory.Constant(0));
}

if (method == Enumerable_ToList && arguments[0] is SqlFunctionExpression { Arguments: [{ TypeMapping.StoreType: "hstore" }] })
{
return arguments[0];
}

if (instance?.TypeMapping?.StoreType != "hstore")
{
return null;
}

if (method == Dictionary_ContainsKey || method == ImmutableDictionary_ContainsKey)
{
return _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.HStoreContainsKey, instance, arguments[0]);
}

if (method == Dictionary_ContainsValue || method == ImmutableDictionary_ContainsValue)
{
return _sqlExpressionFactory.Any(
arguments[0],
Translate(instance, Dictionary_Values, typeof(List<string>), logger)!,
PgAnyOperatorType.Equal);
}

if (method == Dictionary_Item_Getter || method == ImmutableDictionary_Item_Getter)
{
return _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.HStoreValueForKey, instance, arguments[0], _stringTypeMapping);
}

return null;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public SqlExpression? Translate(
SqlExpression? instance,
MemberInfo member,
Type returnType,
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
{

if (instance?.TypeMapping?.StoreType != "hstore")
{
return null;
}

if (member == Dictionary_Count || member == ImmutableDictionary_Count)
{
return _sqlExpressionFactory.Function("array_length",
[
Translate(instance, Dictionary_Keys, typeof(List<string>), logger)!,
_sqlExpressionFactory.Constant(1)
], false, TrueArrays[2], typeof(int));
}

if (member == Dictionary_Keys || member == ImmutableDictionary_Keys)
{
return _sqlExpressionFactory.Function(
"akeys", [instance], true, TrueArrays[1], typeof(List<string>), _stringListTypeMapping);
}

if (member == Dictionary_Values || member == ImmutableDictionary_Values)
{
return _sqlExpressionFactory.Function(
"avals", [instance], true, TrueArrays[1], typeof(List<string>), _stringListTypeMapping);
}

if (member == ImmutableDictionary_IsEmpty)
{
return _sqlExpressionFactory.Equal(
Translate(instance, Dictionary_Count, typeof(int), logger)!,
_sqlExpressionFactory.Constant(0));
}

return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ public NpgsqlMemberTranslatorProvider(
JsonPocoTranslator,
new NpgsqlRangeTranslator(typeMappingSource, sqlExpressionFactory, model, supportsMultiranges),
new NpgsqlStringMemberTranslator(sqlExpressionFactory),
new NpgsqlTimeSpanMemberTranslator(sqlExpressionFactory)
new NpgsqlTimeSpanMemberTranslator(sqlExpressionFactory),
new NpgsqlHstoreTranslator(typeMappingSource, sqlExpressionFactory)
]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ public NpgsqlMethodCallTranslatorProvider(
new NpgsqlRegexIsMatchTranslator(sqlExpressionFactory),
new NpgsqlRowValueTranslator(sqlExpressionFactory),
new NpgsqlStringMethodTranslator(typeMappingSource, sqlExpressionFactory),
new NpgsqlTrigramsMethodTranslator(typeMappingSource, sqlExpressionFactory, model)
new NpgsqlTrigramsMethodTranslator(typeMappingSource, sqlExpressionFactory, model),
new NpgsqlHstoreTranslator(typeMappingSource, sqlExpressionFactory)
]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ protected override void Print(ExpressionPrinter expressionPrinter)

PgExpressionType.Distance => "<->",

PgExpressionType.HStoreContainsKey => "?",
PgExpressionType.HStoreValueForKey => "->",

_ => throw new ArgumentOutOfRangeException($"Unhandled operator type: {OperatorType}")
})
.Append(" ");
Expand Down
14 changes: 14 additions & 0 deletions src/EFCore.PG/Query/Expressions/PgExpressionType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,4 +159,18 @@ public enum PgExpressionType
LTreeFirstMatches, // ?~ or ?@

#endregion LTree

#region HStore

/// <summary>
/// Represents a PostgreSQL operator for checking if a hstore contains the given key
/// </summary>
HStoreContainsKey, // ?

/// <summary>
/// Represents a PostgreSQL operator for accessing a hstore value for a given key
/// </summary>
HStoreValueForKey, // ->

#endregion HStore
}
3 changes: 3 additions & 0 deletions src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,9 @@ when binaryExpression.Left.TypeMapping is NpgsqlInetTypeMapping or NpgsqlCidrTyp

PgExpressionType.Distance => "<->",

PgExpressionType.HStoreContainsKey => "?",
PgExpressionType.HStoreValueForKey => "->",

_ => throw new ArgumentOutOfRangeException($"Unhandled operator type: {binaryExpression.OperatorType}")
})
.Append(" ");
Expand Down
14 changes: 14 additions & 0 deletions src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public class NpgsqlSqlExpressionFactory : SqlExpressionFactory
{
private readonly NpgsqlTypeMappingSource _typeMappingSource;
private readonly RelationalTypeMapping _boolTypeMapping;
private readonly RelationalTypeMapping _stringTypeMapping;

private static Type? _nodaTimeDurationType;
private static Type? _nodaTimePeriodType;
Expand All @@ -29,6 +30,7 @@ public NpgsqlSqlExpressionFactory(SqlExpressionFactoryDependencies dependencies)
{
_typeMappingSource = (NpgsqlTypeMappingSource)dependencies.TypeMappingSource;
_boolTypeMapping = _typeMappingSource.FindMapping(typeof(bool), dependencies.Model)!;
_stringTypeMapping = _typeMappingSource.FindMapping(typeof(string), dependencies.Model)!;
}

#region Expression factory methods
Expand Down Expand Up @@ -307,6 +309,7 @@ public virtual SqlExpression MakePostgresBinary(
case PgExpressionType.JsonExists:
case PgExpressionType.JsonExistsAny:
case PgExpressionType.JsonExistsAll:
case PgExpressionType.HStoreContainsKey:
returnType = typeof(bool);
break;

Expand Down Expand Up @@ -773,6 +776,7 @@ private SqlExpression ApplyTypeMappingOnPostgresBinary(
case PgExpressionType.JsonExists:
case PgExpressionType.JsonExistsAny:
case PgExpressionType.JsonExistsAll:
case PgExpressionType.HStoreContainsKey:
{
// TODO: For networking, this probably needs to be cleaned up, i.e. we know where the CIDR and INET are
// based on operator type?
Expand Down Expand Up @@ -823,6 +827,16 @@ when left.Type.FullName is "NodaTime.Instant" or "NodaTime.LocalDateTime" or "No
break;
}

case PgExpressionType.HStoreValueForKey:
{
return new PgBinaryExpression(
operatorType,
ApplyDefaultTypeMapping(left),
ApplyDefaultTypeMapping(right),
typeof(string),
_stringTypeMapping);
}

default:
throw new InvalidOperationException(
$"Incorrect {nameof(operatorType)} for {nameof(pgBinaryExpression)}: {operatorType}");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;

/// <summary>
/// The type mapping for the PostgreSQL hstore type. Supports both <see cref="Dictionary{TKey,TValue} " />
/// and <see cref="ImmutableDictionary{TKey,TValue}" /> over strings.
/// and <see cref="ImmutableDictionary{TKey,TValue}" /> where TKey and TValue are both strings.
/// </summary>
/// <remarks>
/// See: https://www.postgresql.org/docs/current/static/hstore.html
Expand Down
Loading

0 comments on commit 8bb6452

Please sign in to comment.