Skip to content

Commit 8f6dad8

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

14 files changed

+606
-3
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
using System.Collections.Immutable;
2+
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions;
3+
using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
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.GetProperty("Item")!.GetMethod!;
33+
34+
private static readonly MethodInfo ImmutableDictionary_Item_Getter =
35+
ImmutableDictionaryType.GetProperty("Item")!.GetMethod!;
36+
37+
private static readonly PropertyInfo Dictionary_Count = DictionaryType.GetProperty(nameof(Dictionary<string, string>.Count))!;
38+
39+
private static readonly PropertyInfo ImmutableDictionary_Count =
40+
ImmutableDictionaryType.GetProperty(nameof(ImmutableDictionary<string, string>.Count))!;
41+
42+
private static readonly PropertyInfo ImmutableDictionary_IsEmpty =
43+
ImmutableDictionaryType.GetProperty(nameof(ImmutableDictionary<string, string>.IsEmpty))!;
44+
45+
private readonly RelationalTypeMapping _stringListTypeMapping;
46+
private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory;
47+
48+
/// <summary>
49+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
50+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
51+
/// any release. You should only use it directly in your code with extreme caution and knowing that
52+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
53+
/// </summary>
54+
public NpgsqlHstoreTranslator(IRelationalTypeMappingSource typeMappingSource, NpgsqlSqlExpressionFactory sqlExpressionFactory)
55+
{
56+
_sqlExpressionFactory = sqlExpressionFactory;
57+
_stringListTypeMapping = typeMappingSource.FindMapping(typeof(List<string>))!;
58+
}
59+
60+
/// <summary>
61+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
62+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
63+
/// any release. You should only use it directly in your code with extreme caution and knowing that
64+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
65+
/// </summary>
66+
public SqlExpression? Translate(
67+
SqlExpression? instance,
68+
MethodInfo method,
69+
IReadOnlyList<SqlExpression> arguments,
70+
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
71+
{
72+
if (instance?.TypeMapping is null || instance.TypeMapping.StoreType != NpgsqlHstoreTypeMapping.HstoreType)
73+
{
74+
return null;
75+
}
76+
77+
if (method == Dictionary_ContainsKey || method == ImmutableDictionary_ContainsKey)
78+
{
79+
return _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.HStoreContainsKey, instance, arguments[0]);
80+
}
81+
82+
if (method == Dictionary_ContainsValue || method == ImmutableDictionary_ContainsValue)
83+
{
84+
return _sqlExpressionFactory.Equal(
85+
arguments[0],
86+
_sqlExpressionFactory.Function(
87+
"ANY", new[]
88+
{
89+
_sqlExpressionFactory.Function(
90+
"avals", new[] { instance }, false, FalseArrays[1], typeof(List<string>), _stringListTypeMapping)
91+
}, false, FalseArrays[1], typeof(string)));
92+
}
93+
94+
if (method == Dictionary_Item_Getter || method == ImmutableDictionary_Item_Getter)
95+
{
96+
return _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.HStoreValueForKey, instance, arguments[0]);
97+
}
98+
return null;
99+
}
100+
101+
/// <summary>
102+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
103+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
104+
/// any release. You should only use it directly in your code with extreme caution and knowing that
105+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
106+
/// </summary>
107+
public SqlExpression? Translate(
108+
SqlExpression? instance,
109+
MemberInfo member,
110+
Type returnType,
111+
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
112+
{
113+
114+
if (instance?.TypeMapping is null || instance.TypeMapping.StoreType != NpgsqlHstoreTypeMapping.HstoreType)
115+
{
116+
return null;
117+
}
118+
119+
if (member == Dictionary_Count || member == ImmutableDictionary_Count)
120+
{
121+
return _sqlExpressionFactory.Function("array_length", new []
122+
{
123+
_sqlExpressionFactory.Function(
124+
"akeys", new[] { instance }, false, FalseArrays[1], typeof(List<string>), _stringListTypeMapping),
125+
_sqlExpressionFactory.Constant(1)
126+
}, false, FalseArrays[2], typeof(int));
127+
}
128+
129+
if (member == ImmutableDictionary_IsEmpty)
130+
{
131+
return _sqlExpressionFactory.Equal(
132+
Translate(instance, Dictionary_Count, typeof(int), logger)!,
133+
_sqlExpressionFactory.Constant(0));
134+
}
135+
return null;
136+
}
137+
}

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.HStoreValueForKey => "->",
531+
PgExpressionType.HStoreContainsKey => "?",
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)