Skip to content

Commit b4e4966

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

File tree

9 files changed

+805
-3
lines changed

9 files changed

+805
-3
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
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+
// store1.Concat(store2) => store1 || store2
127+
if (method == Enumerable_Concat
128+
&& arguments[0].TypeMapping?.StoreType == "hstore"
129+
&& arguments[1].TypeMapping?.StoreType == "hstore")
130+
{
131+
return _sqlExpressionFactory.MakePostgresBinary(
132+
PgExpressionType.HStoreConcat, arguments[0], arguments[1], arguments[1].TypeMapping);
133+
}
134+
135+
return null;
136+
}
137+
138+
if (arguments.Count is not 1)
139+
{
140+
return null;
141+
}
142+
143+
if (arguments[0].TypeMapping?.StoreType == "hstore")
144+
{
145+
// store.Any() => cardinality(akeys(store)) <> 0
146+
if (method == Enumerable_Any)
147+
{
148+
return _sqlExpressionFactory.NotEqual(Count(arguments[0]), _sqlExpressionFactory.Constant(0));
149+
}
150+
151+
// store.ToDictionary() => store OR CAST(store as hstore) OR store::hstore
152+
if (method == Enumerable_ToDictionary)
153+
{
154+
return arguments[0].Type == ImmutableDictionaryType
155+
? _sqlExpressionFactory.Convert(arguments[0], DictionaryType, _dictionaryMapping)
156+
: arguments[0];
157+
}
158+
159+
// store.ToImmutableDictionary() => store OR CAST(store as hstore) OR store::hstore
160+
if (method == ImmutableDictionary_ToImmutableDictionary)
161+
{
162+
return arguments[0].Type == DictionaryType
163+
? _sqlExpressionFactory.Convert(arguments[0], ImmutableDictionaryType, _immutableDictionaryMapping)
164+
: arguments[0];
165+
}
166+
167+
return null;
168+
}
169+
170+
// store.Keys.ToList() => akeys(store) OR store.Values.ToList() -> avals(store)
171+
if (method == Enumerable_ToList && arguments[0] is SqlFunctionExpression { Arguments: [{ TypeMapping.StoreType: "hstore" }] })
172+
{
173+
return arguments[0];
174+
}
175+
176+
return null;
177+
}
178+
179+
if (instance.TypeMapping?.StoreType != "hstore")
180+
{
181+
return null;
182+
}
183+
184+
// store.ContainsKey(key) => store ? key
185+
if (method == Dictionary_ContainsKey || method == ImmutableDictionary_ContainsKey)
186+
{
187+
return _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.HStoreContainsKey, instance, arguments[0]);
188+
}
189+
190+
// store.ContainsValue(value) => value ANY(avals(store))
191+
if (method == Dictionary_ContainsValue || method == ImmutableDictionary_ContainsValue)
192+
{
193+
return _sqlExpressionFactory.Any(arguments[0], Values(instance), PgAnyOperatorType.Equal);
194+
}
195+
196+
// store[key] => store -> key
197+
if (method == Dictionary_Item_Getter || method == ImmutableDictionary_Item_Getter)
198+
{
199+
return _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.HStoreValueForKey, instance, arguments[0], _stringTypeMapping);
200+
}
201+
202+
return null;
203+
}
204+
205+
/// <summary>
206+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
207+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
208+
/// any release. You should only use it directly in your code with extreme caution and knowing that
209+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
210+
/// </summary>
211+
public SqlExpression? Translate(
212+
SqlExpression? instance,
213+
MemberInfo member,
214+
Type returnType,
215+
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
216+
{
217+
218+
if (instance?.TypeMapping?.StoreType != "hstore")
219+
{
220+
return null;
221+
}
222+
223+
// store.Count => cardinality(akeys(store))
224+
if (member == Dictionary_Count || member == ImmutableDictionary_Count)
225+
{
226+
return Count(instance, true);
227+
}
228+
229+
// store.Keys => akeys(store)
230+
if (member == Dictionary_Keys || member == ImmutableDictionary_Keys)
231+
{
232+
return Keys(instance);
233+
}
234+
235+
// store.Values => avals(store)
236+
if (member == Dictionary_Values || member == ImmutableDictionary_Values)
237+
{
238+
return Values(instance);
239+
}
240+
241+
// store.IsEmpty => cardinality(akeys(store)) = 0
242+
if (member == ImmutableDictionary_IsEmpty)
243+
{
244+
return _sqlExpressionFactory.Equal(Count(instance), _sqlExpressionFactory.Constant(0));
245+
}
246+
247+
return null;
248+
}
249+
250+
private SqlExpression Keys(SqlExpression instance)
251+
=> _sqlExpressionFactory.Function(
252+
"akeys", [instance], true, TrueArrays[1], typeof(List<string>), _stringListTypeMapping);
253+
254+
private SqlExpression Values(SqlExpression instance)
255+
=> _sqlExpressionFactory.Function(
256+
"avals", [instance], true, TrueArrays[1], typeof(List<string>), _stringListTypeMapping);
257+
258+
private SqlExpression Count(SqlExpression instance, bool nullable = false)
259+
=> _sqlExpressionFactory.Function("cardinality", [Keys(instance)], nullable, TrueArrays[1], typeof(int));
260+
}

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)