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 b4e4966
Show file tree
Hide file tree
Showing 9 changed files with 805 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
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 MethodInfo Enumerable_ToDictionary =
typeof(Enumerable).GetMethod(
nameof(Enumerable.ToDictionary), BindingFlags.Public | BindingFlags.Static,
[
typeof(IEnumerable<>).MakeGenericType(
typeof(KeyValuePair<,>).MakeGenericType(Type.MakeGenericMethodParameter(0), Type.MakeGenericMethodParameter(1)))
])!.MakeGenericMethod(typeof(string), typeof(string));

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

private static readonly MethodInfo Enumerable_Concat = typeof(Enumerable).GetMethod(
nameof(Enumerable.Concat), BindingFlags.Public | BindingFlags.Static,
[
typeof(IEnumerable<>).MakeGenericType(Type.MakeGenericMethodParameter(0)),
typeof(IEnumerable<>).MakeGenericType(Type.MakeGenericMethodParameter(0))
])!.MakeGenericMethod(typeof(KeyValuePair<string, 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 RelationalTypeMapping _dictionaryMapping;
private readonly RelationalTypeMapping _immutableDictionaryMapping;
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))!;
_dictionaryMapping = typeMappingSource.FindMapping(DictionaryType)!;
_immutableDictionaryMapping = typeMappingSource.FindMapping(ImmutableDictionaryType)!;
}

/// <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 (instance is null)
{
if (arguments.Count is 2)
{
// store1.Concat(store2) => store1 || store2
if (method == Enumerable_Concat
&& arguments[0].TypeMapping?.StoreType == "hstore"
&& arguments[1].TypeMapping?.StoreType == "hstore")
{
return _sqlExpressionFactory.MakePostgresBinary(
PgExpressionType.HStoreConcat, arguments[0], arguments[1], arguments[1].TypeMapping);
}

return null;
}

if (arguments.Count is not 1)
{
return null;
}

if (arguments[0].TypeMapping?.StoreType == "hstore")
{
// store.Any() => cardinality(akeys(store)) <> 0
if (method == Enumerable_Any)
{
return _sqlExpressionFactory.NotEqual(Count(arguments[0]), _sqlExpressionFactory.Constant(0));
}

// store.ToDictionary() => store OR CAST(store as hstore) OR store::hstore
if (method == Enumerable_ToDictionary)
{
return arguments[0].Type == ImmutableDictionaryType
? _sqlExpressionFactory.Convert(arguments[0], DictionaryType, _dictionaryMapping)
: arguments[0];
}

// store.ToImmutableDictionary() => store OR CAST(store as hstore) OR store::hstore
if (method == ImmutableDictionary_ToImmutableDictionary)
{
return arguments[0].Type == DictionaryType
? _sqlExpressionFactory.Convert(arguments[0], ImmutableDictionaryType, _immutableDictionaryMapping)
: arguments[0];
}

return null;
}

// store.Keys.ToList() => akeys(store) OR store.Values.ToList() -> avals(store)
if (method == Enumerable_ToList && arguments[0] is SqlFunctionExpression { Arguments: [{ TypeMapping.StoreType: "hstore" }] })
{
return arguments[0];
}

return null;
}

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

// store.ContainsKey(key) => store ? key
if (method == Dictionary_ContainsKey || method == ImmutableDictionary_ContainsKey)
{
return _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.HStoreContainsKey, instance, arguments[0]);
}

// store.ContainsValue(value) => value ANY(avals(store))
if (method == Dictionary_ContainsValue || method == ImmutableDictionary_ContainsValue)
{
return _sqlExpressionFactory.Any(arguments[0], Values(instance), PgAnyOperatorType.Equal);
}

// store[key] => store -> key
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;
}

// store.Count => cardinality(akeys(store))
if (member == Dictionary_Count || member == ImmutableDictionary_Count)
{
return Count(instance, true);
}

// store.Keys => akeys(store)
if (member == Dictionary_Keys || member == ImmutableDictionary_Keys)
{
return Keys(instance);
}

// store.Values => avals(store)
if (member == Dictionary_Values || member == ImmutableDictionary_Values)
{
return Values(instance);
}

// store.IsEmpty => cardinality(akeys(store)) = 0
if (member == ImmutableDictionary_IsEmpty)
{
return _sqlExpressionFactory.Equal(Count(instance), _sqlExpressionFactory.Constant(0));
}

return null;
}

private SqlExpression Keys(SqlExpression instance)
=> _sqlExpressionFactory.Function(
"akeys", [instance], true, TrueArrays[1], typeof(List<string>), _stringListTypeMapping);

private SqlExpression Values(SqlExpression instance)
=> _sqlExpressionFactory.Function(
"avals", [instance], true, TrueArrays[1], typeof(List<string>), _stringListTypeMapping);

private SqlExpression Count(SqlExpression instance, bool nullable = false)
=> _sqlExpressionFactory.Function("cardinality", [Keys(instance)], nullable, TrueArrays[1], typeof(int));
}
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,10 @@ protected override void Print(ExpressionPrinter expressionPrinter)

PgExpressionType.Distance => "<->",

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

_ => throw new ArgumentOutOfRangeException($"Unhandled operator type: {OperatorType}")
})
.Append(" ");
Expand Down
19 changes: 19 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,23 @@ 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, // ->

/// <summary>
/// Represents a PostgreSQL operator for concatenating hstores
/// </summary>
HStoreConcat, // ||

#endregion HStore
}
4 changes: 4 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,10 @@ when binaryExpression.Left.TypeMapping is NpgsqlInetTypeMapping or NpgsqlCidrTyp

PgExpressionType.Distance => "<->",

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

_ => throw new ArgumentOutOfRangeException($"Unhandled operator type: {binaryExpression.OperatorType}")
})
.Append(" ");
Expand Down
13 changes: 13 additions & 0 deletions src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,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 +774,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 +825,17 @@ when left.Type.FullName is "NodaTime.Instant" or "NodaTime.LocalDateTime" or "No
break;
}

case PgExpressionType.HStoreValueForKey:
case PgExpressionType.HStoreConcat:
{
return new PgBinaryExpression(
operatorType,
ApplyDefaultTypeMapping(left),
ApplyDefaultTypeMapping(right),
typeMapping!.ClrType,
typeMapping);
}

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 b4e4966

Please sign in to comment.