Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hstore query support #3285

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
using System.Collections.Immutable;
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions;
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;
using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics;

namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public class NpgsqlHstoreTranslator : IMethodCallTranslator, IMemberTranslator
{
private static readonly Type DictionaryType = typeof(Dictionary<string, string>);
private static readonly Type ImmutableDictionaryType = typeof(ImmutableDictionary<string, string>);

private static readonly MethodInfo Dictionary_ContainsKey =
DictionaryType.GetMethod(nameof(Dictionary<string, string>.ContainsKey))!;

private static readonly MethodInfo ImmutableDictionary_ContainsKey =
ImmutableDictionaryType.GetMethod(nameof(ImmutableDictionary<string, string>.ContainsKey))!;

private static readonly MethodInfo Dictionary_ContainsValue =
DictionaryType.GetMethod(nameof(Dictionary<string, string>.ContainsValue))!;

private static readonly MethodInfo ImmutableDictionary_ContainsValue =
ImmutableDictionaryType.GetMethod(nameof(ImmutableDictionary<string, string>.ContainsValue))!;

private static readonly MethodInfo Dictionary_Item_Getter =
DictionaryType.GetProperty("Item")!.GetMethod!;

private static readonly MethodInfo ImmutableDictionary_Item_Getter =
ImmutableDictionaryType.GetProperty("Item")!.GetMethod!;

private static readonly MethodInfo Enumerable_Any =
typeof(Enumerable).GetMethod(nameof(Enumerable.Any),
BindingFlags.Public | BindingFlags.Static, new[] { typeof(IEnumerable<>).MakeGenericType(Type.MakeGenericMethodParameter(0)) })!
.MakeGenericMethod(typeof(KeyValuePair<string, string>));

private static readonly PropertyInfo Dictionary_Count = DictionaryType.GetProperty(nameof(Dictionary<string, string>.Count))!;

private static readonly PropertyInfo ImmutableDictionary_Count =
ImmutableDictionaryType.GetProperty(nameof(ImmutableDictionary<string, string>.Count))!;

private static readonly PropertyInfo ImmutableDictionary_IsEmpty =
ImmutableDictionaryType.GetProperty(nameof(ImmutableDictionary<string, string>.IsEmpty))!;

private readonly RelationalTypeMapping _stringListTypeMapping;
private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public NpgsqlHstoreTranslator(IRelationalTypeMappingSource typeMappingSource, NpgsqlSqlExpressionFactory sqlExpressionFactory)
{
_sqlExpressionFactory = sqlExpressionFactory;
_stringListTypeMapping = typeMappingSource.FindMapping(typeof(List<string>))!;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public SqlExpression? Translate(
SqlExpression? instance,
MethodInfo method,
IReadOnlyList<SqlExpression> arguments,
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
{
if (method == Enumerable_Any)
{
var value = instance ?? arguments[0];
if (value.TypeMapping?.StoreType == NpgsqlHstoreTypeMapping.HstoreType)
{
return _sqlExpressionFactory.NotEqual(
Translate(value, Dictionary_Count, typeof(int), logger)!,
_sqlExpressionFactory.Constant(0));
}
return null;
}

if (instance?.TypeMapping is null || instance.TypeMapping.StoreType != NpgsqlHstoreTypeMapping.HstoreType)
{
return null;
}

if (method == Dictionary_ContainsKey || method == ImmutableDictionary_ContainsKey)
{
return _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.HStoreContainsKey, instance, arguments[0]);
}

if (method == Dictionary_ContainsValue || method == ImmutableDictionary_ContainsValue)
{
return _sqlExpressionFactory.Any(
arguments[0],
_sqlExpressionFactory.Function(
"avals", new[] { instance }, false, FalseArrays[1], typeof(List<string>), _stringListTypeMapping),
PgAnyOperatorType.Equal);
}

if (method == Dictionary_Item_Getter || method == ImmutableDictionary_Item_Getter)
{
return _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.HStoreValueForKey, instance, arguments[0]);
}
return null;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public SqlExpression? Translate(
SqlExpression? instance,
MemberInfo member,
Type returnType,
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
{

if (instance?.TypeMapping is null || instance.TypeMapping.StoreType != NpgsqlHstoreTypeMapping.HstoreType)
{
return null;
}

if (member == Dictionary_Count || member == ImmutableDictionary_Count)
{
return _sqlExpressionFactory.Function("array_length", new []
{
_sqlExpressionFactory.Function(
"akeys", new[] { instance }, false, FalseArrays[1], typeof(List<string>), _stringListTypeMapping),
_sqlExpressionFactory.Constant(1)
}, false, FalseArrays[2], typeof(int));
}

if (member == ImmutableDictionary_IsEmpty)
{
return _sqlExpressionFactory.Equal(
Translate(instance, Dictionary_Count, typeof(int), logger)!,
_sqlExpressionFactory.Constant(0));
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ public NpgsqlMemberTranslatorProvider(
JsonPocoTranslator,
new NpgsqlRangeTranslator(typeMappingSource, sqlExpressionFactory, model, supportsMultiranges),
new NpgsqlStringMemberTranslator(sqlExpressionFactory),
new NpgsqlTimeSpanMemberTranslator(sqlExpressionFactory)
new NpgsqlTimeSpanMemberTranslator(sqlExpressionFactory),
new NpgsqlHstoreTranslator(typeMappingSource, sqlExpressionFactory)
]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ public NpgsqlMethodCallTranslatorProvider(
new NpgsqlRegexIsMatchTranslator(sqlExpressionFactory),
new NpgsqlRowValueTranslator(sqlExpressionFactory),
new NpgsqlStringMethodTranslator(typeMappingSource, sqlExpressionFactory),
new NpgsqlTrigramsMethodTranslator(typeMappingSource, sqlExpressionFactory, model)
new NpgsqlTrigramsMethodTranslator(typeMappingSource, sqlExpressionFactory, model),
new NpgsqlHstoreTranslator(typeMappingSource, sqlExpressionFactory)
]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ protected override void Print(ExpressionPrinter expressionPrinter)

PgExpressionType.Distance => "<->",

PgExpressionType.HStoreContainsKey => "?",
PgExpressionType.HStoreValueForKey => "->",

_ => throw new ArgumentOutOfRangeException($"Unhandled operator type: {OperatorType}")
})
.Append(" ");
Expand Down
14 changes: 14 additions & 0 deletions src/EFCore.PG/Query/Expressions/PgExpressionType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,4 +159,18 @@ public enum PgExpressionType
LTreeFirstMatches, // ?~ or ?@

#endregion LTree

#region HStore

/// <summary>
/// Represents a PostgreSQL operator for checking if a hstore contains the given key
/// </summary>
HStoreContainsKey, // ?

/// <summary>
/// Represents a PostgreSQL operator for accessing a hstore value for a given key
/// </summary>
HStoreValueForKey, // ->

#endregion HStore
}
3 changes: 3 additions & 0 deletions src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,9 @@ when binaryExpression.Left.TypeMapping is NpgsqlInetTypeMapping or NpgsqlCidrTyp

PgExpressionType.Distance => "<->",

PgExpressionType.HStoreContainsKey => "?",
PgExpressionType.HStoreValueForKey => "->",

_ => throw new ArgumentOutOfRangeException($"Unhandled operator type: {binaryExpression.OperatorType}")
})
.Append(" ");
Expand Down
18 changes: 18 additions & 0 deletions src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public class NpgsqlSqlExpressionFactory : SqlExpressionFactory
{
private readonly NpgsqlTypeMappingSource _typeMappingSource;
private readonly RelationalTypeMapping _boolTypeMapping;
private readonly RelationalTypeMapping _stringTypeMapping;

private static Type? _nodaTimeDurationType;
private static Type? _nodaTimePeriodType;
Expand All @@ -29,6 +30,7 @@ public NpgsqlSqlExpressionFactory(SqlExpressionFactoryDependencies dependencies)
{
_typeMappingSource = (NpgsqlTypeMappingSource)dependencies.TypeMappingSource;
_boolTypeMapping = _typeMappingSource.FindMapping(typeof(bool), dependencies.Model)!;
_stringTypeMapping = _typeMappingSource.FindMapping(typeof(string), dependencies.Model)!;
}

#region Expression factory methods
Expand Down Expand Up @@ -307,12 +309,17 @@ public virtual SqlExpression MakePostgresBinary(
case PgExpressionType.JsonExists:
case PgExpressionType.JsonExistsAny:
case PgExpressionType.JsonExistsAll:
case PgExpressionType.HStoreContainsKey:
returnType = typeof(bool);
break;

case PgExpressionType.Distance:
returnType = typeof(double);
break;

case PgExpressionType.HStoreValueForKey:
returnType = typeof(string);
break;
}

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

case PgExpressionType.HStoreValueForKey:
{
return new PgBinaryExpression(
operatorType,
ApplyDefaultTypeMapping(left),
ApplyDefaultTypeMapping(right),
typeof(string),
_stringTypeMapping);
}

default:
throw new InvalidOperationException(
$"Incorrect {nameof(operatorType)} for {nameof(pgBinaryExpression)}: {operatorType}");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ public class NpgsqlHstoreTypeMapping : NpgsqlTypeMapping
{
private static readonly HstoreMutableComparer MutableComparerInstance = new();

/// <summary>
/// The database store type of the Hstore type
/// </summary>
public const string HstoreType = "hstore";

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Expand All @@ -32,7 +37,7 @@ public NpgsqlHstoreTypeMapping(Type clrType)
: base(
new RelationalTypeMappingParameters(
new CoreTypeMappingParameters(clrType, comparer: GetComparer(clrType)),
"hstore"),
HstoreType),
NpgsqlDbType.Hstore)
{
}
Expand Down
73 changes: 73 additions & 0 deletions test/EFCore.PG.FunctionalTests/Query/HstoreQueryFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using Npgsql.EntityFrameworkCore.PostgreSQL.TestModels.Dictionary;
using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities;

namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query;

public class HstoreQueryFixture : SharedStoreFixtureBase<DictionaryQueryContext>, IQueryFixtureBase, ITestSqlLoggerFactory
{
protected override string StoreName
=> "HstoreQueryTest";

protected override ITestStoreFactory TestStoreFactory
=> NpgsqlTestStoreFactory.Instance;

public TestSqlLoggerFactory TestSqlLoggerFactory
=> (TestSqlLoggerFactory)ListLoggerFactory;

private DictionaryQueryData _expectedData;

public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
=> base.AddOptions(builder).ConfigureWarnings(wcb => wcb.Ignore(CoreEventId.CollectionWithoutComparer));

protected override Task SeedAsync(DictionaryQueryContext context)
=> DictionaryQueryContext.SeedAsync(context);

public Func<DbContext> GetContextCreator()
=> CreateContext;

public ISetSource GetExpectedData()
=> _expectedData ??= new DictionaryQueryData();

public IReadOnlyDictionary<Type, object> EntitySorters
=> new Dictionary<Type, Func<object, object>>
{
{ typeof(DictionaryEntity), e => ((DictionaryEntity)e)?.Id }, { typeof(DictionaryContainerEntity), e => ((DictionaryContainerEntity)e)?.Id }
}.ToDictionary(e => e.Key, e => (object)e.Value);

public IReadOnlyDictionary<Type, object> EntityAsserters
=> new Dictionary<Type, Action<object, object>>
{
{
typeof(DictionaryEntity), (e, a) =>
{
Assert.Equal(e is null, a is null);
if (a is not null)
{
var ee = (DictionaryEntity)e;
var aa = (DictionaryEntity)a;

Assert.Equal(ee.Id, aa.Id);
Assert.Equal(ee.Dictionary, ee.Dictionary);
Assert.Equal(ee.ImmutableDictionary, ee.ImmutableDictionary);
Assert.Equal(ee.NullableDictionary, ee.NullableDictionary);
Assert.Equal(ee.NullableImmutableDictionary, ee.NullableImmutableDictionary);

}
}
},
{
typeof(DictionaryContainerEntity), (e, a) =>
{
Assert.Equal(e is null, a is null);
if (a is not null)
{
var ee = (DictionaryContainerEntity)e;
var aa = (DictionaryContainerEntity)a;

Assert.Equal(ee.Id, aa.Id);
Assert.Equal(ee.DictionaryEntities, ee.DictionaryEntities);
}
}
}
}.ToDictionary(e => e.Key, e => (object)e.Value);
}
Loading