Skip to content

Commit 92992e5

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

14 files changed

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

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

+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(" ");

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

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

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

+18
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,12 +309,17 @@ 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

313316
case PgExpressionType.Distance:
314317
returnType = typeof(double);
315318
break;
319+
320+
case PgExpressionType.HStoreValueForKey:
321+
returnType = typeof(string);
322+
break;
316323
}
317324

318325
return (PgBinaryExpression)ApplyTypeMapping(
@@ -773,6 +780,7 @@ private SqlExpression ApplyTypeMappingOnPostgresBinary(
773780
case PgExpressionType.JsonExists:
774781
case PgExpressionType.JsonExistsAny:
775782
case PgExpressionType.JsonExistsAll:
783+
case PgExpressionType.HStoreContainsKey:
776784
{
777785
// TODO: For networking, this probably needs to be cleaned up, i.e. we know where the CIDR and INET are
778786
// based on operator type?
@@ -823,6 +831,16 @@ when left.Type.FullName is "NodaTime.Instant" or "NodaTime.LocalDateTime" or "No
823831
break;
824832
}
825833

834+
case PgExpressionType.HStoreValueForKey:
835+
{
836+
return new PgBinaryExpression(
837+
operatorType,
838+
ApplyDefaultTypeMapping(left),
839+
ApplyDefaultTypeMapping(right),
840+
typeof(string),
841+
_stringTypeMapping);
842+
}
843+
826844
default:
827845
throw new InvalidOperationException(
828846
$"Incorrect {nameof(operatorType)} for {nameof(pgBinaryExpression)}: {operatorType}");

Diff for: src/EFCore.PG/Storage/Internal/Mapping/NpgsqlHstoreTypeMapping.cs

+6-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ public class NpgsqlHstoreTypeMapping : NpgsqlTypeMapping
1414
{
1515
private static readonly HstoreMutableComparer MutableComparerInstance = new();
1616

17+
/// <summary>
18+
/// The database store type of the Hstore type
19+
/// </summary>
20+
public const string HstoreType = "hstore";
21+
1722
/// <summary>
1823
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
1924
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -32,7 +37,7 @@ public NpgsqlHstoreTypeMapping(Type clrType)
3237
: base(
3338
new RelationalTypeMappingParameters(
3439
new CoreTypeMappingParameters(clrType, comparer: GetComparer(clrType)),
35-
"hstore"),
40+
HstoreType),
3641
NpgsqlDbType.Hstore)
3742
{
3843
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using Npgsql.EntityFrameworkCore.PostgreSQL.TestModels.Dictionary;
2+
using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities;
3+
4+
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query;
5+
6+
public class HstoreQueryFixture : SharedStoreFixtureBase<DictionaryQueryContext>, IQueryFixtureBase, ITestSqlLoggerFactory
7+
{
8+
protected override string StoreName
9+
=> "HstoreQueryTest";
10+
11+
protected override ITestStoreFactory TestStoreFactory
12+
=> NpgsqlTestStoreFactory.Instance;
13+
14+
public TestSqlLoggerFactory TestSqlLoggerFactory
15+
=> (TestSqlLoggerFactory)ListLoggerFactory;
16+
17+
private DictionaryQueryData _expectedData;
18+
19+
public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
20+
=> base.AddOptions(builder).ConfigureWarnings(wcb => wcb.Ignore(CoreEventId.CollectionWithoutComparer));
21+
22+
protected override Task SeedAsync(DictionaryQueryContext context)
23+
=> DictionaryQueryContext.SeedAsync(context);
24+
25+
public Func<DbContext> GetContextCreator()
26+
=> CreateContext;
27+
28+
public ISetSource GetExpectedData()
29+
=> _expectedData ??= new DictionaryQueryData();
30+
31+
public IReadOnlyDictionary<Type, object> EntitySorters
32+
=> new Dictionary<Type, Func<object, object>>
33+
{
34+
{ typeof(DictionaryEntity), e => ((DictionaryEntity)e)?.Id }, { typeof(DictionaryContainerEntity), e => ((DictionaryContainerEntity)e)?.Id }
35+
}.ToDictionary(e => e.Key, e => (object)e.Value);
36+
37+
public IReadOnlyDictionary<Type, object> EntityAsserters
38+
=> new Dictionary<Type, Action<object, object>>
39+
{
40+
{
41+
typeof(DictionaryEntity), (e, a) =>
42+
{
43+
Assert.Equal(e is null, a is null);
44+
if (a is not null)
45+
{
46+
var ee = (DictionaryEntity)e;
47+
var aa = (DictionaryEntity)a;
48+
49+
Assert.Equal(ee.Id, aa.Id);
50+
Assert.Equal(ee.Dictionary, ee.Dictionary);
51+
Assert.Equal(ee.ImmutableDictionary, ee.ImmutableDictionary);
52+
Assert.Equal(ee.NullableDictionary, ee.NullableDictionary);
53+
Assert.Equal(ee.NullableImmutableDictionary, ee.NullableImmutableDictionary);
54+
55+
}
56+
}
57+
},
58+
{
59+
typeof(DictionaryContainerEntity), (e, a) =>
60+
{
61+
Assert.Equal(e is null, a is null);
62+
if (a is not null)
63+
{
64+
var ee = (DictionaryContainerEntity)e;
65+
var aa = (DictionaryContainerEntity)a;
66+
67+
Assert.Equal(ee.Id, aa.Id);
68+
Assert.Equal(ee.DictionaryEntities, ee.DictionaryEntities);
69+
}
70+
}
71+
}
72+
}.ToDictionary(e => e.Key, e => (object)e.Value);
73+
}

0 commit comments

Comments
 (0)