Skip to content

Commit 9496959

Browse files
committed
Hstore query support
Fixes npgsql#212
1 parent 30cebf0 commit 9496959

File tree

9 files changed

+569
-3
lines changed

9 files changed

+569
-3
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
using System.Collections.Immutable;
2+
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions;
3+
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;
4+
using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics;
5+
6+
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal;
7+
8+
/// <summary>
9+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
10+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
11+
/// any release. You should only use it directly in your code with extreme caution and knowing that
12+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
13+
/// </summary>
14+
public class NpgsqlHstoreTranslator : IMethodCallTranslator, IMemberTranslator
15+
{
16+
private static readonly Type DictionaryType = typeof(Dictionary<string, string>);
17+
private static readonly Type ImmutableDictionaryType = typeof(ImmutableDictionary<string, string>);
18+
19+
private static readonly MethodInfo Dictionary_ContainsKey =
20+
DictionaryType.GetMethod(nameof(Dictionary<string, string>.ContainsKey))!;
21+
22+
private static readonly MethodInfo ImmutableDictionary_ContainsKey =
23+
ImmutableDictionaryType.GetMethod(nameof(ImmutableDictionary<string, string>.ContainsKey))!;
24+
25+
private static readonly MethodInfo Dictionary_ContainsValue =
26+
DictionaryType.GetMethod(nameof(Dictionary<string, string>.ContainsValue))!;
27+
28+
private static readonly MethodInfo ImmutableDictionary_ContainsValue =
29+
ImmutableDictionaryType.GetMethod(nameof(ImmutableDictionary<string, string>.ContainsValue))!;
30+
31+
private static readonly MethodInfo Dictionary_Item_Getter =
32+
DictionaryType.FindIndexerProperty()!.GetMethod!;
33+
34+
private static readonly MethodInfo ImmutableDictionary_Item_Getter =
35+
ImmutableDictionaryType.FindIndexerProperty()!.GetMethod!;
36+
37+
private static readonly MethodInfo Enumerable_Any =
38+
typeof(Enumerable).GetMethod(
39+
nameof(Enumerable.Any), BindingFlags.Public | BindingFlags.Static,
40+
[typeof(IEnumerable<>).MakeGenericType(Type.MakeGenericMethodParameter(0))])!
41+
.MakeGenericMethod(typeof(KeyValuePair<string, string>));
42+
43+
private static readonly PropertyInfo Dictionary_Count = DictionaryType.GetProperty(nameof(Dictionary<string, string>.Count))!;
44+
45+
private static readonly PropertyInfo ImmutableDictionary_Count =
46+
ImmutableDictionaryType.GetProperty(nameof(ImmutableDictionary<string, string>.Count))!;
47+
48+
private static readonly PropertyInfo ImmutableDictionary_IsEmpty =
49+
ImmutableDictionaryType.GetProperty(nameof(ImmutableDictionary<string, string>.IsEmpty))!;
50+
51+
private static readonly PropertyInfo Dictionary_Keys = DictionaryType.GetProperty(nameof(Dictionary<string, string>.Keys))!;
52+
53+
private static readonly PropertyInfo ImmutableDictionary_Keys =
54+
ImmutableDictionaryType.GetProperty(nameof(ImmutableDictionary<string, string>.Keys))!;
55+
56+
private static readonly PropertyInfo Dictionary_Values = DictionaryType.GetProperty(nameof(Dictionary<string, string>.Values))!;
57+
58+
private static readonly PropertyInfo ImmutableDictionary_Values =
59+
ImmutableDictionaryType.GetProperty(nameof(ImmutableDictionary<string, string>.Values))!;
60+
61+
private readonly RelationalTypeMapping _stringListTypeMapping;
62+
private readonly RelationalTypeMapping _stringTypeMapping;
63+
private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory;
64+
private readonly RelationalTypeMapping _dictionaryKeyCollectionMapping;
65+
66+
/// <summary>
67+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
68+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
69+
/// any release. You should only use it directly in your code with extreme caution and knowing that
70+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
71+
/// </summary>
72+
public NpgsqlHstoreTranslator(IRelationalTypeMappingSource typeMappingSource, NpgsqlSqlExpressionFactory sqlExpressionFactory)
73+
{
74+
_sqlExpressionFactory = sqlExpressionFactory;
75+
_stringListTypeMapping = typeMappingSource.FindMapping(typeof(List<string>))!;
76+
_stringTypeMapping = typeMappingSource.FindMapping(typeof(string))!;
77+
_dictionaryKeyCollectionMapping = typeMappingSource.FindMapping(typeof(Dictionary<string, string>.KeyCollection))!;
78+
}
79+
80+
/// <summary>
81+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
82+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
83+
/// any release. You should only use it directly in your code with extreme caution and knowing that
84+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
85+
/// </summary>
86+
public SqlExpression? Translate(
87+
SqlExpression? instance,
88+
MethodInfo method,
89+
IReadOnlyList<SqlExpression> arguments,
90+
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
91+
{
92+
if (method == Enumerable_Any && arguments[0].TypeMapping?.StoreType == "hstore")
93+
{
94+
return _sqlExpressionFactory.NotEqual(
95+
Translate(arguments[0], Dictionary_Count, typeof(int), logger)!,
96+
_sqlExpressionFactory.Constant(0));
97+
}
98+
99+
if (instance?.TypeMapping?.StoreType != "hstore")
100+
{
101+
return null;
102+
}
103+
104+
if (method == Dictionary_ContainsKey || method == ImmutableDictionary_ContainsKey)
105+
{
106+
return _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.HStoreContainsKey, instance, arguments[0]);
107+
}
108+
109+
if (method == Dictionary_ContainsValue || method == ImmutableDictionary_ContainsValue)
110+
{
111+
return _sqlExpressionFactory.Any(
112+
arguments[0],
113+
Translate(instance, Dictionary_Values, typeof(List<string>), logger)!,
114+
PgAnyOperatorType.Equal);
115+
}
116+
117+
if (method == Dictionary_Item_Getter || method == ImmutableDictionary_Item_Getter)
118+
{
119+
return _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.HStoreValueForKey, instance, arguments[0], _stringTypeMapping);
120+
}
121+
122+
return null;
123+
}
124+
125+
/// <summary>
126+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
127+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
128+
/// any release. You should only use it directly in your code with extreme caution and knowing that
129+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
130+
/// </summary>
131+
public SqlExpression? Translate(
132+
SqlExpression? instance,
133+
MemberInfo member,
134+
Type returnType,
135+
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
136+
{
137+
138+
if (instance?.TypeMapping?.StoreType != "hstore")
139+
{
140+
return null;
141+
}
142+
143+
if (member == Dictionary_Count || member == ImmutableDictionary_Count)
144+
{
145+
return _sqlExpressionFactory.Function("array_length",
146+
[
147+
Translate(instance, Dictionary_Keys, null!, logger)!,
148+
_sqlExpressionFactory.Constant(1)
149+
], true, TrueArrays[2], typeof(int));
150+
}
151+
152+
if (member == Dictionary_Keys || member == ImmutableDictionary_Keys)
153+
{
154+
return _sqlExpressionFactory.Function(
155+
"akeys", [instance], true, TrueArrays[1], typeof(List<string>), _stringListTypeMapping);
156+
}
157+
158+
if (member == Dictionary_Values || member == ImmutableDictionary_Values)
159+
{
160+
return _sqlExpressionFactory.Function(
161+
"avals", [instance], true, TrueArrays[1], typeof(List<string>), _stringListTypeMapping);
162+
}
163+
164+
if (member == ImmutableDictionary_IsEmpty)
165+
{
166+
return _sqlExpressionFactory.Equal(
167+
Translate(instance, Dictionary_Count, typeof(int), logger)!,
168+
_sqlExpressionFactory.Constant(0));
169+
}
170+
171+
return null;
172+
}
173+
}

