diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlHstoreTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlHstoreTranslator.cs new file mode 100644 index 000000000..33f875eb3 --- /dev/null +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlHstoreTranslator.cs @@ -0,0 +1,179 @@ +using System.Collections.Immutable; +using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions; +using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; +using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; + +/// +/// 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. +/// +public class NpgsqlHstoreTranslator : IMethodCallTranslator, IMemberTranslator +{ + private static readonly Type DictionaryType = typeof(Dictionary); + private static readonly Type ImmutableDictionaryType = typeof(ImmutableDictionary); + + private static readonly MethodInfo Dictionary_ContainsKey = + DictionaryType.GetMethod(nameof(Dictionary.ContainsKey))!; + + private static readonly MethodInfo ImmutableDictionary_ContainsKey = + ImmutableDictionaryType.GetMethod(nameof(ImmutableDictionary.ContainsKey))!; + + private static readonly MethodInfo Dictionary_ContainsValue = + DictionaryType.GetMethod(nameof(Dictionary.ContainsValue))!; + + private static readonly MethodInfo ImmutableDictionary_ContainsValue = + ImmutableDictionaryType.GetMethod(nameof(ImmutableDictionary.ContainsValue))!; + + private static readonly MethodInfo Dictionary_Item_Getter = + DictionaryType.FindIndexerProperty()!.GetMethod!; + + private static readonly MethodInfo ImmutableDictionary_Item_Getter = + ImmutableDictionaryType.FindIndexerProperty()!.GetMethod!; + + private static readonly MethodInfo Enumerable_Any = + typeof(Enumerable).GetMethod( + nameof(Enumerable.Any), BindingFlags.Public | BindingFlags.Static, + [typeof(IEnumerable<>).MakeGenericType(Type.MakeGenericMethodParameter(0))])! + .MakeGenericMethod(typeof(KeyValuePair)); + + private static readonly MethodInfo Enumerable_ToList = + typeof(Enumerable).GetMethod( + nameof(Enumerable.ToList), BindingFlags.Public | BindingFlags.Static, + [typeof(IEnumerable<>).MakeGenericType(Type.MakeGenericMethodParameter(0))])! + .MakeGenericMethod(typeof(string)); + + private static readonly PropertyInfo Dictionary_Count = DictionaryType.GetProperty(nameof(Dictionary.Count))!; + + private static readonly PropertyInfo ImmutableDictionary_Count = + ImmutableDictionaryType.GetProperty(nameof(ImmutableDictionary.Count))!; + + private static readonly PropertyInfo ImmutableDictionary_IsEmpty = + ImmutableDictionaryType.GetProperty(nameof(ImmutableDictionary.IsEmpty))!; + + private static readonly PropertyInfo Dictionary_Keys = DictionaryType.GetProperty(nameof(Dictionary.Keys))!; + + private static readonly PropertyInfo ImmutableDictionary_Keys = + ImmutableDictionaryType.GetProperty(nameof(ImmutableDictionary.Keys))!; + + private static readonly PropertyInfo Dictionary_Values = DictionaryType.GetProperty(nameof(Dictionary.Values))!; + + private static readonly PropertyInfo ImmutableDictionary_Values = + ImmutableDictionaryType.GetProperty(nameof(ImmutableDictionary.Values))!; + + private readonly RelationalTypeMapping _stringListTypeMapping; + private readonly RelationalTypeMapping _stringTypeMapping; + private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory; + /// + /// 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. + /// + public NpgsqlHstoreTranslator(IRelationalTypeMappingSource typeMappingSource, NpgsqlSqlExpressionFactory sqlExpressionFactory) + { + _sqlExpressionFactory = sqlExpressionFactory; + _stringListTypeMapping = typeMappingSource.FindMapping(typeof(List))!; + _stringTypeMapping = typeMappingSource.FindMapping(typeof(string))!; + } + + /// + /// 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. + /// + public SqlExpression? Translate( + SqlExpression? instance, + MethodInfo method, + IReadOnlyList arguments, + IDiagnosticsLogger logger) + { + if (method == Enumerable_Any && arguments[0].TypeMapping?.StoreType == "hstore") + { + return _sqlExpressionFactory.NotEqual(Count(arguments[0]), _sqlExpressionFactory.Constant(0)); + } + + if (method == Enumerable_ToList && arguments[0] is SqlFunctionExpression { Arguments: [{ TypeMapping.StoreType: "hstore" }] }) + { + return arguments[0]; + } + + if (instance?.TypeMapping?.StoreType != "hstore") + { + 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], Values(instance), PgAnyOperatorType.Equal); + } + + if (method == Dictionary_Item_Getter || method == ImmutableDictionary_Item_Getter) + { + return _sqlExpressionFactory.MakePostgresBinary(PgExpressionType.HStoreValueForKey, instance, arguments[0], _stringTypeMapping); + } + + return null; + } + + /// + /// 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. + /// + public SqlExpression? Translate( + SqlExpression? instance, + MemberInfo member, + Type returnType, + IDiagnosticsLogger logger) + { + + if (instance?.TypeMapping?.StoreType != "hstore") + { + return null; + } + + if (member == Dictionary_Count || member == ImmutableDictionary_Count) + { + return Count(instance, true); + } + + if (member == Dictionary_Keys || member == ImmutableDictionary_Keys) + { + return Keys(instance); + } + + if (member == Dictionary_Values || member == ImmutableDictionary_Values) + { + return Values(instance); + } + + if (member == ImmutableDictionary_IsEmpty) + { + return _sqlExpressionFactory.Equal(Count(instance), _sqlExpressionFactory.Constant(0)); + } + + return null; + } + + private SqlExpression Keys(SqlExpression instance) + => _sqlExpressionFactory.Function( + "akeys", [instance], true, TrueArrays[1], typeof(List), _stringListTypeMapping); + + private SqlExpression Values(SqlExpression instance) + => _sqlExpressionFactory.Function( + "avals", [instance], true, TrueArrays[1], typeof(List), _stringListTypeMapping); + + private SqlExpression Count(SqlExpression instance, bool nullable = false) + => _sqlExpressionFactory.Function("cardinality", [Keys(instance)], nullable, TrueArrays[1], typeof(int)); +} diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMemberTranslatorProvider.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMemberTranslatorProvider.cs index 28ab9785a..62859b1c4 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMemberTranslatorProvider.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMemberTranslatorProvider.cs @@ -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) ]); } } diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs index 63843eab3..785f67a57 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlMethodCallTranslatorProvider.cs @@ -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) ]); } } diff --git a/src/EFCore.PG/Query/Expressions/Internal/PgBinaryExpression.cs b/src/EFCore.PG/Query/Expressions/Internal/PgBinaryExpression.cs index 03387b988..040bee0fd 100644 --- a/src/EFCore.PG/Query/Expressions/Internal/PgBinaryExpression.cs +++ b/src/EFCore.PG/Query/Expressions/Internal/PgBinaryExpression.cs @@ -151,6 +151,9 @@ protected override void Print(ExpressionPrinter expressionPrinter) PgExpressionType.Distance => "<->", + PgExpressionType.HStoreContainsKey => "?", + PgExpressionType.HStoreValueForKey => "->", + _ => throw new ArgumentOutOfRangeException($"Unhandled operator type: {OperatorType}") }) .Append(" "); diff --git a/src/EFCore.PG/Query/Expressions/PgExpressionType.cs b/src/EFCore.PG/Query/Expressions/PgExpressionType.cs index 270a67e01..3a167d59e 100644 --- a/src/EFCore.PG/Query/Expressions/PgExpressionType.cs +++ b/src/EFCore.PG/Query/Expressions/PgExpressionType.cs @@ -159,4 +159,18 @@ public enum PgExpressionType LTreeFirstMatches, // ?~ or ?@ #endregion LTree + + #region HStore + + /// + /// Represents a PostgreSQL operator for checking if a hstore contains the given key + /// + HStoreContainsKey, // ? + + /// + /// Represents a PostgreSQL operator for accessing a hstore value for a given key + /// + HStoreValueForKey, // -> + + #endregion HStore } diff --git a/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs b/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs index 3418d5045..73fec4157 100644 --- a/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs +++ b/src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs @@ -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(" "); diff --git a/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs b/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs index 84fac6794..7ffd21594 100644 --- a/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs +++ b/src/EFCore.PG/Query/NpgsqlSqlExpressionFactory.cs @@ -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; @@ -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 @@ -307,6 +309,7 @@ public virtual SqlExpression MakePostgresBinary( case PgExpressionType.JsonExists: case PgExpressionType.JsonExistsAny: case PgExpressionType.JsonExistsAll: + case PgExpressionType.HStoreContainsKey: returnType = typeof(bool); break; @@ -773,6 +776,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? @@ -823,6 +827,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}"); diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlHstoreTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlHstoreTypeMapping.cs index 3d6eef2f0..0b8b5f0dc 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlHstoreTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlHstoreTypeMapping.cs @@ -5,7 +5,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; /// /// The type mapping for the PostgreSQL hstore type. Supports both -/// and over strings. +/// and where TKey and TValue are both strings. /// /// /// See: https://www.postgresql.org/docs/current/static/hstore.html diff --git a/test/EFCore.PG.FunctionalTests/Query/HstoreQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/HstoreQueryTest.cs new file mode 100644 index 000000000..14c4a91b5 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/HstoreQueryTest.cs @@ -0,0 +1,386 @@ +using System.Collections.Immutable; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; + +public class DictionaryEntity +{ + public int Id { get; set; } + + public Dictionary Dictionary { get; set; } = null!; + + public ImmutableDictionary ImmutableDictionary { get; set; } = null!; + +} + +public class DictionaryQueryContext(DbContextOptions options) : PoolableDbContext(options) +{ + public DbSet SomeEntities { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(); + } + + public static async Task SeedAsync(DictionaryQueryContext context) + { + var arrayEntities = DictionaryQueryData.CreateDictionaryEntities(); + + context.SomeEntities.AddRange(arrayEntities); + await context.SaveChangesAsync(); + } +} + +public class DictionaryQueryData : ISetSource +{ + public IReadOnlyList DictionaryEntities { get; } = CreateDictionaryEntities(); + + public IQueryable Set() + where TEntity : class + { + if (typeof(TEntity) == typeof(DictionaryEntity)) + { + return (IQueryable)DictionaryEntities.AsQueryable(); + } + + throw new InvalidOperationException("Invalid entity type: " + typeof(TEntity)); + } + + public static IReadOnlyList CreateDictionaryEntities() + => + [ + new() + { + Id = 1, + Dictionary = new() { ["key"] = "value" }, + ImmutableDictionary = new Dictionary { ["key2"] = "value2" }.ToImmutableDictionary(), + }, + new() + { + Id = 2, + Dictionary = new() { ["key"] = "value" }, + ImmutableDictionary = new Dictionary { ["key3"] = "value3" }.ToImmutableDictionary(), + } + ]; +} + +public class HstoreQueryFixture : SharedStoreFixtureBase, 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 GetContextCreator() + => CreateContext; + + public ISetSource GetExpectedData() + => _expectedData ??= new DictionaryQueryData(); + + public IReadOnlyDictionary EntitySorters + => new Dictionary> + { + { typeof(DictionaryEntity), e => ((DictionaryEntity)e)?.Id } + }.ToDictionary(e => e.Key, e => (object)e.Value); + + public IReadOnlyDictionary EntityAsserters + => new Dictionary> + { + { + 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); + + } + } + } + }.ToDictionary(e => e.Key, e => (object)e.Value); +} + +public class HstoreQueryTest : QueryTestBase +{ + public HstoreQueryTest(HstoreQueryFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task Dictionary_Contains_key(bool async) + { + var keyToTest = "key"; + await AssertQuery(async, ss => ss.Set().Where(s => s.Dictionary.ContainsKey(keyToTest))); + AssertSql(""" +@__keyToTest_0='key' + +SELECT s."Id", s."Dictionary", s."ImmutableDictionary" +FROM "SomeEntities" AS s +WHERE s."Dictionary" ? @__keyToTest_0 +"""); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task ImmutableDictionary_Contains_key(bool async) + { + var keyToTest = "key3"; + await AssertQuery(async, ss => ss.Set().Where(s => s.ImmutableDictionary.ContainsKey(keyToTest))); + AssertSql( + """ +@__keyToTest_0='key3' + +SELECT s."Id", s."Dictionary", s."ImmutableDictionary" +FROM "SomeEntities" AS s +WHERE s."ImmutableDictionary" ? @__keyToTest_0 +"""); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task Dictionary_Contains_value(bool async) + { + var valueToTest = "value"; + await AssertQuery(async, ss => ss.Set().Where(s => s.Dictionary.ContainsValue(valueToTest))); + AssertSql( + """ +@__valueToTest_0='value' + +SELECT s."Id", s."Dictionary", s."ImmutableDictionary" +FROM "SomeEntities" AS s +WHERE @__valueToTest_0 = ANY (avals(s."Dictionary")) +"""); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task Dictionary_Keys_ToList(bool async) + { + await AssertQuery( + async, ss => ss.Set().Select(s => s.Dictionary.Keys.ToList()), elementAsserter: Assert.Equal, assertOrder: true); + AssertSql( + """ +SELECT akeys(s."Dictionary") +FROM "SomeEntities" AS s +"""); + } + + // Note: There is no "Dictionary_Keys" or "Dictionary_Values" tests as they return a Dictionary.KeyCollection and Dictionary.ValueCollection + // which cannot be translated from a `List` which is what the `avals` and `akeys` functions returns. ImmutableDictionary.Keys and ImmutableDictionary.Values + // does have tests as they return an `IEnumerable` that `List` is compatible with + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task ImmutableDictionary_Keys(bool async) + { + await AssertQuery( + async, ss => ss.Set().Select(s => s.ImmutableDictionary.Keys), elementAsserter: Assert.Equal, assertOrder: true); + AssertSql( + """ +SELECT akeys(s."ImmutableDictionary") +FROM "SomeEntities" AS s +"""); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task ImmutableDictionary_Keys_ToList(bool async) + { + await AssertQuery( + async, ss => ss.Set().Select(s => s.ImmutableDictionary.Keys), elementAsserter: Assert.Equal, assertOrder: true); + AssertSql( + """ +SELECT akeys(s."ImmutableDictionary") +FROM "SomeEntities" AS s +"""); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task Dictionary_Values_ToList(bool async) + { + await AssertQuery( + async, ss => ss.Set().Select(s => s.Dictionary.Values.ToList()), elementAsserter: Assert.Equal, assertOrder: true); + AssertSql( + """ +SELECT avals(s."Dictionary") +FROM "SomeEntities" AS s +"""); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task ImmutableDictionary_Values(bool async) + { + await AssertQuery( + async, ss => ss.Set().Select(s => s.ImmutableDictionary.Values), elementAsserter: Assert.Equal, assertOrder: true); + AssertSql( + """ +SELECT avals(s."ImmutableDictionary") +FROM "SomeEntities" AS s +"""); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task ImmutableDictionary_Values_ToList(bool async) + { + await AssertQuery( + async, ss => ss.Set().Select(s => s.ImmutableDictionary.Values.ToList()), elementAsserter: Assert.Equal, assertOrder: true); + AssertSql( + """ +SELECT avals(s."ImmutableDictionary") +FROM "SomeEntities" AS s +"""); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task Dictionary_Item_equals(bool async) + { + var keyToTest = "key"; + var valueToTest = "value"; + await AssertQuery(async, ss => ss.Set().Where(s => s.Dictionary[keyToTest] == valueToTest)); + AssertSql( + """ +@__valueToTest_0='value' + +SELECT s."Id", s."Dictionary", s."ImmutableDictionary" +FROM "SomeEntities" AS s +WHERE s."Dictionary" -> 'key' = @__valueToTest_0 +"""); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task ImmutableDictionary_Item_equals(bool async) + { + var keyToTest = "key2"; + var valueToTest = "value2"; + await AssertQuery(async, ss => + ss.Set().Where(s => s.ImmutableDictionary[keyToTest] == valueToTest), + ss => ss.Set().Where(s => + s.ImmutableDictionary.ContainsKey(keyToTest) && s.ImmutableDictionary[keyToTest] == valueToTest)); + AssertSql( + """ +@__keyToTest_0='key2' +@__valueToTest_1='value2' + +SELECT s."Id", s."Dictionary", s."ImmutableDictionary" +FROM "SomeEntities" AS s +WHERE s."ImmutableDictionary" -> @__keyToTest_0 = @__valueToTest_1 +"""); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task Dictionary_Where_Count(bool async) + { + await AssertQuery(async, ss => ss.Set().Where(s => s.Dictionary.Count >= 1)); + AssertSql( + """ +SELECT s."Id", s."Dictionary", s."ImmutableDictionary" +FROM "SomeEntities" AS s +WHERE cardinality(akeys(s."Dictionary")) >= 1 +"""); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task Dictionary_Select_Count(bool async) + { + await AssertQuery(async, ss => ss.Set().Select(s => s.Dictionary.Count)); + AssertSql( + """ +SELECT cardinality(akeys(s."Dictionary")) +FROM "SomeEntities" AS s +"""); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task ImmutableDictionary_Where_Count(bool async) + { + await AssertQuery(async, ss => ss.Set().Where(s => s.ImmutableDictionary.Count >= 1)); + AssertSql( + """ +SELECT s."Id", s."Dictionary", s."ImmutableDictionary" +FROM "SomeEntities" AS s +WHERE cardinality(akeys(s."ImmutableDictionary")) >= 1 +"""); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task ImmutableDictionary_Select_Count(bool async) + { + await AssertQuery(async, ss => ss.Set().Select(s => s.ImmutableDictionary.Count)); + AssertSql( + """ +SELECT cardinality(akeys(s."ImmutableDictionary")) +FROM "SomeEntities" AS s +"""); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task ImmutableDictionary_Where_IsEmpty(bool async) + { + await AssertQuery(async, ss => ss.Set().Where(s => !s.ImmutableDictionary.IsEmpty)); + AssertSql( + """ +SELECT s."Id", s."Dictionary", s."ImmutableDictionary" +FROM "SomeEntities" AS s +WHERE cardinality(akeys(s."ImmutableDictionary")) <> 0 +"""); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task Dictionary_Where_Any(bool async) + { + await AssertQuery(async, ss => ss.Set().Where(s => s.Dictionary.Any())); + AssertSql( + """ +SELECT s."Id", s."Dictionary", s."ImmutableDictionary" +FROM "SomeEntities" AS s +WHERE cardinality(akeys(s."Dictionary")) <> 0 +"""); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public async Task ImmutableDictionary_Where_Any(bool async) + { + await AssertQuery(async, ss => ss.Set().Where(s => s.ImmutableDictionary.Any())); + AssertSql( + """ +SELECT s."Id", s."Dictionary", s."ImmutableDictionary" +FROM "SomeEntities" AS s +WHERE cardinality(akeys(s."ImmutableDictionary")) <> 0 +"""); + } + + protected void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +}