Skip to content

Commit 1d6ee17

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

14 files changed

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

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

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

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)