src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMemberTranslatorProvider.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ public NpgsqlMemberTranslatorProvider(
4343
JsonPocoTranslator,
4444
new NpgsqlRangeTranslator(typeMappingSource, sqlExpressionFactory, model, supportsMultiranges),
4545
new NpgsqlStringMemberTranslator(sqlExpressionFactory),
46-
new NpgsqlTimeSpanMemberTranslator(sqlExpressionFactory)
46+
new NpgsqlTimeSpanMemberTranslator(sqlExpressionFactory),
47+
new NpgsqlHstoreTranslator(typeMappingSource, sqlExpressionFactory)
4748
]);
4849
}
4950
}

src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ public NpgsqlMethodCallTranslatorProvider(
6161
new NpgsqlRegexIsMatchTranslator(sqlExpressionFactory),
6262
new NpgsqlRowValueTranslator(sqlExpressionFactory),
6363
new NpgsqlStringMethodTranslator(typeMappingSource, sqlExpressionFactory),
64-
new NpgsqlTrigramsMethodTranslator(typeMappingSource, sqlExpressionFactory, model)
64+
new NpgsqlTrigramsMethodTranslator(typeMappingSource, sqlExpressionFactory, model),
65+
new NpgsqlHstoreTranslator(typeMappingSource, sqlExpressionFactory)
6566
]);
6667
}
6768
}

