Skip to content

Commit df2adf1

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

File tree

9 files changed

+793
-3
lines changed

9 files changed

+793
-3
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
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 MethodInfo Enumerable_ToList =
44+
typeof(Enumerable).GetMethod(
45+
nameof(Enumerable.ToList), BindingFlags.Public | BindingFlags.Static,
46+
[typeof(IEnumerable<>).MakeGenericType(Type.MakeGenericMethodParameter(0))])!
47+
.MakeGenericMethod(typeof(string));
48+
49+
private static readonly MethodInfo Enumerable_ToDictionary =
50+
typeof(Enumerable).GetMethod(
51+
nameof(Enumerable.ToDictionary), BindingFlags.Public | BindingFlags.Static,
52+
[
53+
typeof(IEnumerable<>).MakeGenericType(
54+
typeof(KeyValuePair<,>).MakeGenericType(Type.MakeGenericMethodParameter(0), Type.MakeGenericMethodParameter(1)))
55+
])!.MakeGenericMethod(typeof(string), typeof(string));
56+
57+
private static readonly MethodInfo ImmutableDictionary_ToImmutableDictionary =
58+
typeof(ImmutableDictionary).GetMethod(
59+
nameof(ImmutableDictionary.ToImmutableDictionary), BindingFlags.Public | BindingFlags.Static,
60+
[
61+
typeof(IEnumerable<>).MakeGenericType(
62+
typeof(KeyValuePair<,>).MakeGenericType(Type.MakeGenericMethodParameter(0), Type.MakeGenericMethodParameter(1)))
63+
])!.MakeGenericMethod(typeof(string), typeof(string));
64+
65+
private static readonly MethodInfo Enumerable_Concat = typeof(Enumerable).GetMethod(
66+
nameof(Enumerable.Concat), BindingFlags.Public | BindingFlags.Static,
67+
[
68+
typeof(IEnumerable<>).MakeGenericType(Type.MakeGenericMethodParameter(0)),
69+
typeof(IEnumerable<>).MakeGenericType(Type.MakeGenericMethodParameter(0))
70+
])!.MakeGenericMethod(typeof(KeyValuePair<string, string>));
71+
72+
private static readonly PropertyInfo Dictionary_Count = DictionaryType.GetProperty(nameof(Dictionary<string, string>.Count))!;
73+
74+
private static readonly PropertyInfo ImmutableDictionary_Count =
75+
ImmutableDictionaryType.GetProperty(nameof(ImmutableDictionary<string, string>.Count))!;
76+
77+
private static readonly PropertyInfo ImmutableDictionary_IsEmpty =
78+
ImmutableDictionaryType.GetProperty(nameof(ImmutableDictionary<string, string>.IsEmpty))!;
79+
80+
private static readonly PropertyInfo Dictionary_Keys = DictionaryType.GetProperty(nameof(Dictionary<string, string>.Keys))!;
81+
82+
private static readonly PropertyInfo ImmutableDictionary_Keys =
83+
ImmutableDictionaryType.GetProperty(nameof(ImmutableDictionary<string, string>.Keys))!;
84+
85+
private static readonly PropertyInfo Dictionary_Values = DictionaryType.GetProperty(nameof(Dictionary<string, string>.Values))!;
86+
87+
private static readonly PropertyInfo ImmutableDictionary_Values =
88+
ImmutableDictionaryType.GetProperty(nameof(ImmutableDictionary<string, string>.Values))!;
89+
90+
private readonly RelationalTypeMapping _stringListTypeMapping;
91+
private readonly RelationalTypeMapping _stringTypeMapping;
92+
private readonly RelationalTypeMapping _dictionaryMapping;
93+
private readonly RelationalTypeMapping _immutableDictionaryMapping;
94+
private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory;
95+
/// <summary>
96+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
97+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
98+
/// any release. You should only use it directly in your code with extreme caution and knowing that
99+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
100+
/// </summary>
101+
public NpgsqlHstoreTranslator(IRelationalTypeMappingSource typeMappingSource, NpgsqlSqlExpressionFactory sqlExpressionFactory)
102+
{
103+
_sqlExpressionFactory = sqlExpressionFactory;
104+
_stringListTypeMapping = typeMappingSource.FindMapping(typeof(List<string>))!;
105+
_stringTypeMapping = typeMappingSource.FindMapping(typeof(string))!;
106+
_dictionaryMapping = typeMappingSource.FindMapping(DictionaryType)!;
107+
_immutableDictionaryMapping = typeMappingSource.FindMapping(ImmutableDictionaryType)!;
108+
}
109+
110+
/// <summary>
111+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
112+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
113+
/// any release. You should only use it directly in your code with extreme caution and knowing that
114+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
115+
/// </summary>
116+
public SqlExpression? Translate(
117+
SqlExpression? instance,
118+
MethodInfo method,
119+
IReadOnlyList<SqlExpression> arguments,
120+
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
121+
{
122+
if (instance is null)
123+
{
124+
if (arguments.Count is 2)
125+
{
126+
if (method == Enumerable_Concat
127+
&& arguments[0].TypeMapping?.StoreType == "hstore"
128+
&& arguments[1].TypeMapping?.StoreType == "hstore")
129+
{
130+
return _sqlExpressionFactory.MakePostgresBinary(
131+
PgExpressionType.HStoreConcat, arguments[0], arguments[1], arguments[1].TypeMapping);
132+
}
133+
134+
return null;
135+
}
136+
137+
if (arguments.Count is not 1)
138+
{
139+
return null;
140+
}
141+
142+
if (arguments[0].TypeMapping?.StoreType == "hstore")
143+
{
144+
if (method == Enumerable_Any)
145+
{
146+
return _sqlExpressionFactory.NotEqual(Count(arguments[0]), _sqlExpressionFactory.Constant(0));
147+
}
148+
149+
if (method == Enumerable_ToDictionary)
150+
{
151+
return arguments[0].Type == ImmutableDictionaryType
152+
? _sqlExpressionFactory.Convert(arguments[0], DictionaryType, _dictionaryMapping)
153+
: arguments[0];
154+
}
155+
156+
if (method == ImmutableDictionary_ToImmutableDictionary)
157+
{
158+
return arguments[0].Type == DictionaryType
159+
? _sqlExpressionFactory.Convert(arguments[0], ImmutableDictionaryType, _immutableDictionaryMapping)
160+
: arguments[0];
161+
}
162+
163+
return null;
164+
}
165+
166+
if (method == Enumerable_ToList && arguments[0] is SqlFunctionExpression { Arguments: [{ TypeMapping.StoreType: "hstore" }] })
167+
{
168+
return arguments[0];
169+
}
170+
171+
return null;
172+
}
173+
174+
if (instance.TypeMapping?.StoreType != "hstore")
175+
{
176+
return null;
177+
}
178+
179+
if (method == Dictionary_ContainsKey || method == ImmutableDictionary_ContainsKey)
180+
{
181+
return _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.HStoreContainsKey, instance, arguments[0]);
182+
}
183+
184+
if (method == Dictionary_ContainsValue || method == ImmutableDictionary_ContainsValue)
185+
{
186+
return _sqlExpressionFactory.Any(arguments[0], Values(instance), PgAnyOperatorType.Equal);
187+
}
188+
189+
if (method == Dictionary_Item_Getter || method == ImmutableDictionary_Item_Getter)
190+
{
191+
return _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.HStoreValueForKey, instance, arguments[0], _stringTypeMapping);
192+
}
193+
194+
return null;
195+
}
196+
197+
/// <summary>
198+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
199+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
200+
/// any release. You should only use it directly in your code with extreme caution and knowing that
201+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
202+
/// </summary>
203+
public SqlExpression? Translate(
204+
SqlExpression? instance,
205+
MemberInfo member,
206+
Type returnType,
207+
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
208+
{
209+
210+
if (instance?.TypeMapping?.StoreType != "hstore")
211+
{
212+
return null;
213+
}
214+
215+
if (member == Dictionary_Count || member == ImmutableDictionary_Count)
216+
{
217+
return Count(instance, true);
218+
}
219+
220+
if (member == Dictionary_Keys || member == ImmutableDictionary_Keys)
221+
{
222+
return Keys(instance);
223+
}
224+
225+
if (member == Dictionary_Values || member == ImmutableDictionary_Values)
226+
{
227+
return Values(instance);
228+
}
229+
230+
if (member == ImmutableDictionary_IsEmpty)
231+
{
232+
return _sqlExpressionFactory.Equal(Count(instance), _sqlExpressionFactory.Constant(0));
233+
}
234+
235+
return null;
236+
}
237+
238+
private SqlExpression Keys(SqlExpression instance)
239+
=> _sqlExpressionFactory.Function(
240+
"akeys", [instance], true, TrueArrays[1], typeof(List<string>), _stringListTypeMapping);
241+
242+
private SqlExpression Values(SqlExpression instance)
243+
=> _sqlExpressionFactory.Function(
244+
"avals", [instance], true, TrueArrays[1], typeof(List<string>), _stringListTypeMapping);
245+
246+
private SqlExpression Count(SqlExpression instance, bool nullable = false)
247+
=> _sqlExpressionFactory.Function("cardinality", [Keys(instance)], nullable, TrueArrays[1], typeof(int));
248+
}

