Skip to content

Commit

Permalink
Hstore query support
Browse files Browse the repository at this point in the history
  • Loading branch information
yinzara committed Sep 19, 2024
1 parent 30cebf0 commit 8f6dad8
Show file tree
Hide file tree
Showing 14 changed files with 606 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
using System.Collections.Immutable;
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions;
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 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 (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.Equal(
arguments[0],
_sqlExpressionFactory.Function(
"ANY", new[]
{
_sqlExpressionFactory.Function(
"avals", new[] { instance }, false, FalseArrays[1], typeof(List<string>), _stringListTypeMapping)
}, false, FalseArrays[1], typeof(string)));
}

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

_ => 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

0 comments on commit 8f6dad8

Please sign in to comment.