src/EFCore.PG/Query/Expressions/Internal/PgBinaryExpression.cs

+3
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,9 @@ protected override void Print(ExpressionPrinter expressionPrinter)
151151

152152
PgExpressionType.Distance => "<->",
153153

154+
PgExpressionType.HStoreContainsKey => "?",
155+
PgExpressionType.HStoreValueForKey => "->",
156+
154157
_ => throw new ArgumentOutOfRangeException($"Unhandled operator type: {OperatorType}")
155158
})
156159
.Append(" ");

src/EFCore.PG/Query/Expressions/PgExpressionType.cs

+14
Original file line numberDiff line numberDiff line change
@@ -159,4 +159,18 @@ public enum PgExpressionType
159159
LTreeFirstMatches, // ?~ or ?@
160160

161161
#endregion LTree
162+
163+
#region HStore
164+
165+
/// <summary>
166+
/// Represents a PostgreSQL operator for checking if a hstore contains the given key
167+
/// </summary>
168+
HStoreContainsKey, // ?
169+
170+
/// <summary>
171+
/// Represents a PostgreSQL operator for accessing a hstore value for a given key
172+
/// </summary>
173+
HStoreValueForKey, // ->
174+
175+
#endregion HStore
162176
}

src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs

+3
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,9 @@ when binaryExpression.Left.TypeMapping is NpgsqlInetTypeMapping or NpgsqlCidrTyp
527527

528528
PgExpressionType.Distance => "<->",
529529

530+
PgExpressionType.HStoreContainsKey => "?",
531+
PgExpressionType.HStoreValueForKey => "->",
532+
530533
_ => throw new ArgumentOutOfRangeException($"Unhandled operator type: {binaryExpression.OperatorType}")
531534
})
532535
.Append(" ");

src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs

+14
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public class NpgsqlSqlExpressionFactory : SqlExpressionFactory
1616
{
1717
private readonly NpgsqlTypeMappingSource _typeMappingSource;
1818
private readonly RelationalTypeMapping _boolTypeMapping;
19+
private readonly RelationalTypeMapping _stringTypeMapping;
1920

2021
private static Type? _nodaTimeDurationType;
2122
private static Type? _nodaTimePeriodType;
@@ -29,6 +30,7 @@ public NpgsqlSqlExpressionFactory(SqlExpressionFactoryDependencies dependencies)
2930
{
3031
_typeMappingSource = (NpgsqlTypeMappingSource)dependencies.TypeMappingSource;
3132
_boolTypeMapping = _typeMappingSource.FindMapping(typeof(bool), dependencies.Model)!;
33+
_stringTypeMapping = _typeMappingSource.FindMapping(typeof(string), dependencies.Model)!;
3234
}
3335

3436
#region Expression factory methods
@@ -307,6 +309,7 @@ public virtual SqlExpression MakePostgresBinary(
307309
case PgExpressionType.JsonExists:
308310
case PgExpressionType.JsonExistsAny:
309311
case PgExpressionType.JsonExistsAll:
312+
case PgExpressionType.HStoreContainsKey:
310313
returnType = typeof(bool);
311314
break;
312315

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

830+
case PgExpressionType.HStoreValueForKey:
831+
{
832+
return new PgBinaryExpression(
833+
operatorType,
834+
ApplyDefaultTypeMapping(left),
835+
ApplyDefaultTypeMapping(right),
836+
typeof(string),
837+
_stringTypeMapping);
838+
}
839+
826840
default:
827841
throw new InvalidOperationException(
828842
$"Incorrect {nameof(operatorType)} for {nameof(pgBinaryExpression)}: {operatorType}");

src/EFCore.PG/Storage/Internal/Mapping/NpgsqlHstoreTypeMapping.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
55

66
/// <summary>
77
/// The type mapping for the PostgreSQL hstore type. Supports both <see cref="Dictionary{TKey,TValue} " />
8-
/// and <see cref="ImmutableDictionary{TKey,TValue}" /> over strings.
8+
/// and <see cref="ImmutableDictionary{TKey,TValue}" /> where TKey and TValue are both strings.
99
/// </summary>
1010
/// <remarks>
1111
/// See: https://www.postgresql.org/docs/current/static/hstore.html

0 commit comments

Comments
 (0)