Diff for: 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
}

Diff for: 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
}

Diff for: src/EFCore.PG/Query/Expressions/Internal/PgBinaryExpression.cs

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

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

154+
PgExpressionType.HStoreContainsKey => "?",
155+
PgExpressionType.HStoreValueForKey => "->",
156+
PgExpressionType.HStoreConcat => "||",
157+
154158
_ => throw new ArgumentOutOfRangeException($"Unhandled operator type: {OperatorType}")
155159
})
156160
.Append(" ");

Diff for: src/EFCore.PG/Query/Expressions/PgExpressionType.cs

+19
Original file line numberDiff line numberDiff line change
@@ -159,4 +159,23 @@ 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+
/// <summary>
176+
/// Represents a PostgreSQL operator for concatenating hstores
177+
/// </summary>
178+
HStoreConcat, // ||
179+
180+
#endregion HStore
162181
}

Diff for: src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs

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

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

530+
PgExpressionType.HStoreContainsKey => "?",
531+
PgExpressionType.HStoreValueForKey => "->",
532+
PgExpressionType.HStoreConcat => "||",
533+
530534
_ => throw new ArgumentOutOfRangeException($"Unhandled operator type: {binaryExpression.OperatorType}")
531535
})
532536
.Append(" ");

Diff for: src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs

+13
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@ public virtual SqlExpression MakePostgresBinary(
307307
case PgExpressionType.JsonExists:
308308
case PgExpressionType.JsonExistsAny:
309309
case PgExpressionType.JsonExistsAll:
310+
case PgExpressionType.HStoreContainsKey:
310311
returnType = typeof(bool);
311312
break;
312313

@@ -773,6 +774,7 @@ private SqlExpression ApplyTypeMappingOnPostgresBinary(
773774
case PgExpressionType.JsonExists:
774775
case PgExpressionType.JsonExistsAny:
775776
case PgExpressionType.JsonExistsAll:
777+
case PgExpressionType.HStoreContainsKey:
776778
{
777779
// TODO: For networking, this probably needs to be cleaned up, i.e. we know where the CIDR and INET are
778780
// based on operator type?
@@ -823,6 +825,17 @@ when left.Type.FullName is "NodaTime.Instant" or "NodaTime.LocalDateTime" or "No
823825
break;
824826
}
825827

828+
case PgExpressionType.HStoreValueForKey:
829+
case PgExpressionType.HStoreConcat:
830+
{
831+
return new PgBinaryExpression(
832+
operatorType,
833+
ApplyDefaultTypeMapping(left),
834+
ApplyDefaultTypeMapping(right),
835+
typeMapping!.ClrType,
836+
typeMapping);
837+
}
838+
826839
default:
827840
throw new InvalidOperationException(
828841
$"Incorrect {nameof(operatorType)} for {nameof(pgBinaryExpression)}: {operatorType}");

Diff for: 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)